@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
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ applyConfidenceEvent,
4
+ createConfidenceState,
5
+ type InvalidationReason,
6
+ } from '../scene/visibility/occlusion-confidence';
7
+
8
+ describe('occlusion confidence invalidation matrix', () => {
9
+ it.each([
10
+ 'cut',
11
+ 'teleport',
12
+ 'resize',
13
+ 'projection',
14
+ 'primitive',
15
+ 'bounds',
16
+ 'topology',
17
+ 'world-reorder',
18
+ 'detach',
19
+ 'failed-submit',
20
+ 'device-loss',
21
+ ] satisfies readonly InvalidationReason[])('invalidates %s without suppressing the facet', (reason) => {
22
+ const state = applyConfidenceEvent(
23
+ applyConfidenceEvent(createConfidenceState(), {
24
+ type: 'result',
25
+ samples: 0,
26
+ submissionGeneration: 1,
27
+ }),
28
+ { type: 'result', samples: 0, submissionGeneration: 2 },
29
+ );
30
+ const next = applyConfidenceEvent(state, { type: 'invalidate', reason });
31
+ expect(next).toMatchObject({
32
+ status: 'visible',
33
+ zeroStreak: 0,
34
+ queryable: reason !== 'detach',
35
+ });
36
+ });
37
+ });
@@ -0,0 +1,16 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const typedPrimitivesSource = readFileSync(
5
+ new URL('../typed-render-graph-primitives.ts', import.meta.url),
6
+ 'utf8',
7
+ );
8
+
9
+ describe('typed scene occlusion pass routing', () => {
10
+ it('only forwards a projection to the pass that owns its query set', () => {
11
+ expect(typedPrimitivesSource).toContain('const activeOcclusion = options.occlusion;');
12
+ expect(typedPrimitivesSource).not.toContain(
13
+ '...(internal.occlusion === undefined ? {} : { occlusion: internal.occlusion })',
14
+ );
15
+ });
16
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildOcclusionPassPlan } from '../scene/visibility/occlusion-pass';
3
+
4
+ describe('occlusion pass Dawn contract', () => {
5
+ it('keeps resolve, copy, and async map in one shared submission plan', () => {
6
+ const plan = buildOcclusionPassPlan({
7
+ opaquePass: 'opaque',
8
+ depthResource: 'depth',
9
+ colorResource: 'color',
10
+ bounds: { min: [-1, -1, -1], max: [1, 1, 1] },
11
+ queryIndex: 0,
12
+ });
13
+ expect(plan).toMatchObject({
14
+ resolve: { queryIndex: 0 },
15
+ copy: { asynchronous: true },
16
+ submitCount: 1,
17
+ mapSameFrame: false,
18
+ });
19
+ });
20
+ });
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildOcclusionPassPlan } from '../scene/visibility/occlusion-pass';
3
+
4
+ const bounds = { min: [-1, -1, -1] as const, max: [1, 1, 1] as const };
5
+
6
+ describe('typed occlusion bounds pass', () => {
7
+ it('is ordered after opaque and only reads depth through a conservative proxy', () => {
8
+ expect(
9
+ buildOcclusionPassPlan({
10
+ opaquePass: 'opaque-main',
11
+ depthResource: 'depth-main',
12
+ colorResource: 'color-main',
13
+ bounds,
14
+ queryIndex: 3,
15
+ }),
16
+ ).toMatchObject({
17
+ after: 'opaque-main',
18
+ queryIndex: 3,
19
+ depth: { resource: 'depth-main', usage: 'depth-read-only' },
20
+ writesColor: false,
21
+ writesDepth: false,
22
+ submitCount: 1,
23
+ });
24
+ });
25
+
26
+ it.each([
27
+ { transparent: true },
28
+ { deformed: true },
29
+ { bounds: undefined },
30
+ ])('keeps non-queryable candidates visible: %o', (input) => {
31
+ expect(
32
+ buildOcclusionPassPlan({
33
+ opaquePass: 'opaque-main',
34
+ depthResource: 'depth-main',
35
+ colorResource: 'color-main',
36
+ bounds: input.bounds === undefined ? undefined : bounds,
37
+ queryIndex: 0,
38
+ ...(input.transparent === undefined ? {} : { transparent: input.transparent }),
39
+ ...(input.deformed === undefined ? {} : { deformed: input.deformed }),
40
+ }),
41
+ ).toBeUndefined();
42
+ });
43
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildOcclusionPassPlan } from '../scene/visibility/occlusion-pass';
3
+
4
+ describe('occlusion pass RhiNull structural contract', () => {
5
+ it('does not create color/depth writes and keeps query index separate from identity', () => {
6
+ const plan = buildOcclusionPassPlan({
7
+ opaquePass: 'opaque',
8
+ depthResource: 'depth',
9
+ colorResource: 'color',
10
+ bounds: { min: [-2, -2, -2], max: [2, 2, 2] },
11
+ queryIndex: 17,
12
+ identity: { primitiveSlot: 4, slotGeneration: 9 },
13
+ });
14
+ expect(plan).toMatchObject({
15
+ queryIndex: 17,
16
+ identity: { primitiveSlot: 4, slotGeneration: 9 },
17
+ writesColor: false,
18
+ writesDepth: false,
19
+ });
20
+ expect(plan?.depth.usage).toBe('depth-read-only');
21
+ });
22
+ });
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OcclusionQueryPool } from '../scene/visibility/occlusion-query-pool';
3
+
4
+ describe('occlusion query RhiNull structure', () => {
5
+ it('disposes pages and reuses them without exposing query identity', () => {
6
+ const pool = new OcclusionQueryPool();
7
+ expect(pool.inspect().disposed).toBe(false);
8
+ pool.dispose();
9
+ expect(pool.inspect()).toMatchObject({ disposed: true, inFlight: 0, availablePages: 0 });
10
+ expect(
11
+ pool.reserve({
12
+ viewKey: 'view',
13
+ attachmentId: 'attachment',
14
+ deviceGeneration: 1,
15
+ worldGeneration: 1,
16
+ primitiveSlot: 0,
17
+ slotGeneration: 1,
18
+ }),
19
+ ).toBeUndefined();
20
+ });
21
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ OCCLUSION_QUERY_PAGE_BYTES,
4
+ OCCLUSION_QUERY_PAGE_COUNT,
5
+ OCCLUSION_QUERY_PAGE_INDEX_LIMIT,
6
+ OcclusionQueryPool,
7
+ } from '../scene/visibility/occlusion-query-pool';
8
+
9
+ const identity = (index: number) => ({
10
+ viewKey: `view-${index}`,
11
+ attachmentId: 'attachment-1',
12
+ deviceGeneration: 4,
13
+ worldGeneration: 2,
14
+ primitiveSlot: index,
15
+ slotGeneration: 9,
16
+ });
17
+
18
+ describe('occlusion query pool budgets', () => {
19
+ it('owns three bounded pages with 4096 indices and 32 KiB resolve/staging each', () => {
20
+ const pool = new OcclusionQueryPool();
21
+ expect(pool.inspect()).toMatchObject({
22
+ pageCount: OCCLUSION_QUERY_PAGE_COUNT,
23
+ pageIndexLimit: OCCLUSION_QUERY_PAGE_INDEX_LIMIT,
24
+ resolveBytes: OCCLUSION_QUERY_PAGE_BYTES,
25
+ stagingBytes: OCCLUSION_QUERY_PAGE_BYTES,
26
+ });
27
+ });
28
+
29
+ it('fills one page to the 4096-index boundary before advancing fairly', () => {
30
+ const pool = new OcclusionQueryPool();
31
+ const firstPage = Array.from({ length: OCCLUSION_QUERY_PAGE_INDEX_LIMIT }, (_, index) =>
32
+ pool.reserve(identity(index)),
33
+ );
34
+ expect(firstPage.every((reservation) => reservation !== undefined)).toBe(true);
35
+ expect(firstPage[0]?.queryIndex).toBe(0);
36
+ expect(firstPage.at(-1)?.queryIndex).toBe(OCCLUSION_QUERY_PAGE_INDEX_LIMIT - 1);
37
+ expect(pool.reserve(identity(4096))?.pageIndex).toBe(1);
38
+ });
39
+
40
+ it('releases a page only after its tickets retire and then resets its index cursor', () => {
41
+ const pool = new OcclusionQueryPool();
42
+ const first = pool.reserve(identity(0));
43
+ const second = pool.reserve(identity(1));
44
+ expect(first).toBeDefined();
45
+ expect(second).toBeDefined();
46
+ if (first === undefined || second === undefined)
47
+ throw new Error('test fixture reservations must exist');
48
+ const firstTicket = pool.publish(first, { submitted: true, submissionGeneration: 7 });
49
+ const secondTicket = pool.publish(second, { submitted: true, submissionGeneration: 7 });
50
+ expect(firstTicket).toBeDefined();
51
+ expect(secondTicket).toBeDefined();
52
+ if (firstTicket === undefined || secondTicket === undefined)
53
+ throw new Error('test fixture tickets must exist');
54
+ pool.complete(firstTicket, 1);
55
+ expect(pool.inspect().inFlight).toBe(1);
56
+ pool.complete(secondTicket, 1);
57
+ const reused = pool.reserve(identity(2));
58
+ expect(reused).toMatchObject({ pageIndex: 0, queryIndex: 0 });
59
+ });
60
+
61
+ it('does not submit new queries to a page while its staging buffer is in flight', () => {
62
+ const pool = new OcclusionQueryPool();
63
+ const first = pool.reserve(identity(0));
64
+ expect(first).toBeDefined();
65
+ if (first === undefined) throw new Error('test fixture reservation must exist');
66
+ const ticket = pool.publish(first, { submitted: true, submissionGeneration: 7 });
67
+ expect(ticket).toBeDefined();
68
+ if (ticket === undefined) throw new Error('test fixture ticket must exist');
69
+
70
+ const next = pool.reserve(identity(1));
71
+ expect(next).toMatchObject({ pageIndex: 1, queryIndex: 0 });
72
+ expect(pool.inspect().availablePages).toBe(OCCLUSION_QUERY_PAGE_COUNT - 1);
73
+ pool.complete(ticket, 1);
74
+ });
75
+
76
+ it('reports no reservable pages while every page has an in-flight ticket', () => {
77
+ const pool = new OcclusionQueryPool();
78
+ const tickets = [];
79
+ for (let index = 0; index < OCCLUSION_QUERY_PAGE_COUNT; index += 1) {
80
+ const reservation = pool.reserve(identity(index));
81
+ expect(reservation).toBeDefined();
82
+ if (reservation === undefined) throw new Error('page reservation must exist');
83
+ const ticket = pool.publish(reservation, { submitted: true, submissionGeneration: 8 });
84
+ expect(ticket).toBeDefined();
85
+ if (ticket === undefined) throw new Error('page ticket must exist');
86
+ tickets.push(ticket);
87
+ }
88
+
89
+ expect(pool.inspect().availablePages).toBe(0);
90
+ expect(pool.reserve(identity(OCCLUSION_QUERY_PAGE_COUNT))).toBeUndefined();
91
+
92
+ for (const ticket of tickets) pool.complete(ticket, 0);
93
+ expect(pool.inspect().availablePages).toBe(OCCLUSION_QUERY_PAGE_COUNT);
94
+ });
95
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OcclusionQueryPool } from '../scene/visibility/occlusion-query-pool';
3
+
4
+ const identity = (primitiveSlot: number) => ({
5
+ viewKey: 'view-main',
6
+ attachmentId: 'attachment-1',
7
+ deviceGeneration: 8,
8
+ worldGeneration: 3,
9
+ primitiveSlot,
10
+ slotGeneration: 2,
11
+ });
12
+
13
+ describe('occlusion query ticket publication', () => {
14
+ it('does not publish a ticket before the shared submission succeeds', () => {
15
+ const pool = new OcclusionQueryPool();
16
+ const reservation = pool.reserve(identity(1));
17
+ if (reservation === undefined) throw new Error('reservation unavailable');
18
+ expect(
19
+ pool.publish(reservation, { submitted: false, submissionGeneration: 1 }),
20
+ ).toBeUndefined();
21
+ expect(pool.inspect().inFlight).toBe(0);
22
+ expect(pool.publish(reservation, { submitted: true, submissionGeneration: 2 })).toMatchObject({
23
+ queryIndex: reservation.queryIndex,
24
+ pageIndex: reservation.pageIndex,
25
+ submissionGeneration: 2,
26
+ primitiveSlot: 1,
27
+ deviceGeneration: 8,
28
+ });
29
+ });
30
+
31
+ it('accepts delayed and out-of-order completions only for matching generation identity', () => {
32
+ const pool = new OcclusionQueryPool();
33
+ const first = pool.reserve(identity(1));
34
+ const second = pool.reserve(identity(2));
35
+ if (first === undefined || second === undefined) throw new Error('reservation unavailable');
36
+ const firstTicket = pool.publish(first, { submitted: true, submissionGeneration: 4 });
37
+ const secondTicket = pool.publish(second, { submitted: true, submissionGeneration: 4 });
38
+ if (firstTicket === undefined || secondTicket === undefined)
39
+ throw new Error('ticket unavailable');
40
+ expect(pool.complete(secondTicket, 0)).toEqual({ status: 'accepted', visible: true });
41
+ expect(pool.complete(firstTicket, 7)).toEqual({ status: 'accepted', visible: true });
42
+ expect(pool.complete({ ...firstTicket, deviceGeneration: 7 }, 0)).toEqual({
43
+ status: 'stale',
44
+ visible: true,
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { runOcclusionQueryDawnEvidence } from './occlusion-query.evidence';
3
+
4
+ describe('occlusion query Dawn evidence', () => {
5
+ it('maps real zero and positive query results with identity and lifecycle evidence', async () => {
6
+ const evidence = await runOcclusionQueryDawnEvidence();
7
+ expect(evidence.status).toBe('available');
8
+ expect(evidence.identity.commit.length).toBeGreaterThan(0);
9
+ expect(evidence.identity.device).toBe('webgpu-dawn');
10
+ expect(evidence.zeroSamples).toBe(0);
11
+ expect(evidence.positiveSamples).toBeGreaterThan(0);
12
+ expect(evidence.outOfOrder).toBe(true);
13
+ expect(evidence.staleVisible).toBe(true);
14
+ expect(evidence.faultVisible).toBe(true);
15
+ });
16
+ });
@@ -0,0 +1,178 @@
1
+ import type { RhiDevice } from '@forgeax/engine-rhi';
2
+ import { createShaderModule, rhi } from '@forgeax/engine-rhi-webgpu';
3
+ import { GPU_TEXTURE_USAGE_RENDER_ATTACHMENT } from '../gpu-texture-usage';
4
+ import { GPU_BUFFER_USAGE_MAP_READ } from '../gpu-usage';
5
+ import {
6
+ createOcclusionQueryResources,
7
+ recordOcclusionResolve,
8
+ } from '../scene/visibility/occlusion-pass';
9
+ import { OcclusionQueryPool } from '../scene/visibility/occlusion-query-pool';
10
+ import { resolveEvidenceCommitIdentity } from './evidence-identity';
11
+
12
+ export interface OcclusionQueryEvidenceIdentity {
13
+ readonly commit: string;
14
+ readonly device: string;
15
+ readonly view: string;
16
+ }
17
+
18
+ export interface OcclusionQueryDawnEvidence {
19
+ readonly status: 'available' | 'unavailable';
20
+ readonly identity: OcclusionQueryEvidenceIdentity;
21
+ readonly zeroSamples: number;
22
+ readonly positiveSamples: number;
23
+ readonly outOfOrder: boolean;
24
+ readonly staleVisible: boolean;
25
+ readonly faultVisible: boolean;
26
+ readonly reason?: string;
27
+ }
28
+
29
+ function identity(device: string): OcclusionQueryEvidenceIdentity {
30
+ return {
31
+ commit: resolveEvidenceCommitIdentity(),
32
+ device,
33
+ view: 'occlusion-query-evidence-view:main:1',
34
+ };
35
+ }
36
+
37
+ function unavailable(reason: string): OcclusionQueryDawnEvidence {
38
+ return {
39
+ status: 'unavailable',
40
+ identity: identity('webgpu-unavailable'),
41
+ zeroSamples: 0,
42
+ positiveSamples: 0,
43
+ outOfOrder: false,
44
+ staleVisible: true,
45
+ faultVisible: true,
46
+ reason,
47
+ };
48
+ }
49
+
50
+ async function requestDevice(): Promise<RhiDevice | OcclusionQueryDawnEvidence> {
51
+ const adapter = await rhi.requestAdapter();
52
+ if (!adapter.ok) return unavailable(adapter.error.code);
53
+ const device = await adapter.value.requestDevice();
54
+ if (!device.ok) return unavailable(device.error.code);
55
+ return device.value;
56
+ }
57
+
58
+ function lifecycleEvidence(): Pick<
59
+ OcclusionQueryDawnEvidence,
60
+ 'outOfOrder' | 'staleVisible' | 'faultVisible'
61
+ > {
62
+ const pool = new OcclusionQueryPool();
63
+ const base = {
64
+ viewKey: 'evidence-view',
65
+ attachmentId: 'evidence-depth',
66
+ deviceGeneration: 1,
67
+ worldGeneration: 2,
68
+ primitiveSlot: 0,
69
+ slotGeneration: 1,
70
+ };
71
+ const first = pool.reserve({ ...base, primitiveSlot: 1 });
72
+ const second = pool.reserve({ ...base, primitiveSlot: 2 });
73
+ if (first === undefined || second === undefined) {
74
+ return { outOfOrder: false, staleVisible: true, faultVisible: true };
75
+ }
76
+ const firstTicket = pool.publish(first, { submitted: true, submissionGeneration: 4 });
77
+ const secondTicket = pool.publish(second, { submitted: true, submissionGeneration: 5 });
78
+ if (firstTicket === undefined || secondTicket === undefined) {
79
+ return { outOfOrder: false, staleVisible: true, faultVisible: true };
80
+ }
81
+ const secondCompletion = pool.complete(secondTicket, 3);
82
+ const firstCompletion = pool.complete(firstTicket, 0);
83
+ const staleCompletion = pool.complete({ ...firstTicket, slotGeneration: 99 }, 1);
84
+ return {
85
+ outOfOrder: secondCompletion.status === 'accepted' && firstCompletion.status === 'accepted',
86
+ staleVisible: staleCompletion.visible,
87
+ faultVisible: staleCompletion.visible,
88
+ };
89
+ }
90
+
91
+ export async function runOcclusionQueryDawnEvidence(): Promise<OcclusionQueryDawnEvidence> {
92
+ const requested = await requestDevice();
93
+ if (!('createQuerySet' in requested)) return requested;
94
+ const device = requested;
95
+ const resourcesResult = createOcclusionQueryResources(device, 0);
96
+ if (!resourcesResult.ok) return unavailable(resourcesResult.error.code);
97
+ const resources = resourcesResult.value;
98
+
99
+ const shader = await createShaderModule(device, {
100
+ label: 'occlusion-query-evidence-shader',
101
+ code: `
102
+ @vertex fn vs(@builtin(vertex_index) vertex: u32) -> @builtin(position) vec4f {
103
+ var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
104
+ return vec4f(positions[vertex], 0.0, 1.0);
105
+ }
106
+ @fragment fn fs() -> @location(0) vec4f {
107
+ return vec4f(1.0, 0.0, 0.0, 1.0);
108
+ }
109
+ `,
110
+ });
111
+ if (!shader.ok) return unavailable(shader.error.code);
112
+ const pipeline = device.createRenderPipeline({
113
+ label: 'occlusion-query-evidence-pipeline',
114
+ layout: 'auto',
115
+ vertex: { module: shader.value, entryPoint: 'vs', buffers: [] },
116
+ fragment: {
117
+ module: shader.value,
118
+ entryPoint: 'fs',
119
+ targets: [{ format: 'rgba8unorm' }],
120
+ },
121
+ primitive: { topology: 'triangle-list' },
122
+ });
123
+ if (!pipeline.ok) return unavailable(pipeline.error.code);
124
+ const texture = device.createTexture({
125
+ label: 'occlusion-query-evidence-target',
126
+ size: { width: 4, height: 4, depthOrArrayLayers: 1 },
127
+ format: 'rgba8unorm',
128
+ usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
129
+ } as never);
130
+ if (!texture.ok) return unavailable(texture.error.code);
131
+ const view = device.createTextureView(texture.value, {});
132
+ if (!view.ok) return unavailable(view.error.code);
133
+ const encoder = device.createCommandEncoder({ label: 'occlusion-query-evidence-submit' });
134
+ if (!encoder.ok) return unavailable(encoder.error.code);
135
+ const pass = encoder.value.beginRenderPass({
136
+ colorAttachments: [
137
+ {
138
+ view: view.value,
139
+ loadOp: 'clear',
140
+ storeOp: 'store',
141
+ clearValue: { r: 0, g: 0, b: 0, a: 1 },
142
+ },
143
+ ],
144
+ occlusionQuerySet: resources.querySet,
145
+ });
146
+ const beginZero = pass.beginOcclusionQuery(0);
147
+ if (!beginZero.ok) return unavailable(beginZero.error.code);
148
+ const endZero = pass.endOcclusionQuery();
149
+ if (!endZero.ok) return unavailable(endZero.error.code);
150
+ const beginPositive = pass.beginOcclusionQuery(1);
151
+ if (!beginPositive.ok) return unavailable(beginPositive.error.code);
152
+ pass.setPipeline(pipeline.value);
153
+ pass.draw(3);
154
+ const endPositive = pass.endOcclusionQuery();
155
+ if (!endPositive.ok) return unavailable(endPositive.error.code);
156
+ pass.end();
157
+ const resolved = recordOcclusionResolve(encoder.value, resources, 0, 2);
158
+ if (!resolved.ok) return unavailable(resolved.error.code);
159
+ const command = encoder.value.finish();
160
+ if (!command.ok) return unavailable(command.error.code);
161
+ const submitted = device.queue.submit([command.value]);
162
+ if (!submitted.ok) return unavailable(submitted.error.code);
163
+ await device.queue.onSubmittedWorkDone();
164
+ const mapped = await resources.stagingBuffer.mapAsync(GPU_BUFFER_USAGE_MAP_READ, 0, 16);
165
+ if (!mapped.ok) return unavailable(mapped.error.code);
166
+ const range = mapped.value.getMappedRange(0, 16);
167
+ if (!range.ok) return unavailable(range.error.code);
168
+ const values = new BigUint64Array(range.value.slice(0));
169
+ const lifecycle = lifecycleEvidence();
170
+ mapped.value.unmap();
171
+ return {
172
+ status: 'available',
173
+ identity: identity('webgpu-dawn'),
174
+ zeroSamples: Number(values[0] ?? 0n),
175
+ positiveSamples: Number(values[1] ?? 0n),
176
+ ...lifecycle,
177
+ };
178
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { OcclusionQueryPool } from '../scene/visibility/occlusion-query-pool';
3
+
4
+ describe('occlusion query RhiNull structure', () => {
5
+ it('keeps zero, positive, out-of-order, and fault completions all-visible', () => {
6
+ const pool = new OcclusionQueryPool();
7
+ const base = {
8
+ viewKey: 'view',
9
+ attachmentId: 'depth-0',
10
+ deviceGeneration: 4,
11
+ worldGeneration: 9,
12
+ primitiveSlot: 2,
13
+ slotGeneration: 3,
14
+ };
15
+ const first = pool.reserve({ ...base, primitiveSlot: 1 });
16
+ const second = pool.reserve({ ...base, primitiveSlot: 2 });
17
+ expect(first && second).toBeTruthy();
18
+ if (first === undefined || second === undefined) return;
19
+ const firstTicket = pool.publish(first, { submitted: true, submissionGeneration: 10 });
20
+ const secondTicket = pool.publish(second, { submitted: true, submissionGeneration: 11 });
21
+ expect(firstTicket && secondTicket).toBeTruthy();
22
+ if (firstTicket === undefined || secondTicket === undefined) return;
23
+
24
+ expect(pool.complete(secondTicket, 7)).toMatchObject({ status: 'accepted', visible: true });
25
+ expect(pool.complete(firstTicket, 0)).toMatchObject({ status: 'accepted', visible: true });
26
+ expect(pool.complete({ ...firstTicket, slotGeneration: 99 }, 1)).toMatchObject({
27
+ status: 'stale',
28
+ visible: true,
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,120 @@
1
+ import type { RhiRenderPassEncoder, ShaderModule } from '@forgeax/engine-rhi';
2
+ import { rhi } from '@forgeax/engine-rhi-null';
3
+ import { ok } from '@forgeax/engine-types';
4
+ import { describe, expect, it, vi } from 'vitest';
5
+ import { createVisibilityBudget } from '../scene/visibility/budget';
6
+ import { VisibilityFacetStore } from '../scene/visibility/facet';
7
+ import {
8
+ OcclusionRenderRuntime,
9
+ type OcclusionRuntimeCandidate,
10
+ } from '../scene/visibility/occlusion-runtime';
11
+
12
+ describe('occlusion runtime proxy encoding', () => {
13
+ it('binds one proxy buffer and advances firstVertex across candidates', async () => {
14
+ const adapter = (await rhi.requestAdapter()).unwrap();
15
+ const device = (await adapter.requestDevice()).unwrap();
16
+ const facets = new VisibilityFacetStore(createVisibilityBudget(2));
17
+ const candidates: OcclusionRuntimeCandidate[] = [candidate(0, 3), candidate(1, 2)];
18
+ const runtime = new OcclusionRenderRuntime(device, facets, {
19
+ createShaderModule: () => ok({} as ShaderModule),
20
+ });
21
+ const writeBuffer = vi.spyOn(device.queue, 'writeBuffer');
22
+ const projection = runtime.prepareBatch(candidates, 1, false);
23
+ expect(projection).toBeDefined();
24
+ if (projection === undefined) return;
25
+
26
+ const vertexBindings: unknown[][] = [];
27
+ const draws: unknown[][] = [];
28
+ const beginQueries: number[] = [];
29
+ let endCount = 0;
30
+ const pass = {
31
+ setPipeline: () => undefined,
32
+ setVertexBuffer: (...args: unknown[]) => vertexBindings.push(args),
33
+ draw: (...args: unknown[]) => draws.push(args),
34
+ beginOcclusionQuery: (queryIndex: number) => {
35
+ beginQueries.push(queryIndex);
36
+ return ok(undefined);
37
+ },
38
+ endOcclusionQuery: () => {
39
+ endCount += 1;
40
+ return ok(undefined);
41
+ },
42
+ } as unknown as RhiRenderPassEncoder;
43
+
44
+ expect(projection.encodeProxyBounds(pass).ok).toBe(true);
45
+ expect(vertexBindings).toHaveLength(1);
46
+ expect(vertexBindings[0]?.[0]).toBe(0);
47
+ expect(vertexBindings[0]?.[2]).toBeUndefined();
48
+ expect(vertexBindings[0]?.[3]).toBeUndefined();
49
+ expect(beginQueries).toEqual([0, 1]);
50
+ expect(draws.map((draw) => draw.slice(0, 4))).toEqual([
51
+ [3, 1, 0, 0],
52
+ [2, 1, 3, 0],
53
+ ]);
54
+ expect(endCount).toBe(2);
55
+ expect(runtime.prepareBatch(candidates, 1, false)).toBeUndefined();
56
+ expect(runtime.prepareUnavailable).toBe(true);
57
+ const profilePhases: string[] = [];
58
+ projection.commit(false, 1, (phase, action) => {
59
+ profilePhases.push(phase);
60
+ return action();
61
+ });
62
+ expect(profilePhases).toEqual(['record/occlusion-query-submit']);
63
+
64
+ const nextProjection = runtime.prepareBatch(candidates, 1, false);
65
+ expect(nextProjection).toBeDefined();
66
+ if (nextProjection === undefined) return;
67
+ expect(nextProjection.encodeProxyBounds(pass).ok).toBe(true);
68
+ expect(writeBuffer).toHaveBeenCalledTimes(2);
69
+ const firstUpload = writeBuffer.mock.calls[0]?.[2];
70
+ const secondUpload = writeBuffer.mock.calls[1]?.[2];
71
+ expect(ArrayBuffer.isView(firstUpload) && ArrayBuffer.isView(secondUpload)).toBe(true);
72
+ if (ArrayBuffer.isView(firstUpload) && ArrayBuffer.isView(secondUpload)) {
73
+ expect(firstUpload.buffer).toBe(secondUpload.buffer);
74
+ }
75
+ nextProjection.commit(false, 2);
76
+ runtime.dispose();
77
+ });
78
+
79
+ it('cancels and requeues a reservation when the fixed scratch overflows', async () => {
80
+ const adapter = (await rhi.requestAdapter()).unwrap();
81
+ const device = (await adapter.requestDevice()).unwrap();
82
+ const budget = createVisibilityBudget(1);
83
+ const facets = new VisibilityFacetStore(budget);
84
+ const oversized = candidate(0, budget.effectiveQueryBudget * 36 + 1);
85
+ facets.activateView(oversized.view);
86
+ expect(facets.setCandidate(oversized.view, oversized.primitive, oversized.candidate)).toBe(
87
+ true,
88
+ );
89
+ const runtime = new OcclusionRenderRuntime(device, facets, {
90
+ createShaderModule: () => ok({} as ShaderModule),
91
+ });
92
+
93
+ expect(runtime.prepareBatch([oversized], 1, true)).toBeUndefined();
94
+ expect(runtime.prepareUnavailable).toBe(true);
95
+ expect(facets.dequeueQueryCandidates(oversized.view, 1)).toHaveLength(1);
96
+ runtime.dispose();
97
+ });
98
+ });
99
+
100
+ function candidate(primitiveSlot: number, vertexCount: number): OcclusionRuntimeCandidate {
101
+ return {
102
+ view: {
103
+ attachmentId: 'color-depth',
104
+ cameraEntity: 7,
105
+ viewRole: 'main',
106
+ viewGeneration: 1,
107
+ },
108
+ primitive: {
109
+ attachmentId: 'color-depth',
110
+ worldGeneration: 3,
111
+ primitiveSlot,
112
+ slotGeneration: 1,
113
+ },
114
+ epoch: 0,
115
+ bounds: { min: [-1, -1, 0], max: [1, 1, 1] },
116
+ candidate: { level: 0, confidence: 1 },
117
+ deviceGeneration: 1,
118
+ proxyVertices: new Float32Array(vertexCount * 4),
119
+ };
120
+ }