@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
@@ -7,6 +7,38 @@
7
7
  // channel (see render-contract.ts RendererError composition).
8
8
 
9
9
  import type { DeviceScopeReceipt } from '../device/resource-types';
10
+ import type { ReflectionFallbackRecoveryAction } from '../inspection-types';
11
+ import type { SsrAdmissionError, SsrOwnerRecoveryAction } from '../ssr/errors';
12
+
13
+ export type { ReflectionFallbackRecoveryAction } from '../inspection-types';
14
+
15
+ export function reflectionFallbackRecoveryAction(
16
+ source: 'probe' | 'skylight' | 'neutral',
17
+ hasLastKnownGood: boolean,
18
+ ): ReflectionFallbackRecoveryAction {
19
+ if (source === 'probe' && hasLastKnownGood) return 'use-LKG';
20
+ if (source === 'skylight') return 'use-Skylight';
21
+ return 'use-neutral';
22
+ }
23
+
24
+ /**
25
+ * Maps a detached admission failure to the owning producer action. This is
26
+ * an instruction projection only; SSR never performs the recovery itself.
27
+ */
28
+ export function ssrAdmissionRecoveryAction(error: SsrAdmissionError): SsrOwnerRecoveryAction {
29
+ switch (error.code) {
30
+ case 'ssr-not-requested':
31
+ case 'ssr-receipt-stale':
32
+ return 'retry';
33
+ case 'ssr-reflection-fallback-unavailable':
34
+ return error.detail.action;
35
+ case 'ssr-format-unavailable':
36
+ case 'ssr-temporal-unavailable':
37
+ return error.detail.action;
38
+ case 'ssr-receipt-identity-mismatch':
39
+ return 'rebuild';
40
+ }
41
+ }
10
42
 
11
43
  // ── RecoverError (feat-20260621-renderer-health-recover-skeleton M1) ─────────
12
44
 
@@ -116,6 +116,60 @@ export function createRenderSurfaceError(
116
116
  return new RenderSurfaceError(`surface-${kind}-failed` as SurfaceFailureCode, detail);
117
117
  }
118
118
 
119
+ export type LightResourceFailureReason = 'asset' | 'format' | 'capability' | 'capacity';
120
+
121
+ export interface LightResourceUnavailableDetail {
122
+ readonly entity: number;
123
+ readonly feature: 'ies' | 'cookie' | 'probe';
124
+ readonly generation: number;
125
+ readonly sourceKey: string;
126
+ readonly reason: LightResourceFailureReason;
127
+ }
128
+
129
+ export class LightResourceUnavailableError extends Error {
130
+ readonly code = 'light-resource-unavailable' as const;
131
+ readonly expected: string;
132
+ readonly hint: string;
133
+ readonly detail: LightResourceUnavailableDetail;
134
+
135
+ constructor(
136
+ detail: LightResourceUnavailableDetail,
137
+ expected: string,
138
+ actual: string,
139
+ hint: string,
140
+ ) {
141
+ super(`light-resource-unavailable: ${detail.feature} ${actual}`);
142
+ this.name = 'LightResourceUnavailableError';
143
+ this.expected = expected;
144
+ this.hint = hint;
145
+ this.detail = detail;
146
+ }
147
+ }
148
+
149
+ export function createLightResourceUnavailable(input: {
150
+ readonly entity: number;
151
+ readonly feature: LightResourceUnavailableDetail['feature'];
152
+ readonly generation: number;
153
+ readonly sourceKey: string;
154
+ readonly reason: LightResourceFailureReason;
155
+ readonly expected: string;
156
+ readonly actual: string;
157
+ readonly hint: string;
158
+ }): LightResourceUnavailableError {
159
+ return new LightResourceUnavailableError(
160
+ {
161
+ entity: input.entity,
162
+ feature: input.feature,
163
+ generation: input.generation,
164
+ sourceKey: input.sourceKey,
165
+ reason: input.reason,
166
+ },
167
+ input.expected,
168
+ input.actual,
169
+ input.hint,
170
+ );
171
+ }
172
+
119
173
  export interface TemporalFrameSubmitDetail {
120
174
  readonly reason: 'queue-submit-failed';
121
175
  }
@@ -132,7 +186,6 @@ export class TemporalFrameSubmitError extends Error {
132
186
  this.detail = { reason };
133
187
  }
134
188
  }
135
-
136
189
  // ── ShadowInvalidConfigError ──────────────────────────────────────────────
137
190
 
138
191
  /**
@@ -1821,6 +1874,7 @@ export type RenderError =
1821
1874
  | PointsLinesMaterialUnsupportedError
1822
1875
  | PointsLinesBudgetExceededError
1823
1876
  | PointsLinesPrepareFailedError
1877
+ | LightResourceUnavailableError
1824
1878
  | RenderTargetDescriptorInvalidError
1825
1879
  | RenderTargetCapabilityMissingError
1826
1880
  | RenderTargetStateInvalidError
@@ -1,7 +1,7 @@
1
1
  import type { EntityHandle, World } from '@forgeax/engine-ecs';
2
2
  import { readRenderArrayView } from '@forgeax/engine-ecs/projection';
3
3
  import { mat4, vec3 } from '@forgeax/engine-math';
4
- import { Transform } from '@forgeax/engine-scene';
4
+ import { GlobalTransform } from '@forgeax/engine-scene';
5
5
  import {
6
6
  antialiasFromF32,
7
7
  bloomEnabledFromF32,
@@ -14,7 +14,7 @@ import type { CameraSnapshot } from '../render-contract';
14
14
  import { getActiveCamera, selectActiveCameraIndex } from '../systems/active-camera';
15
15
 
16
16
  function readWorldMatrix(world: World, entity: EntityHandle): Float32Array | undefined {
17
- const view = readRenderArrayView(world, entity, Transform, 'world');
17
+ const view = readRenderArrayView(world, entity, GlobalTransform, 'world');
18
18
  return view === undefined ? undefined : new Float32Array(view);
19
19
  }
20
20
 
@@ -23,7 +23,7 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
23
23
  const cameras: CameraSnapshot[] = [];
24
24
  const cameraEntities: number[] = [];
25
25
  const cameraQuery = world
26
- .query({ read: [Camera], optional: [MotionBlur], with: [Transform] })
26
+ .query({ read: [Camera], optional: [MotionBlur], with: [GlobalTransform] })
27
27
  .unwrap();
28
28
  for (const row of cameraQuery) {
29
29
  const cam = row.get(Camera);
@@ -0,0 +1,108 @@
1
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
2
+ import type { MeshAsset } from '@forgeax/engine-types';
3
+ import {
4
+ type GpuDrivenDrawRange,
5
+ type PreparedGpuDrivenDraw,
6
+ type PrepareGpuDrivenDrawInput,
7
+ prepareGpuDrivenDraw,
8
+ } from '../gpu-driven/prepared-draw';
9
+ import type { MaterialSnapshot } from '../render-system-extract';
10
+
11
+ export {
12
+ GpuDrivenPreparationError,
13
+ type GpuDrivenPreparationErrorCode,
14
+ type GpuDrivenPreparationErrorDetail,
15
+ type GpuDrivenPreparationReason,
16
+ } from '../errors/gpu-driven';
17
+ export {
18
+ type GpuDrivenDrawRange,
19
+ type GpuDrivenGeometryReceipt,
20
+ type GpuDrivenSkinReceipt,
21
+ type PreparedGpuDrivenDraw,
22
+ type PrepareGpuDrivenDrawInput,
23
+ prepareGpuDrivenDraw,
24
+ } from '../gpu-driven/prepared-draw';
25
+
26
+ export interface GpuDrivenDrawSnapshot {
27
+ readonly kind: GpuDrivenDrawRange['kind'];
28
+ readonly first: GpuDrivenDrawRange['first'];
29
+ readonly count: GpuDrivenDrawRange['count'];
30
+ readonly baseVertex: GpuDrivenDrawRange['baseVertex'];
31
+ readonly materialSlot: number;
32
+ readonly topology: GpuDrivenDrawRange['topology'];
33
+ readonly pipelineClass: string;
34
+ readonly materialResourceClass: string;
35
+ /** Producer-owned prepared ABI facts used by the PBR GPU lane. */
36
+ readonly prepared?: PreparedGpuDrivenDraw;
37
+ }
38
+
39
+ /**
40
+ * Resolves prepared facts once at extract time; record never re-reads assets.
41
+ * The resolver surfaces UV, reflection, vertex, alpha, resource, and
42
+ * generation failures as GpuDrivenPreparationError before recording.
43
+ */
44
+ export function resolvePreparedGpuDrivenDraw(input: PrepareGpuDrivenDrawInput):
45
+ | { readonly ok: true; readonly value: PreparedGpuDrivenDraw }
46
+ | {
47
+ readonly ok: false;
48
+ readonly error: import('../errors/gpu-driven').GpuDrivenPreparationError;
49
+ } {
50
+ return prepareGpuDrivenDraw(input);
51
+ }
52
+
53
+ /** Keeps the artifact type visible at this owner boundary for contract checks. */
54
+ export type PreparedGpuDrivenArtifact = MaterialShaderArtifact;
55
+
56
+ /**
57
+ * Derives the resource identity consumed by the existing GPU-driven batch
58
+ * owner. The extract stage is the only place that resolves material values;
59
+ * this helper keeps the batch adapter from re-reading assets later.
60
+ */
61
+ export function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
62
+ const textures = [...(material.textureHandles?.entries() ?? [])]
63
+ .map(([name, handle]) => [name, Number(handle)] as const)
64
+ .sort(([left], [right]) => left.localeCompare(right));
65
+ const samplers = [...(material.samplerHandles?.entries() ?? [])]
66
+ .map(([name, handle]) => [name, Number(handle)] as const)
67
+ .sort(([left], [right]) => left.localeCompare(right));
68
+ return JSON.stringify({
69
+ textures,
70
+ samplers,
71
+ video: [...(material.videoTextureFields?.keys() ?? [])].sort(),
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Projects already-resolved mesh facts into the existing GPU-driven draw
77
+ * snapshot. No World or AssetRegistry access is permitted at this boundary.
78
+ */
79
+ export function buildGpuDrivenDraws(input: {
80
+ readonly submeshes: readonly MeshAsset['submeshes'][number][];
81
+ readonly indexed: boolean;
82
+ readonly materials: readonly MaterialSnapshot[];
83
+ readonly fallbackMaterial: MaterialSnapshot;
84
+ readonly prepare?: (
85
+ draw: GpuDrivenDrawSnapshot,
86
+ material: MaterialSnapshot,
87
+ ) => PreparedGpuDrivenDraw | undefined;
88
+ }): GpuDrivenDrawSnapshot[] {
89
+ let nonIndexedFirst = 0;
90
+ return input.submeshes.flatMap((submesh) => {
91
+ const drawMaterial = input.materials[submesh.materialSlot] ?? input.fallbackMaterial;
92
+ const first = input.indexed ? submesh.indexOffset : nonIndexedFirst;
93
+ nonIndexedFirst += submesh.vertexCount;
94
+ if (drawMaterial.transparent === true) return [];
95
+ const draw: GpuDrivenDrawSnapshot = {
96
+ kind: input.indexed ? ('indexed' as const) : ('non-indexed' as const),
97
+ first,
98
+ count: input.indexed ? submesh.indexCount : submesh.vertexCount,
99
+ baseVertex: 0,
100
+ materialSlot: submesh.materialSlot,
101
+ topology: submesh.topology,
102
+ pipelineClass: `${drawMaterial.materialShaderId ?? 'forgeax::default-unlit'}|${submesh.topology}|${JSON.stringify(drawMaterial.renderState ?? null)}`,
103
+ materialResourceClass: gpuDrivenMaterialResourceClass(drawMaterial),
104
+ };
105
+ const prepared = input.prepare?.(draw, drawMaterial);
106
+ return [prepared === undefined ? draw : { ...draw, prepared }];
107
+ });
108
+ }
@@ -1,5 +1,4 @@
1
1
  import type { EntityHandle, World } from '@forgeax/engine-ecs';
2
- import { createWorldProjection } from '@forgeax/engine-ecs/projection';
3
2
  import {
4
3
  projectHierarchy,
5
4
  type SceneHierarchyDiagnostic,
@@ -38,14 +37,6 @@ interface ResolutionState {
38
37
  readonly source: VisibilitySource;
39
38
  }
40
39
 
41
- interface VisibilityCacheEntry {
42
- readonly changes: ReturnType<typeof createWorldProjection>;
43
- readonly hierarchy: SceneHierarchySnapshot;
44
- readonly snapshot: VisibilitySnapshot;
45
- }
46
-
47
- const VISIBILITY_CACHE = new WeakMap<World, VisibilityCacheEntry>();
48
-
49
40
  /**
50
41
  * Resolve author intent against the scene-owned valid parent projection.
51
42
  * Diagnostics are preserved for callers to repair hierarchy input before retry.
@@ -54,19 +45,6 @@ export function resolveVisibility(
54
45
  world: World,
55
46
  hierarchy: SceneHierarchySnapshot = projectHierarchy(world),
56
47
  ): VisibilitySnapshot {
57
- const cached = VISIBILITY_CACHE.get(world);
58
- const changes = cached?.changes ?? createWorldProjection(world, { components: [Visibility] });
59
- if (cached !== undefined) {
60
- const evidence = changes.poll();
61
- if (
62
- evidence.status === 'delta' &&
63
- evidence.changes.length === 0 &&
64
- cached.hierarchy === hierarchy
65
- ) {
66
- return cached.snapshot;
67
- }
68
- }
69
-
70
48
  // Visibility is an opt-in component. Query only the owning archetypes
71
49
  // instead of walking every Entity row with an optional column; the latter
72
50
  // made a World with no visibility intents pay a full-scene scan on each
@@ -158,6 +136,5 @@ export function resolveVisibility(
158
136
  return resolveEntity?.(entity)?.effective ?? 'visible';
159
137
  },
160
138
  };
161
- VISIBILITY_CACHE.set(world, { changes, hierarchy, snapshot });
162
139
  return snapshot;
163
140
  }
@@ -1,6 +1,10 @@
1
1
  import type { PrimitiveTopology } from '@forgeax/engine-types';
2
2
  import type { BatchTopologyInspection } from '../inspection-types';
3
+ import type { CameraSnapshot } from '../render-contract';
3
4
  import type { RenderSceneApplyResult, RenderSceneSlot } from '../scene/render-scene-types';
5
+ import { type GpuLodRow, selectGpuLod } from '../scene/visibility/gpu-lod';
6
+ import { projectedHeight as measureProjectedHeight } from '../scene/visibility/lod-selector';
7
+ import type { PreparedGpuDrivenDraw } from './prepared-draw';
4
8
 
5
9
  export type { BatchTopologyInspection } from '../inspection-types';
6
10
 
@@ -14,6 +18,12 @@ export interface GpuDrivenBatchKey {
14
18
  readonly topology: PrimitiveTopology;
15
19
  readonly pipelineClass: string;
16
20
  readonly materialResourceClass: string;
21
+ /** Stable producer-owned identity; topology never derives this from a shader name. */
22
+ readonly preparedIdentity?: string;
23
+ /** Resource identity projected by the prepared material contract. */
24
+ readonly resourceIdentity?: string;
25
+ /** Closed admission lane for the prepared Standard PBR draw. */
26
+ readonly admission?: 'opaque' | 'alpha-mask';
17
27
  }
18
28
 
19
29
  export interface GpuDrivenCandidate {
@@ -21,6 +31,16 @@ export interface GpuDrivenCandidate {
21
31
  readonly generation: number;
22
32
  readonly drawItemIndex: number;
23
33
  readonly instanceOrdinal: number;
34
+ /** Absolute coverage rows uploaded to the GPU selector; root is implicit. */
35
+ readonly lodCoverages?: readonly number[];
36
+ readonly lodHysteresis?: number;
37
+ readonly projectedHeight?: number;
38
+ readonly lodRanges?: readonly {
39
+ readonly first: number;
40
+ readonly count: number;
41
+ readonly baseVertex: number;
42
+ }[];
43
+ readonly prepared?: PreparedGpuDrivenDraw;
24
44
  }
25
45
 
26
46
  export interface GpuDrivenBatch {
@@ -40,6 +60,32 @@ export interface SubmissionPlan {
40
60
  readonly visibleCapacity: number;
41
61
  }
42
62
 
63
+ export interface GpuLodCandidateInput {
64
+ readonly candidate: GpuDrivenCandidate;
65
+ readonly rows: readonly GpuLodRow[];
66
+ readonly projectedHeight: number;
67
+ readonly previousLevel: number;
68
+ readonly historyValid: boolean;
69
+ }
70
+
71
+ export interface GpuLodCompactCandidate {
72
+ readonly candidate: GpuDrivenCandidate;
73
+ readonly level: number;
74
+ readonly confidence: number;
75
+ }
76
+
77
+ /** Selects one level per stable topology candidate without mutating membership. */
78
+ export function compactGpuLodCandidates(
79
+ inputs: readonly GpuLodCandidateInput[],
80
+ ): readonly GpuLodCompactCandidate[] {
81
+ return Object.freeze(
82
+ inputs.map((input) => {
83
+ const selection = selectGpuLod(input.rows, input);
84
+ return Object.freeze({ candidate: input.candidate, ...selection });
85
+ }),
86
+ );
87
+ }
88
+
43
89
  interface MutableBatch {
44
90
  readonly batchId: number;
45
91
  readonly generation: number;
@@ -56,27 +102,97 @@ function eligibleKeys(slot: RenderSceneSlot): readonly GpuDrivenBatchKey[] {
56
102
  snapshot.instances?.instanceCount === 0 ||
57
103
  snapshot.localAabb === undefined ||
58
104
  snapshot.skin !== undefined ||
105
+ snapshot.morph !== undefined ||
59
106
  snapshot.spriteInstances !== undefined
60
107
  ) {
61
108
  return [];
62
109
  }
63
- return draws.map((draw) => ({
64
- assetHandle: snapshot.assetHandle,
65
- drawKind: draw.kind,
66
- first: draw.first,
67
- count: draw.count,
68
- baseVertex: draw.baseVertex,
69
- materialSlot: draw.materialSlot,
70
- topology: draw.topology,
71
- pipelineClass: draw.pipelineClass,
72
- materialResourceClass: draw.materialResourceClass,
73
- }));
110
+ return draws.flatMap((draw) => {
111
+ const prepared = draw.prepared;
112
+ const material = snapshot.materials[draw.materialSlot] ?? snapshot.material;
113
+ if (prepared !== undefined) {
114
+ const isStandardPbr =
115
+ (material.materialShaderId === 'forgeax::default-standard-pbr' &&
116
+ prepared.identity.material === 'forgeax::default-standard-pbr') ||
117
+ (material.materialShaderId === 'forgeax::default-standard-pbr-skin' &&
118
+ prepared.identity.material === 'forgeax::default-standard-pbr-skin');
119
+ const isAlphaBlend =
120
+ material.transparent === true || material.renderState?.blend !== undefined;
121
+ if (!isStandardPbr || isAlphaBlend || prepared.identity.deformation !== 'rigid') return [];
122
+ const preparedIdentity = `${prepared.identity.material}|${prepared.identity.geometry}|${prepared.identity.deformation}`;
123
+ const admission =
124
+ typeof material.paramSnapshot?.alphaCutoff === 'number' ? 'alpha-mask' : 'opaque';
125
+ return [
126
+ {
127
+ assetHandle: snapshot.assetHandle,
128
+ drawKind: draw.kind,
129
+ first: prepared.first,
130
+ count: prepared.count,
131
+ baseVertex: prepared.baseVertex,
132
+ materialSlot: draw.materialSlot,
133
+ topology: prepared.topology,
134
+ pipelineClass: preparedIdentity,
135
+ materialResourceClass: draw.materialResourceClass,
136
+ preparedIdentity,
137
+ resourceIdentity: draw.materialResourceClass,
138
+ admission,
139
+ },
140
+ ];
141
+ }
142
+ return [
143
+ {
144
+ assetHandle: snapshot.assetHandle,
145
+ drawKind: draw.kind,
146
+ first: draw.first,
147
+ count: draw.count,
148
+ baseVertex: draw.baseVertex,
149
+ materialSlot: draw.materialSlot,
150
+ topology: draw.topology,
151
+ pipelineClass: draw.pipelineClass,
152
+ materialResourceClass: draw.materialResourceClass,
153
+ },
154
+ ];
155
+ });
74
156
  }
75
157
 
76
158
  function keyText(key: GpuDrivenBatchKey): string {
77
159
  return JSON.stringify(key);
78
160
  }
79
161
 
162
+ /**
163
+ * A GPU indirect draw has one index/vertex range for the whole batch. LOD
164
+ * selection is per candidate, so sharing a batch between LOD candidates would
165
+ * let `finalizeView` pick the first candidate's range for every visible
166
+ * instance. Keep LOD candidates in single-candidate batches; ordinary meshes
167
+ * retain the existing compatibility batching path.
168
+ */
169
+ function candidateBatchText(
170
+ key: GpuDrivenBatchKey,
171
+ primitiveIndex: number,
172
+ drawItemIndex: number,
173
+ instanceOrdinal: number,
174
+ hasLod: boolean,
175
+ ): string {
176
+ return hasLod
177
+ ? `${keyText(key)}|lod:${primitiveIndex}:${drawItemIndex}:${instanceOrdinal}`
178
+ : keyText(key);
179
+ }
180
+
181
+ function membershipTexts(slot: RenderSceneSlot): readonly string[] {
182
+ const keys = eligibleKeys(slot);
183
+ const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
184
+ const hasLod = (slot.snapshot.lods?.length ?? 0) > 0;
185
+ const texts: string[] = [];
186
+ for (let drawItemIndex = 0; drawItemIndex < keys.length; drawItemIndex += 1) {
187
+ const key = keys[drawItemIndex];
188
+ if (key === undefined) continue;
189
+ for (let instanceOrdinal = 0; instanceOrdinal < instanceCount; instanceOrdinal += 1) {
190
+ texts.push(candidateBatchText(key, slot.slot, drawItemIndex, instanceOrdinal, hasLod));
191
+ }
192
+ }
193
+ return texts;
194
+ }
195
+
80
196
  function alignInstanceBase(value: number): number {
81
197
  // Raster binds each batch's visible-index and compact-transform segment at
82
198
  // a static storage-buffer offset. WebGPU requires those offsets to satisfy
@@ -86,6 +202,42 @@ function alignInstanceBase(value: number): number {
86
202
  return Math.ceil(value / 64) * 64;
87
203
  }
88
204
 
205
+ export function projectedHeightForCandidate(slot: RenderSceneSlot, camera: CameraSnapshot): number {
206
+ const aabb = slot.snapshot.localAabb;
207
+ if (aabb === undefined || aabb.length < 6) return Number.NaN;
208
+ const halfX = Math.abs((aabb[3] ?? 0) - (aabb[0] ?? 0)) * 0.5;
209
+ const halfY = Math.abs((aabb[4] ?? 0) - (aabb[1] ?? 0)) * 0.5;
210
+ const halfZ = Math.abs((aabb[5] ?? 0) - (aabb[2] ?? 0)) * 0.5;
211
+ const world = slot.snapshot.transform.world;
212
+ // Transform the local AABB's half extents through the absolute value of the
213
+ // world 3x3. This is the conservative world-space sphere bound for every
214
+ // rotation and signed/non-uniform scale, and keeps CPU projection aligned
215
+ // with the world transform uploaded to the GPU selector.
216
+ const worldHalfX =
217
+ Math.abs(world[0] ?? 0) * halfX +
218
+ Math.abs(world[4] ?? 0) * halfY +
219
+ Math.abs(world[8] ?? 0) * halfZ;
220
+ const worldHalfY =
221
+ Math.abs(world[1] ?? 0) * halfX +
222
+ Math.abs(world[5] ?? 0) * halfY +
223
+ Math.abs(world[9] ?? 0) * halfZ;
224
+ const worldHalfZ =
225
+ Math.abs(world[2] ?? 0) * halfX +
226
+ Math.abs(world[6] ?? 0) * halfY +
227
+ Math.abs(world[10] ?? 0) * halfZ;
228
+ const radius = Math.hypot(worldHalfX, worldHalfY, worldHalfZ);
229
+ const dx = (world[12] ?? 0) - (camera.position[0] ?? 0);
230
+ const dy = (world[13] ?? 0) - (camera.position[1] ?? 0);
231
+ const dz = (world[14] ?? 0) - (camera.position[2] ?? 0);
232
+ return measureProjectedHeight({
233
+ radius,
234
+ depth: Math.hypot(dx, dy, dz),
235
+ projection: camera.projection,
236
+ fov: camera.fov,
237
+ orthoHeight: Math.abs(camera.orthoTop - camera.orthoBottom),
238
+ });
239
+ }
240
+
89
241
  /** Stable compatibility grouping; per-view visibility never mutates this owner. */
90
242
  export class BatchTopology {
91
243
  private readonly batches = new Map<string, MutableBatch>();
@@ -120,15 +272,17 @@ export class BatchTopology {
120
272
  changed = this.remove(slot.slot) || changed;
121
273
  changed = this.add(slot) || changed;
122
274
  }
275
+ // Most updated slots are transform-only. A full renderable update can
276
+ // still carry a changed material/geometry/draw topology, so compare the
277
+ // stable batch memberships and patch only when that key actually changed.
278
+ // This keeps moving entities on the zero-work transform lane while making
279
+ // same-entity resource edits visible to the GPU-driven owner.
123
280
  for (const slot of delta.updatedSlots) {
124
- const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
125
- const nextTexts = eligibleKeys(slot).flatMap((key) =>
126
- Array.from({ length: instanceCount }, () => keyText(key)),
127
- );
128
- const current = this.membershipByPrimitive.get(slot.slot) ?? [];
281
+ const previous = this.membershipByPrimitive.get(slot.slot) ?? [];
282
+ const next = membershipTexts(slot);
129
283
  if (
130
- current.length === nextTexts.length &&
131
- current.every((membership, index) => membership.batchText === nextTexts[index])
284
+ previous.length === next.length &&
285
+ previous.every((membership, index) => membership.batchText === next[index])
132
286
  ) {
133
287
  continue;
134
288
  }
@@ -199,26 +353,50 @@ export class BatchTopology {
199
353
  this.ineligiblePrimitives.delete(slot.slot);
200
354
  const memberships: Array<{ batchText: string; candidateKey: string }> = [];
201
355
  const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
356
+ const hasLod = (slot.snapshot.lods?.length ?? 0) > 0;
202
357
  for (let drawItemIndex = 0; drawItemIndex < keys.length; drawItemIndex += 1) {
203
358
  const key = keys[drawItemIndex];
204
359
  if (key === undefined) continue;
205
- const text = keyText(key);
206
- let batch = this.batches.get(text);
207
- if (batch === undefined) {
208
- const reused = this.freeBatchIds.pop();
209
- const batchId = reused ?? this.generationByBatchId.length;
210
- const generation = reused === undefined ? 0 : (this.generationByBatchId[batchId] ?? -1) + 1;
211
- this.generationByBatchId[batchId] = generation;
212
- batch = { batchId, generation, key, candidates: new Map() };
213
- this.batches.set(text, batch);
214
- }
215
360
  for (let instanceOrdinal = 0; instanceOrdinal < instanceCount; instanceOrdinal += 1) {
361
+ const text = candidateBatchText(key, slot.slot, drawItemIndex, instanceOrdinal, hasLod);
362
+ let batch = this.batches.get(text);
363
+ if (batch === undefined) {
364
+ const reused = this.freeBatchIds.pop();
365
+ const batchId = reused ?? this.generationByBatchId.length;
366
+ const generation =
367
+ reused === undefined ? 0 : (this.generationByBatchId[batchId] ?? -1) + 1;
368
+ this.generationByBatchId[batchId] = generation;
369
+ batch = { batchId, generation, key, candidates: new Map() };
370
+ this.batches.set(text, batch);
371
+ }
216
372
  const candidateKey = `${slot.slot}:${drawItemIndex}:${instanceOrdinal}`;
373
+ const draw = slot.snapshot.gpuDrivenDraws?.[drawItemIndex];
374
+ const prepared = draw?.prepared;
217
375
  batch.candidates.set(candidateKey, {
218
376
  primitiveIndex: slot.slot,
219
377
  generation: slot.generation,
220
378
  drawItemIndex,
221
379
  instanceOrdinal,
380
+ ...(draw?.projectedHeight === undefined ? {} : { projectedHeight: draw.projectedHeight }),
381
+ ...(slot.snapshot.lods === undefined
382
+ ? {}
383
+ : {
384
+ lodCoverages: [1, ...slot.snapshot.lods.map((lod) => lod.screenCoverage)],
385
+ ...(slot.snapshot.gpuDrivenDraws?.[drawItemIndex]?.lodRanges === undefined
386
+ ? {}
387
+ : {
388
+ lodRanges: slot.snapshot.gpuDrivenDraws[drawItemIndex]
389
+ ?.lodRanges as readonly {
390
+ readonly first: number;
391
+ readonly count: number;
392
+ readonly baseVertex: number;
393
+ }[],
394
+ }),
395
+ ...(slot.snapshot.lodHysteresis === undefined
396
+ ? {}
397
+ : { lodHysteresis: slot.snapshot.lodHysteresis }),
398
+ }),
399
+ ...(prepared === undefined ? {} : { prepared }),
222
400
  });
223
401
  memberships.push({ batchText: text, candidateKey });
224
402
  }