@forgeax/engine-render 0.1.19 → 0.1.21

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 (452) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +34 -20
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/chunk-FS2KNUQH.mjs +1294 -0
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/components/spot-light.d.ts +12 -0
  42. package/dist/components/spot-light.d.ts.map +1 -1
  43. package/dist/construct-renderer.mjs +8552 -1612
  44. package/dist/construct-renderer.mjs.map +1 -1
  45. package/dist/device/gpu-residency.d.ts +19 -2
  46. package/dist/device/gpu-residency.d.ts.map +1 -1
  47. package/dist/environment/frame.d.ts.map +1 -1
  48. package/dist/environment/lifecycle.d.ts +5 -0
  49. package/dist/environment/lifecycle.d.ts.map +1 -1
  50. package/dist/errors/render.d.ts +209 -93
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  53. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  54. package/dist/extract/environment.d.ts +14 -0
  55. package/dist/extract/environment.d.ts.map +1 -1
  56. package/dist/extract/visibility.d.ts.map +1 -1
  57. package/dist/features/fullscreen.d.ts.map +1 -1
  58. package/dist/features/targets.d.ts.map +1 -1
  59. package/dist/features/types.d.ts +10 -0
  60. package/dist/features/types.d.ts.map +1 -1
  61. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  62. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  63. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  64. package/dist/gpu-driven/production-raster.d.ts +0 -1
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-scene.d.ts.map +1 -1
  67. package/dist/gpu-usage.d.ts +2 -0
  68. package/dist/gpu-usage.d.ts.map +1 -1
  69. package/dist/hdrp-buffers.d.ts +19 -17
  70. package/dist/hdrp-buffers.d.ts.map +1 -1
  71. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  72. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  73. package/dist/ibl/kernel-cache.d.ts +36 -0
  74. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  75. package/dist/index.d.ts +35 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.mjs +4 -4
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/inspection-types.d.ts +51 -0
  80. package/dist/inspection-types.d.ts.map +1 -1
  81. package/dist/instances-derived-bounds.d.ts +50 -0
  82. package/dist/instances-derived-bounds.d.ts.map +1 -0
  83. package/dist/internal.mjs +1 -1
  84. package/dist/light-buffer-layout.d.ts +30 -13
  85. package/dist/light-buffer-layout.d.ts.map +1 -1
  86. package/dist/mesh-material-bindings.d.ts +54 -0
  87. package/dist/mesh-material-bindings.d.ts.map +1 -1
  88. package/dist/pbr-pipeline.d.ts +20 -7
  89. package/dist/pbr-pipeline.d.ts.map +1 -1
  90. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  93. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  95. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  97. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  99. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  101. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  103. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  104. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  105. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  106. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  107. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  108. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  109. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  110. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  111. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts.map +1 -1
  113. package/dist/pipeline/standard-profile.d.ts +12 -20
  114. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  115. package/dist/pipeline-spec.d.ts +40 -7
  116. package/dist/pipeline-spec.d.ts.map +1 -1
  117. package/dist/point-shadow-inspection.d.ts +21 -0
  118. package/dist/point-shadow-inspection.d.ts.map +1 -0
  119. package/dist/record/frame-lighting.d.ts +29 -32
  120. package/dist/record/frame-lighting.d.ts.map +1 -1
  121. package/dist/record/frame-snapshot.d.ts +65 -15
  122. package/dist/record/frame-snapshot.d.ts.map +1 -1
  123. package/dist/record/frame-targets.d.ts +5 -0
  124. package/dist/record/frame-targets.d.ts.map +1 -1
  125. package/dist/record/frame.d.ts +12 -5
  126. package/dist/record/frame.d.ts.map +1 -1
  127. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  128. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  130. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  131. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  132. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  133. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  134. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  135. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  136. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  137. package/dist/record/gpu-timing.d.ts +37 -0
  138. package/dist/record/gpu-timing.d.ts.map +1 -0
  139. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  140. package/dist/record/main-pass-material.d.ts +23 -2
  141. package/dist/record/main-pass-material.d.ts.map +1 -1
  142. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  143. package/dist/record/main-pass.d.ts +2 -0
  144. package/dist/record/main-pass.d.ts.map +1 -1
  145. package/dist/record/render-context.d.ts +48 -3
  146. package/dist/record/render-context.d.ts.map +1 -1
  147. package/dist/record/shadow-pass.d.ts.map +1 -1
  148. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  149. package/dist/record/typed-frame-graph.d.ts +81 -5
  150. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  151. package/dist/record/view-ubo.d.ts +7 -4
  152. package/dist/record/view-ubo.d.ts.map +1 -1
  153. package/dist/reflection/filter.d.ts +22 -0
  154. package/dist/reflection/filter.d.ts.map +1 -0
  155. package/dist/reflection/gpu-table.d.ts +25 -0
  156. package/dist/reflection/gpu-table.d.ts.map +1 -0
  157. package/dist/reflection/projection.d.ts +91 -0
  158. package/dist/reflection/projection.d.ts.map +1 -0
  159. package/dist/render-contract.d.ts +114 -7
  160. package/dist/render-contract.d.ts.map +1 -1
  161. package/dist/render-data.d.ts +10 -6
  162. package/dist/render-data.d.ts.map +1 -1
  163. package/dist/render-graph-primitives.d.ts +1 -0
  164. package/dist/render-graph-primitives.d.ts.map +1 -1
  165. package/dist/render-pipeline.d.ts +78 -2
  166. package/dist/render-pipeline.d.ts.map +1 -1
  167. package/dist/render-system-extract.d.ts +113 -20
  168. package/dist/render-system-extract.d.ts.map +1 -1
  169. package/dist/render-system.d.ts +24 -3
  170. package/dist/render-system.d.ts.map +1 -1
  171. package/dist/renderer-inspect.d.ts +29 -0
  172. package/dist/renderer-inspect.d.ts.map +1 -0
  173. package/dist/renderer.d.ts +43 -0
  174. package/dist/renderer.d.ts.map +1 -0
  175. package/dist/scene/render-scene.d.ts +14 -0
  176. package/dist/scene/render-scene.d.ts.map +1 -1
  177. package/dist/shadow-atlas.d.ts +3 -3
  178. package/dist/shadow-atlas.d.ts.map +1 -1
  179. package/dist/ssao-buffers.d.ts +3 -0
  180. package/dist/ssao-buffers.d.ts.map +1 -1
  181. package/dist/targets/contracts.d.ts +66 -0
  182. package/dist/targets/contracts.d.ts.map +1 -0
  183. package/dist/targets/graph-projection.d.ts +29 -0
  184. package/dist/targets/graph-projection.d.ts.map +1 -0
  185. package/dist/targets/material-source.d.ts +34 -0
  186. package/dist/targets/material-source.d.ts.map +1 -0
  187. package/dist/targets/owner.d.ts +32 -0
  188. package/dist/targets/owner.d.ts.map +1 -0
  189. package/dist/targets/physical.d.ts +18 -0
  190. package/dist/targets/physical.d.ts.map +1 -0
  191. package/dist/targets/readback.d.ts +39 -0
  192. package/dist/targets/readback.d.ts.map +1 -0
  193. package/dist/temporal/frame.d.ts +44 -0
  194. package/dist/temporal/frame.d.ts.map +1 -0
  195. package/dist/temporal/index.mjs +3 -3
  196. package/dist/typed-render-graph-primitives.d.ts +6 -1
  197. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  198. package/dist/typed-shadow-atlas.d.ts +10 -0
  199. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  200. package/dist/typed-shadow-passes.d.ts +1 -1
  201. package/dist/typed-shadow-passes.d.ts.map +1 -1
  202. package/dist/volume/capability.d.ts +57 -0
  203. package/dist/volume/capability.d.ts.map +1 -0
  204. package/dist/volume/component.d.ts +61 -0
  205. package/dist/volume/component.d.ts.map +1 -0
  206. package/dist/volume/extract.d.ts +11 -0
  207. package/dist/volume/extract.d.ts.map +1 -0
  208. package/dist/volume/inspection.d.ts +38 -0
  209. package/dist/volume/inspection.d.ts.map +1 -0
  210. package/dist/volume/passes.d.ts +38 -0
  211. package/dist/volume/passes.d.ts.map +1 -0
  212. package/dist/volume/recovery.d.ts +38 -0
  213. package/dist/volume/recovery.d.ts.map +1 -0
  214. package/dist/volume/resources.d.ts +57 -0
  215. package/dist/volume/resources.d.ts.map +1 -0
  216. package/dist/volume/temporal.d.ts +35 -0
  217. package/dist/volume/temporal.d.ts.map +1 -0
  218. package/package.json +20 -20
  219. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  220. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  221. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  222. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  223. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  224. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  225. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  226. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  227. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  228. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  229. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  230. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  231. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  232. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  233. package/src/__tests__/fog-component.unit.test.ts +67 -0
  234. package/src/__tests__/fog.integration.test.ts +86 -0
  235. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  236. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  237. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  238. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  239. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  240. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  241. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  242. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  243. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  244. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  245. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  246. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  247. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  248. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  249. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  250. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  251. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  252. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  253. package/src/__tests__/material-observation.integration.test.ts +101 -0
  254. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  255. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  256. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  257. package/src/__tests__/public-surface.test-d.ts +10 -0
  258. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  259. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  260. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  261. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  262. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  263. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  264. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  265. package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
  266. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  267. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  268. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  269. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  270. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  271. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  272. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  273. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  274. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  275. package/src/__tests__/render-target-public-schema.json +107 -0
  276. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  277. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  278. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  279. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  280. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  281. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  282. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  283. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  284. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  285. package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
  286. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  287. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  288. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  289. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  290. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  291. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  292. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  293. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  294. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  295. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  296. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  297. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
  298. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  299. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  300. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  301. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  302. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  303. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  304. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  305. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  306. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  307. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  308. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  309. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  310. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  311. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  312. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  313. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  315. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  316. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  317. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  318. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  319. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  320. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  321. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  322. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  323. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  324. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  325. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  326. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  327. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  328. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  329. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  330. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  331. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  332. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  333. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  334. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  335. package/src/assembly/device-feature-admission.ts +73 -0
  336. package/src/assembly/directional-shadow-inspection.ts +122 -0
  337. package/src/assembly/factory.ts +1093 -246
  338. package/src/assembly/host-contract.ts +25 -0
  339. package/src/assembly/render-target-host.ts +847 -0
  340. package/src/assets/asset-decoders.ts +1 -1
  341. package/src/assets/render-asset-port.ts +7 -1
  342. package/src/capture/cube-views.ts +69 -0
  343. package/src/capture/scheduler.ts +265 -0
  344. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  345. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  346. package/src/components/camera.ts +4 -0
  347. package/src/components/cube-camera.ts +52 -0
  348. package/src/components/directional-light.ts +22 -2
  349. package/src/components/directional-shadow-filter.ts +57 -0
  350. package/src/components/index.ts +2 -0
  351. package/src/components/light-helpers.ts +85 -7
  352. package/src/components/reflection-probe.ts +49 -0
  353. package/src/components/spot-light.ts +22 -3
  354. package/src/device/gpu-residency.ts +168 -58
  355. package/src/environment/frame.ts +6 -24
  356. package/src/environment/lifecycle.ts +12 -0
  357. package/src/errors/render.ts +405 -143
  358. package/src/extract/directional-shadow-projection.ts +61 -0
  359. package/src/extract/environment.ts +57 -0
  360. package/src/extract/visibility.ts +25 -1
  361. package/src/features/fullscreen.ts +3 -0
  362. package/src/features/targets.ts +4 -1
  363. package/src/features/types.ts +10 -0
  364. package/src/fullscreen-post-process-pass.ts +43 -2
  365. package/src/gpu-driven/batch-topology.ts +1 -0
  366. package/src/gpu-driven/production-raster.ts +1 -3
  367. package/src/gpu-scene.ts +8 -1
  368. package/src/gpu-usage.ts +2 -0
  369. package/src/hdrp-buffers.ts +165 -59
  370. package/src/ibl/IblPipelineCache.ts +11 -0
  371. package/src/ibl/kernel-cache.ts +88 -0
  372. package/src/index.ts +186 -1
  373. package/src/inspection-types.ts +56 -0
  374. package/src/instances-derived-bounds.ts +171 -0
  375. package/src/light-buffer-layout.ts +74 -26
  376. package/src/mesh-material-bindings.ts +132 -0
  377. package/src/pbr-pipeline.ts +139 -27
  378. package/src/pipeline/standard-forward-lane.ts +65 -15
  379. package/src/pipeline/standard-lighting/errors.ts +6 -0
  380. package/src/pipeline/standard-lighting/graph.ts +157 -0
  381. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  382. package/src/pipeline/standard-lighting/layout.ts +31 -0
  383. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  384. package/src/pipeline/standard-lighting/membership.ts +43 -0
  385. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  386. package/src/pipeline/standard-lighting/topology.ts +120 -0
  387. package/src/pipeline/standard-lighting/transport.ts +77 -0
  388. package/src/pipeline/standard-pipeline.ts +141 -192
  389. package/src/pipeline/standard-post.ts +26 -7
  390. package/src/pipeline/standard-profile.ts +23 -30
  391. package/src/pipeline-spec.ts +84 -21
  392. package/src/point-shadow-inspection.ts +55 -0
  393. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  394. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  395. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  396. package/src/record/frame-lighting.ts +470 -516
  397. package/src/record/frame-snapshot.ts +76 -28
  398. package/src/record/frame-targets.ts +23 -2
  399. package/src/record/frame.ts +1169 -79
  400. package/src/record/gpu-pass-timing/contract.ts +180 -0
  401. package/src/record/gpu-pass-timing/errors.ts +34 -0
  402. package/src/record/gpu-pass-timing/index.ts +37 -0
  403. package/src/record/gpu-pass-timing/parser.ts +97 -0
  404. package/src/record/gpu-pass-timing/session.ts +938 -0
  405. package/src/record/gpu-timing.ts +233 -0
  406. package/src/record/main-pass-geometry.ts +120 -80
  407. package/src/record/main-pass-material.ts +101 -11
  408. package/src/record/main-pass-sprite-draws.ts +54 -25
  409. package/src/record/main-pass.ts +71 -15
  410. package/src/record/render-context.ts +77 -0
  411. package/src/record/shadow-pass.ts +104 -66
  412. package/src/record/skybox-post-pass.ts +20 -4
  413. package/src/record/typed-frame-graph.ts +1016 -32
  414. package/src/record/view-ubo.ts +63 -28
  415. package/src/reflection/filter.ts +81 -0
  416. package/src/reflection/gpu-table.ts +44 -0
  417. package/src/reflection/projection.ts +281 -0
  418. package/src/render-contract.ts +147 -4
  419. package/src/render-data.ts +50 -22
  420. package/src/render-graph-primitives.ts +9 -4
  421. package/src/render-pipeline.ts +161 -2
  422. package/src/render-system-extract.ts +886 -103
  423. package/src/render-system.ts +1174 -66
  424. package/src/renderer-inspect.ts +66 -0
  425. package/src/renderer.ts +222 -0
  426. package/src/scene/render-scene.ts +117 -9
  427. package/src/shadow-atlas.ts +5 -5
  428. package/src/ssao-buffers.ts +9 -2
  429. package/src/targets/contracts.ts +182 -0
  430. package/src/targets/graph-projection.ts +169 -0
  431. package/src/targets/material-source.ts +143 -0
  432. package/src/targets/owner.ts +214 -0
  433. package/src/targets/physical.ts +18 -0
  434. package/src/targets/readback.ts +153 -0
  435. package/src/temporal/frame.ts +128 -0
  436. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
  437. package/src/typed-render-graph-primitives.ts +34 -2
  438. package/src/typed-shadow-atlas.ts +17 -0
  439. package/src/typed-shadow-passes.ts +47 -40
  440. package/src/volume/capability.ts +173 -0
  441. package/src/volume/component.ts +110 -0
  442. package/src/volume/extract.ts +21 -0
  443. package/src/volume/inspection.ts +78 -0
  444. package/src/volume/passes.ts +686 -0
  445. package/src/volume/recovery.ts +86 -0
  446. package/src/volume/resources.ts +236 -0
  447. package/src/volume/temporal.ts +89 -0
  448. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  449. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  450. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  451. package/dist/chunk-YTQ5C7YQ.mjs +0 -584
  452. package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
@@ -0,0 +1,938 @@
1
+ import type {
2
+ Buffer,
3
+ ComputePassDescriptor,
4
+ MappedBuffer,
5
+ QuerySet,
6
+ RhiCommandEncoder,
7
+ RhiDevice,
8
+ } from '@forgeax/engine-rhi';
9
+ import { err, ok, type Result } from '@forgeax/engine-types';
10
+ import {
11
+ createGpuPassTimingFrame,
12
+ freezeGpuPassTimingFrame,
13
+ type GpuPassTimingEntry,
14
+ type GpuPassTimingFrame,
15
+ type GpuPassTimingOptions,
16
+ type GpuPassTimingPassIdentity,
17
+ type NormalizedGpuPassTimingOptions,
18
+ normalizeGpuPassTimingOptions,
19
+ } from './contract.js';
20
+ import type { GpuPassTimingReason } from './errors.js';
21
+ import { parseGpuPassTimingTicks } from './parser.js';
22
+
23
+ const GPU_BUFFER_USAGE_MAP_READ = 0x01;
24
+ const GPU_BUFFER_USAGE_COPY_SRC = 0x04;
25
+ const GPU_BUFFER_USAGE_COPY_DST = 0x08;
26
+ const GPU_BUFFER_USAGE_QUERY_RESOLVE = 0x200;
27
+ const GPU_MAP_MODE_READ = 0x01;
28
+ const QUERY_RESOLVE_ALIGNMENT = 256;
29
+ const QUERY_RESULT_BYTES = 8;
30
+
31
+ export interface GpuPassTimingFrameIdentity {
32
+ readonly frameId: number;
33
+ readonly deviceGeneration: number;
34
+ readonly graphGeneration: number;
35
+ }
36
+
37
+ export interface GpuPassTimingSessionOptions {
38
+ readonly mapReadback?: ((buffer: Buffer) => Promise<Result<MappedBuffer, unknown>>) | undefined;
39
+ }
40
+
41
+ export interface GpuPassTimingCapture {
42
+ readonly frameId: number;
43
+ recordPass(identity: GpuPassTimingPassIdentity):
44
+ | {
45
+ readonly querySet: QuerySet;
46
+ readonly beginningOfPassWriteIndex: number;
47
+ readonly endOfPassWriteIndex: number;
48
+ }
49
+ | undefined;
50
+ markOwnerConflict(identity: GpuPassTimingPassIdentity): void;
51
+ markTimestampWriteFailure(
52
+ identity: GpuPassTimingPassIdentity,
53
+ phase: 'begin' | 'end',
54
+ cause: unknown,
55
+ ): void;
56
+ copyBoundaryBefore(identity: GpuPassTimingPassIdentity, encoder: RhiCommandEncoder): void;
57
+ copyBoundaryAfter(identity: GpuPassTimingPassIdentity, encoder: RhiCommandEncoder): void;
58
+ timestampWrites(identity: GpuPassTimingPassIdentity):
59
+ | {
60
+ readonly querySet: QuerySet;
61
+ readonly beginningOfPassWriteIndex: number;
62
+ readonly endOfPassWriteIndex: number;
63
+ }
64
+ | undefined;
65
+ encodeTail(encoder: RhiCommandEncoder): Result<void, GpuPassTimingReason>;
66
+ markSubmitted(completion?: Promise<void>): void;
67
+ snapshot(): GpuPassTimingFrame;
68
+ observe(): Promise<Result<GpuPassTimingFrame, GpuPassTimingReason>>;
69
+ abort(cause?: { readonly code?: string | undefined }): void;
70
+ }
71
+
72
+ export interface GpuPassTimingSession {
73
+ readonly options: NormalizedGpuPassTimingOptions;
74
+ beginFrame(
75
+ identity: GpuPassTimingFrameIdentity,
76
+ ): Result<GpuPassTimingCapture, GpuPassTimingReason>;
77
+ dispose(): void;
78
+ }
79
+
80
+ interface Slot {
81
+ readonly queryCount: number;
82
+ readonly querySet: QuerySet;
83
+ readonly resolveBuffer: Buffer;
84
+ readonly readbackBuffer: Buffer;
85
+ readonly storage: CaptureStorage;
86
+ state: 'idle' | 'encoding' | 'submitted' | 'terminal';
87
+ capture?: CaptureState | undefined;
88
+ }
89
+
90
+ interface CaptureStorage {
91
+ readonly entries: CaptureEntry[];
92
+ readonly queryPairs: CaptureQueryPair[];
93
+ readonly entryIndexesByExecutionIndex: Array<number | undefined>;
94
+ readonly timestampWrites: CaptureTimestampWrites[];
95
+ readonly copyBeginningMarkerWrites: CaptureMarkerWrites;
96
+ readonly copyEndMarkerWrites: CaptureMarkerWrites;
97
+ readonly copyBeginningMarkerDescriptor: ComputePassDescriptor;
98
+ readonly copyEndMarkerDescriptor: ComputePassDescriptor;
99
+ }
100
+
101
+ interface CaptureState {
102
+ readonly identity: GpuPassTimingFrameIdentity;
103
+ readonly graphGeneration: number;
104
+ readonly backendKind: RhiDevice['caps']['backendKind'];
105
+ readonly timestampPeriodNanoseconds: number;
106
+ readonly slot: Slot;
107
+ readonly entries: CaptureEntry[];
108
+ readonly queryPairs: CaptureQueryPair[];
109
+ readonly entryIndexesByExecutionIndex: Array<number | undefined>;
110
+ readonly layout: CaptureLayout | undefined;
111
+ layoutCandidateValid: boolean;
112
+ nextQueryIndex: number;
113
+ droppedPassCount: number;
114
+ timedPassCount: number;
115
+ terminal: Result<GpuPassTimingFrame, GpuPassTimingReason> | undefined;
116
+ submitted: Promise<void> | undefined;
117
+ }
118
+
119
+ type CaptureEntry = GpuPassTimingPassIdentity | GpuPassTimingEntry;
120
+
121
+ interface CaptureQueryPair {
122
+ beginningQueryIndex: number;
123
+ endQueryIndex: number;
124
+ requiresBeginningMarker: boolean;
125
+ requiresEndMarker: boolean;
126
+ needsBeginningMarker: boolean;
127
+ needsEndMarker: boolean;
128
+ }
129
+
130
+ interface CaptureTimestampWrites {
131
+ readonly querySet: QuerySet;
132
+ beginningOfPassWriteIndex: number;
133
+ endOfPassWriteIndex: number;
134
+ }
135
+
136
+ interface CaptureMarkerWrites {
137
+ readonly querySet: QuerySet;
138
+ beginningOfPassWriteIndex?: number | undefined;
139
+ endOfPassWriteIndex?: number | undefined;
140
+ }
141
+
142
+ interface CaptureLayoutEntry {
143
+ readonly identity: GpuPassTimingPassIdentity;
144
+ readonly beginningQueryIndex: number;
145
+ readonly endQueryIndex: number;
146
+ readonly requiresBeginningMarker: boolean;
147
+ readonly requiresEndMarker: boolean;
148
+ }
149
+
150
+ interface CaptureLayout {
151
+ readonly graphGeneration: number;
152
+ readonly queryCount: number;
153
+ readonly entries: readonly CaptureLayoutEntry[];
154
+ }
155
+
156
+ function isTimingEntry(entry: CaptureEntry): entry is GpuPassTimingEntry {
157
+ return 'status' in entry;
158
+ }
159
+
160
+ function isGenericEntry(entry: CaptureEntry | undefined): entry is GpuPassTimingPassIdentity {
161
+ return entry !== undefined && !isTimingEntry(entry);
162
+ }
163
+
164
+ function reason(
165
+ code: GpuPassTimingReason['code'],
166
+ expected: string,
167
+ hint: string,
168
+ detail: { readonly [key: string]: null | boolean | number | string },
169
+ ): GpuPassTimingReason {
170
+ return { code, expected, hint, detail };
171
+ }
172
+
173
+ function causeDetail(value: unknown): { readonly [key: string]: null | boolean | number | string } {
174
+ if (typeof value === 'object' && value !== null) {
175
+ const code = 'code' in value && typeof value.code === 'string' ? value.code : undefined;
176
+ const message = value instanceof Error ? value.message : undefined;
177
+ return {
178
+ type: 'rhi-failure',
179
+ ...(code === undefined ? {} : { code }),
180
+ ...(message === undefined ? {} : { message }),
181
+ };
182
+ }
183
+ return { type: typeof value };
184
+ }
185
+
186
+ function alignedReadbackSize(queryCount: number): number {
187
+ return Math.max(QUERY_RESOLVE_ALIGNMENT, queryCount * QUERY_RESULT_BYTES);
188
+ }
189
+
190
+ function readTick(bytes: ArrayBuffer, index: number): string {
191
+ const view = new DataView(bytes, index * QUERY_RESULT_BYTES, QUERY_RESULT_BYTES);
192
+ return view.getBigUint64(0, true).toString(10);
193
+ }
194
+
195
+ function createResourceFailure(error: unknown, operation: string): GpuPassTimingReason {
196
+ return reason(
197
+ 'timestamp-readback-failed',
198
+ `${operation} succeeds for the timing slot resource`,
199
+ 'wait for a later receipt and inspect the structured timing failure',
200
+ { operation, cause: JSON.stringify(causeDetail(error)) },
201
+ );
202
+ }
203
+
204
+ function createSlot(
205
+ device: RhiDevice,
206
+ options: NormalizedGpuPassTimingOptions,
207
+ ): Result<Slot, GpuPassTimingReason> {
208
+ const queryCount = options.maxPassesPerFrame * 2;
209
+ const querySet = device.createQuerySet({ type: 'timestamp', count: queryCount });
210
+ if (!querySet.ok) {
211
+ return err(
212
+ reason(
213
+ 'timestamp-query-unsupported',
214
+ 'the active device creates a timestamp query set',
215
+ 'use a backend with timestamp-query support or keep timing disabled',
216
+ { cause: querySet.error.code },
217
+ ),
218
+ );
219
+ }
220
+ const resolveBuffer = device.createBuffer({
221
+ size: alignedReadbackSize(queryCount),
222
+ usage: GPU_BUFFER_USAGE_QUERY_RESOLVE | GPU_BUFFER_USAGE_COPY_SRC,
223
+ });
224
+ if (!resolveBuffer.ok) {
225
+ device.destroyQuerySet(querySet.value);
226
+ return err(createResourceFailure(resolveBuffer.error, 'resolve buffer creation'));
227
+ }
228
+ const readbackBuffer = device.createBuffer({
229
+ size: alignedReadbackSize(queryCount),
230
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_MAP_READ,
231
+ });
232
+ if (!readbackBuffer.ok) {
233
+ device.destroyBuffer(resolveBuffer.value);
234
+ device.destroyQuerySet(querySet.value);
235
+ return err(createResourceFailure(readbackBuffer.error, 'readback buffer creation'));
236
+ }
237
+ const copyBeginningMarkerWrites: CaptureMarkerWrites = {
238
+ querySet: querySet.value,
239
+ endOfPassWriteIndex: 0,
240
+ };
241
+ const copyEndMarkerWrites: CaptureMarkerWrites = {
242
+ querySet: querySet.value,
243
+ beginningOfPassWriteIndex: 0,
244
+ };
245
+ return ok({
246
+ queryCount,
247
+ querySet: querySet.value,
248
+ resolveBuffer: resolveBuffer.value,
249
+ readbackBuffer: readbackBuffer.value,
250
+ storage: {
251
+ entries: [],
252
+ queryPairs: Array.from({ length: options.maxPassesPerFrame }, () => ({
253
+ beginningQueryIndex: 0,
254
+ endQueryIndex: 0,
255
+ requiresBeginningMarker: false,
256
+ requiresEndMarker: false,
257
+ needsBeginningMarker: false,
258
+ needsEndMarker: false,
259
+ })),
260
+ entryIndexesByExecutionIndex: Array.from(
261
+ { length: options.maxPassesPerFrame },
262
+ () => undefined,
263
+ ),
264
+ timestampWrites: Array.from({ length: options.maxPassesPerFrame }, () => ({
265
+ querySet: querySet.value,
266
+ beginningOfPassWriteIndex: 0,
267
+ endOfPassWriteIndex: 1,
268
+ })),
269
+ copyBeginningMarkerWrites,
270
+ copyEndMarkerWrites,
271
+ copyBeginningMarkerDescriptor: { timestampWrites: copyBeginningMarkerWrites },
272
+ copyEndMarkerDescriptor: { timestampWrites: copyEndMarkerWrites },
273
+ },
274
+ state: 'idle',
275
+ });
276
+ }
277
+
278
+ function makeUnmeasured(identity: GpuPassTimingPassIdentity): GpuPassTimingEntry {
279
+ return {
280
+ ...identity,
281
+ status: 'unmeasured',
282
+ reason: reason(
283
+ 'timestamp-write-unavailable',
284
+ 'the pass has a reserved timestamp pair',
285
+ 'inspect the terminal observation for the readback result',
286
+ { passName: identity.passName },
287
+ ),
288
+ };
289
+ }
290
+
291
+ function makeOwnerConflict(identity: GpuPassTimingPassIdentity): GpuPassTimingEntry {
292
+ return {
293
+ ...identity,
294
+ status: 'unmeasured',
295
+ reason: reason(
296
+ 'timestamp-owner-conflict',
297
+ 'the pass already has producer-owned timestamp writes',
298
+ 'keep the producer timestamps and report this pass without generic timing',
299
+ { passName: identity.passName },
300
+ ),
301
+ };
302
+ }
303
+
304
+ function makeWriteUnavailable(
305
+ identity: GpuPassTimingPassIdentity,
306
+ phase: 'begin' | 'end',
307
+ cause: unknown,
308
+ ): GpuPassTimingEntry {
309
+ return {
310
+ ...identity,
311
+ status: 'unmeasured',
312
+ reason: {
313
+ ...reason(
314
+ 'timestamp-write-unavailable',
315
+ `the ${phase} timestamp write succeeds for the copy pass`,
316
+ 'keep this pass unmeasured and inspect a later receipt for timing recovery',
317
+ { passName: identity.passName, phase },
318
+ ),
319
+ cause: causeDetail(cause),
320
+ },
321
+ };
322
+ }
323
+
324
+ function freezeSnapshot(state: CaptureState): GpuPassTimingFrame {
325
+ const passes = state.entries.map((entry) =>
326
+ isTimingEntry(entry) ? { ...entry } : makeUnmeasured(entry),
327
+ );
328
+ return freezeGpuPassTimingFrame(
329
+ createGpuPassTimingFrame({
330
+ ...state.identity,
331
+ graphGeneration: state.graphGeneration,
332
+ backendKind: state.backendKind,
333
+ timestampPeriodNanoseconds: state.timestampPeriodNanoseconds,
334
+ passCapacity: state.slot.queryCount / 2,
335
+ passes,
336
+ droppedPassCount: state.droppedPassCount,
337
+ }),
338
+ );
339
+ }
340
+
341
+ function makeCapture(
342
+ state: CaptureState,
343
+ device: RhiDevice,
344
+ period: number,
345
+ mapReadback: GpuPassTimingSessionOptions['mapReadback'],
346
+ layout: CaptureLayout | undefined,
347
+ publishLayout: (layout: CaptureLayout) => void,
348
+ ): GpuPassTimingCapture {
349
+ const entryAt = (index: number): CaptureEntry | undefined => state.entries[index];
350
+ const queryPairAt = (index: number): CaptureQueryPair | undefined => state.queryPairs[index];
351
+ const allocateQueryIndex = (): number => {
352
+ const queryIndex = state.nextQueryIndex;
353
+ state.nextQueryIndex += 1;
354
+ return queryIndex;
355
+ };
356
+ const syncTimestampWrites = (index: number): void => {
357
+ const pair = queryPairAt(index);
358
+ const writes = state.slot.storage.timestampWrites[index];
359
+ if (pair === undefined || writes === undefined) return;
360
+ writes.beginningOfPassWriteIndex = pair.beginningQueryIndex;
361
+ writes.endOfPassWriteIndex = pair.endQueryIndex;
362
+ };
363
+ const configureDynamicPair = (index: number): void => {
364
+ const entry = entryAt(index);
365
+ const pair = queryPairAt(index);
366
+ if (entry === undefined || pair === undefined) return;
367
+ const beginningQueryIndex = allocateQueryIndex();
368
+ const requiresEndMarker = entry.passKind === 'copy';
369
+ pair.beginningQueryIndex = beginningQueryIndex;
370
+ pair.endQueryIndex = allocateQueryIndex();
371
+ pair.requiresBeginningMarker = entry.passKind === 'copy';
372
+ pair.requiresEndMarker = requiresEndMarker;
373
+ pair.needsBeginningMarker = pair.requiresBeginningMarker;
374
+ pair.needsEndMarker = requiresEndMarker;
375
+ syncTimestampWrites(index);
376
+ };
377
+ const rebuildDynamicLayout = (): void => {
378
+ state.nextQueryIndex = 0;
379
+ for (let index = 0; index < state.entries.length; index += 1) {
380
+ configureDynamicPair(index);
381
+ }
382
+ };
383
+ const markEntry = (index: number, replacement: GpuPassTimingEntry): void => {
384
+ const entry = entryAt(index);
385
+ if (!isGenericEntry(entry)) return;
386
+ state.timedPassCount -= 1;
387
+ state.entries[index] = replacement;
388
+ };
389
+ const recordPass = (
390
+ identity: GpuPassTimingPassIdentity,
391
+ ): ReturnType<GpuPassTimingCapture['recordPass']> => {
392
+ if (state.terminal !== undefined) return;
393
+ if (state.entries.length >= state.slot.queryCount / 2) {
394
+ state.droppedPassCount += 1;
395
+ return;
396
+ }
397
+ const index = state.entries.length;
398
+ const layoutEntry = state.layoutCandidateValid ? layout?.entries[index] : undefined;
399
+ const reusableLayoutEntry =
400
+ layoutEntry !== undefined &&
401
+ layoutEntry.identity.passName === identity.passName &&
402
+ layoutEntry.identity.passKind === identity.passKind &&
403
+ layoutEntry.identity.executionIndex === identity.executionIndex;
404
+ const layoutMismatch =
405
+ layout !== undefined && state.layoutCandidateValid && !reusableLayoutEntry;
406
+ if (layoutMismatch) state.layoutCandidateValid = false;
407
+ state.entries.push(
408
+ reusableLayoutEntry
409
+ ? layoutEntry.identity
410
+ : {
411
+ passName: identity.passName,
412
+ passKind: identity.passKind,
413
+ executionIndex: identity.executionIndex,
414
+ },
415
+ );
416
+ state.entryIndexesByExecutionIndex[identity.executionIndex] = index;
417
+ const queryPair = queryPairAt(index);
418
+ if (queryPair === undefined) {
419
+ state.entries.pop();
420
+ state.entryIndexesByExecutionIndex[identity.executionIndex] = undefined;
421
+ state.droppedPassCount += 1;
422
+ return;
423
+ }
424
+ if (reusableLayoutEntry) {
425
+ queryPair.beginningQueryIndex = layoutEntry.beginningQueryIndex;
426
+ queryPair.endQueryIndex = layoutEntry.endQueryIndex;
427
+ queryPair.requiresBeginningMarker = layoutEntry.requiresBeginningMarker;
428
+ queryPair.requiresEndMarker = layoutEntry.requiresEndMarker;
429
+ queryPair.needsBeginningMarker = layoutEntry.requiresBeginningMarker;
430
+ queryPair.needsEndMarker = layoutEntry.requiresEndMarker;
431
+ syncTimestampWrites(index);
432
+ } else if (layoutMismatch) {
433
+ rebuildDynamicLayout();
434
+ } else {
435
+ configureDynamicPair(index);
436
+ }
437
+ state.timedPassCount += 1;
438
+ const writes = state.slot.storage.timestampWrites[index];
439
+ if (writes === undefined) {
440
+ state.entries.pop();
441
+ state.entryIndexesByExecutionIndex[identity.executionIndex] = undefined;
442
+ state.timedPassCount -= 1;
443
+ state.droppedPassCount += 1;
444
+ return undefined;
445
+ }
446
+ return writes;
447
+ };
448
+ const timestampWrites = (identity: GpuPassTimingPassIdentity) => {
449
+ const index = state.entryIndexesByExecutionIndex[identity.executionIndex];
450
+ const entry = index === undefined ? undefined : state.entries[index];
451
+ if (
452
+ index === undefined ||
453
+ entry === undefined ||
454
+ entry.passName !== identity.passName ||
455
+ entry.passKind !== identity.passKind ||
456
+ isTimingEntry(entry) ||
457
+ state.terminal !== undefined
458
+ )
459
+ return undefined;
460
+ const pair = queryPairAt(index);
461
+ const writes = state.slot.storage.timestampWrites[index];
462
+ if (pair === undefined || writes === undefined) return undefined;
463
+ writes.beginningOfPassWriteIndex = pair.beginningQueryIndex;
464
+ writes.endOfPassWriteIndex = pair.endQueryIndex;
465
+ return writes;
466
+ };
467
+ const markOwnerConflict = (identity: GpuPassTimingPassIdentity): void => {
468
+ if (state.terminal !== undefined) return;
469
+ const index = state.entryIndexesByExecutionIndex[identity.executionIndex];
470
+ const entry = index === undefined ? undefined : state.entries[index];
471
+ if (
472
+ index === undefined ||
473
+ entry === undefined ||
474
+ entry.passName !== identity.passName ||
475
+ entry.passKind !== identity.passKind ||
476
+ isTimingEntry(entry)
477
+ )
478
+ return;
479
+ markEntry(index, makeOwnerConflict(identity));
480
+ };
481
+ const markTimestampWriteFailure = (
482
+ identity: GpuPassTimingPassIdentity,
483
+ phase: 'begin' | 'end',
484
+ cause: unknown,
485
+ ): void => {
486
+ if (state.terminal !== undefined) return;
487
+ const index = state.entryIndexesByExecutionIndex[identity.executionIndex];
488
+ const entry = index === undefined ? undefined : state.entries[index];
489
+ if (
490
+ index === undefined ||
491
+ entry === undefined ||
492
+ entry.passName !== identity.passName ||
493
+ entry.passKind !== identity.passKind
494
+ )
495
+ return;
496
+ if (entry !== undefined && isTimingEntry(entry)) {
497
+ if (entry.status === 'unmeasured' && entry.reason.code === 'timestamp-owner-conflict') return;
498
+ return;
499
+ }
500
+ if (entry === undefined) return;
501
+ markEntry(index, makeWriteUnavailable(identity, phase, cause));
502
+ };
503
+ const copyBoundaryBefore = (
504
+ identity: GpuPassTimingPassIdentity,
505
+ encoder: RhiCommandEncoder,
506
+ ): void => {
507
+ const index = state.entryIndexesByExecutionIndex[identity.executionIndex];
508
+ const entry = index === undefined ? undefined : entryAt(index);
509
+ const pair = index === undefined ? undefined : queryPairAt(index);
510
+ if (
511
+ index === undefined ||
512
+ !isGenericEntry(entry) ||
513
+ entry.passName !== identity.passName ||
514
+ entry.passKind !== identity.passKind ||
515
+ pair === undefined ||
516
+ !pair.needsBeginningMarker ||
517
+ state.terminal !== undefined
518
+ ) {
519
+ return;
520
+ }
521
+ try {
522
+ const writes = state.slot.storage.copyBeginningMarkerWrites;
523
+ writes.endOfPassWriteIndex = pair.beginningQueryIndex;
524
+ encoder.encodeEmptyComputePass(state.slot.storage.copyBeginningMarkerDescriptor);
525
+ pair.needsBeginningMarker = false;
526
+ } catch (cause) {
527
+ markTimestampWriteFailure(identity, 'begin', cause);
528
+ }
529
+ };
530
+ const copyBoundaryAfter = (
531
+ identity: GpuPassTimingPassIdentity,
532
+ encoder: RhiCommandEncoder,
533
+ ): void => {
534
+ const index = state.entryIndexesByExecutionIndex[identity.executionIndex];
535
+ if (index === undefined) return;
536
+ const entry = entryAt(index);
537
+ const pair = queryPairAt(index);
538
+ if (
539
+ !isGenericEntry(entry) ||
540
+ entry.passKind !== 'copy' ||
541
+ pair === undefined ||
542
+ entry.passName !== identity.passName ||
543
+ !pair.needsEndMarker ||
544
+ state.terminal !== undefined
545
+ ) {
546
+ return;
547
+ }
548
+ try {
549
+ const writes = state.slot.storage.copyEndMarkerWrites;
550
+ writes.beginningOfPassWriteIndex = pair.endQueryIndex;
551
+ encoder.encodeEmptyComputePass(state.slot.storage.copyEndMarkerDescriptor);
552
+ pair.needsEndMarker = false;
553
+ } catch (cause) {
554
+ markTimestampWriteFailure(entry, 'end', cause);
555
+ }
556
+ };
557
+ const maybePublishLayout = (): void => {
558
+ if (
559
+ (layout !== undefined &&
560
+ state.layoutCandidateValid &&
561
+ state.entries.length === layout.entries.length) ||
562
+ state.droppedPassCount > 0 ||
563
+ state.entries.length === 0 ||
564
+ state.entries.some((entry) => !isGenericEntry(entry))
565
+ ) {
566
+ return;
567
+ }
568
+ const entries: CaptureLayoutEntry[] = [];
569
+ for (let index = 0; index < state.entries.length; index += 1) {
570
+ const identity = state.entries[index];
571
+ const queryPair = queryPairAt(index);
572
+ if (!isGenericEntry(identity) || queryPair === undefined) return;
573
+ entries.push(
574
+ Object.freeze({
575
+ identity: Object.freeze({ ...identity }),
576
+ beginningQueryIndex: queryPair.beginningQueryIndex,
577
+ endQueryIndex: queryPair.endQueryIndex,
578
+ requiresBeginningMarker: queryPair.requiresBeginningMarker,
579
+ requiresEndMarker: queryPair.requiresEndMarker,
580
+ }),
581
+ );
582
+ }
583
+ publishLayout(
584
+ Object.freeze({
585
+ graphGeneration: state.graphGeneration,
586
+ queryCount: state.nextQueryIndex,
587
+ entries: Object.freeze(entries),
588
+ }),
589
+ );
590
+ };
591
+ const hasGenericTiming = (): boolean => state.timedPassCount > 0;
592
+ const encodeTail = (encoder: RhiCommandEncoder): Result<void, GpuPassTimingReason> => {
593
+ if (state.terminal !== undefined) {
594
+ return state.terminal.ok
595
+ ? err(
596
+ reason(
597
+ 'timing-session-disposed',
598
+ 'the timing capture is still encoding',
599
+ 'encode timing before terminalizing the capture',
600
+ { frameId: state.identity.frameId },
601
+ ),
602
+ )
603
+ : err(state.terminal.error);
604
+ }
605
+ if (
606
+ layout !== undefined &&
607
+ state.layoutCandidateValid &&
608
+ state.entries.length !== layout.entries.length
609
+ ) {
610
+ state.layoutCandidateValid = false;
611
+ rebuildDynamicLayout();
612
+ }
613
+ maybePublishLayout();
614
+ if (!hasGenericTiming()) return ok(undefined);
615
+ const queryCount = state.nextQueryIndex;
616
+ try {
617
+ const resolved = encoder.resolveQuerySet(
618
+ state.slot.querySet,
619
+ 0,
620
+ queryCount,
621
+ state.slot.resolveBuffer,
622
+ 0,
623
+ );
624
+ if (!resolved.ok) {
625
+ return err(
626
+ reason(
627
+ 'timestamp-resolve-failed',
628
+ 'the used timestamp range resolves into the resolve buffer',
629
+ 'inspect the RHI cause and wait for a later receipt',
630
+ { cause: resolved.error.code },
631
+ ),
632
+ );
633
+ }
634
+ encoder.copyBufferToBuffer(
635
+ state.slot.resolveBuffer,
636
+ 0,
637
+ state.slot.readbackBuffer,
638
+ 0,
639
+ queryCount * QUERY_RESULT_BYTES,
640
+ );
641
+ return ok(undefined);
642
+ } catch (error) {
643
+ return err(createResourceFailure(error, 'timing resolve/copy'));
644
+ }
645
+ };
646
+ const terminalize = (result: Result<GpuPassTimingFrame, GpuPassTimingReason>): void => {
647
+ if (state.terminal !== undefined) return;
648
+ state.terminal = result;
649
+ state.slot.state = 'terminal';
650
+ };
651
+ const readback = async (): Promise<void> => {
652
+ if (state.terminal !== undefined) return;
653
+ const mapped = await sessionMapReadback(state.slot.readbackBuffer, period, mapReadback);
654
+ if (!mapped.ok) {
655
+ if (state.terminal !== undefined) return;
656
+ terminalize(err(mapped.error));
657
+ state.slot.state = 'idle';
658
+ return;
659
+ }
660
+ if (state.terminal !== undefined) {
661
+ mapped.value.unmap();
662
+ return;
663
+ }
664
+ const range = mapped.value.getMappedRange(0, state.nextQueryIndex * QUERY_RESULT_BYTES);
665
+ if (!range.ok) {
666
+ mapped.value.unmap();
667
+ terminalize(err(createResourceFailure(range.error, 'mapped timing range')));
668
+ state.slot.state = 'idle';
669
+ return;
670
+ }
671
+ const measured: GpuPassTimingEntry[] = [];
672
+ for (let index = 0; index < state.entries.length; index += 1) {
673
+ const identity = state.entries[index];
674
+ if (identity === undefined) continue;
675
+ if (isTimingEntry(identity)) {
676
+ measured.push(identity);
677
+ continue;
678
+ }
679
+ const queryPair = state.queryPairs[index];
680
+ if (queryPair === undefined) {
681
+ mapped.value.unmap();
682
+ terminalize(
683
+ err(
684
+ reason(
685
+ 'timestamp-readback-failed',
686
+ 'every measured pass has a reserved query pair',
687
+ 'inspect the capture query allocator before consuming timing facts',
688
+ { frameId: state.identity.frameId, passIndex: index },
689
+ ),
690
+ ),
691
+ );
692
+ state.slot.state = 'idle';
693
+ return;
694
+ }
695
+ const beginningTick = readTick(range.value, queryPair.beginningQueryIndex);
696
+ const endTick = readTick(range.value, queryPair.endQueryIndex);
697
+ const parsed = parseGpuPassTimingTicks({
698
+ beginningTick,
699
+ endTick,
700
+ timestampPeriodNanoseconds: period,
701
+ });
702
+ if (!parsed.ok) {
703
+ if (identity.passKind === 'copy') {
704
+ measured.push({
705
+ ...identity,
706
+ status: 'unmeasured',
707
+ reason: {
708
+ ...parsed.error,
709
+ detail: {
710
+ ...parsed.error.detail,
711
+ passName: identity.passName,
712
+ passKind: identity.passKind,
713
+ executionIndex: identity.executionIndex,
714
+ beginningQueryIndex: queryPair.beginningQueryIndex,
715
+ endQueryIndex: queryPair.endQueryIndex,
716
+ },
717
+ },
718
+ });
719
+ continue;
720
+ }
721
+ mapped.value.unmap();
722
+ terminalize(
723
+ err({
724
+ ...parsed.error,
725
+ detail: {
726
+ ...parsed.error.detail,
727
+ passName: identity.passName,
728
+ passKind: identity.passKind,
729
+ executionIndex: identity.executionIndex,
730
+ beginningQueryIndex: queryPair.beginningQueryIndex,
731
+ endQueryIndex: queryPair.endQueryIndex,
732
+ },
733
+ }),
734
+ );
735
+ state.slot.state = 'idle';
736
+ return;
737
+ }
738
+ measured.push({
739
+ passName: identity.passName,
740
+ passKind: identity.passKind,
741
+ executionIndex: identity.executionIndex,
742
+ measurementSource:
743
+ identity.passKind === 'copy' ? 'copy-boundary-envelope' : 'pass-boundary',
744
+ ...parsed.value,
745
+ });
746
+ }
747
+ mapped.value.unmap();
748
+ state.entries.splice(0, state.entries.length, ...measured);
749
+ terminalize(ok(freezeSnapshot(state)));
750
+ state.slot.state = 'idle';
751
+ };
752
+ return {
753
+ frameId: state.identity.frameId,
754
+ recordPass,
755
+ markOwnerConflict,
756
+ markTimestampWriteFailure,
757
+ copyBoundaryBefore,
758
+ copyBoundaryAfter,
759
+ timestampWrites,
760
+ encodeTail,
761
+ markSubmitted: (completion) => {
762
+ if (state.terminal !== undefined || state.submitted !== undefined) return;
763
+ if (!hasGenericTiming()) {
764
+ terminalize(ok(freezeSnapshot(state)));
765
+ state.slot.state = 'idle';
766
+ return;
767
+ }
768
+ state.slot.state = 'submitted';
769
+ state.submitted = (completion ?? device.queue.onSubmittedWorkDone())
770
+ .then(readback)
771
+ .catch((error) => {
772
+ terminalize(err(createResourceFailure(error, 'queue completion')));
773
+ state.slot.state = 'idle';
774
+ });
775
+ },
776
+ snapshot: () => (state.terminal?.ok === true ? state.terminal.value : freezeSnapshot(state)),
777
+ observe: async () => {
778
+ if (state.submitted !== undefined) await state.submitted;
779
+ if (state.terminal !== undefined) return state.terminal;
780
+ return err(
781
+ reason(
782
+ 'timing-session-disposed',
783
+ 'the timing capture reaches a terminal state before observation',
784
+ 'submit the frame or abort the capture before observing it',
785
+ { frameId: state.identity.frameId },
786
+ ),
787
+ );
788
+ },
789
+ abort: (cause) => {
790
+ terminalize(
791
+ err(
792
+ reason(
793
+ 'timestamp-readback-failed',
794
+ 'an aborted timing capture is terminal without GPU readback',
795
+ 'ignore the failed timing receipt and continue rendering',
796
+ { frameId: state.identity.frameId, cause: cause?.code ?? 'aborted' },
797
+ ),
798
+ ),
799
+ );
800
+ state.slot.state = 'idle';
801
+ },
802
+ };
803
+ }
804
+
805
+ async function sessionMapReadback(
806
+ buffer: Buffer,
807
+ period: number,
808
+ override: GpuPassTimingSessionOptions['mapReadback'],
809
+ ): Promise<Result<MappedBuffer, GpuPassTimingReason>> {
810
+ try {
811
+ const mapped = await (override === undefined
812
+ ? buffer.mapAsync(GPU_MAP_MODE_READ)
813
+ : override(buffer));
814
+ if (!mapped.ok) {
815
+ return err(
816
+ reason(
817
+ 'timestamp-readback-failed',
818
+ 'the resolved timing buffer maps for read access',
819
+ 'inspect the RHI map error and wait for a later receipt',
820
+ { ...causeDetail(mapped.error), timestampPeriodNanoseconds: period },
821
+ ),
822
+ );
823
+ }
824
+ return ok(mapped.value);
825
+ } catch (error) {
826
+ return err(createResourceFailure(error, 'timing readback map'));
827
+ }
828
+ }
829
+
830
+ export function createGpuPassTimingSession(
831
+ device: RhiDevice,
832
+ options: GpuPassTimingOptions = {},
833
+ sessionOptions: GpuPassTimingSessionOptions = {},
834
+ ): Result<GpuPassTimingSession, GpuPassTimingReason> {
835
+ const normalized = normalizeGpuPassTimingOptions(options);
836
+ if (!normalized.ok) return normalized;
837
+ const period = device.caps.timestampPeriodNanoseconds;
838
+ if (!device.caps.timestampQuery || period === null || !Number.isFinite(period) || period <= 0) {
839
+ return err(
840
+ reason(
841
+ 'timestamp-period-unavailable',
842
+ 'timestamp-query capability and a positive finite timestamp period are available',
843
+ 'use a backend with timestamp-query support and a trustworthy period',
844
+ { timestampQuery: device.caps.timestampQuery, timestampPeriodNanoseconds: period },
845
+ ),
846
+ );
847
+ }
848
+ const slots: Slot[] = [];
849
+ for (let index = 0; index < normalized.value.maxFramesInFlight; index += 1) {
850
+ const slot = createSlot(device, normalized.value);
851
+ if (!slot.ok) {
852
+ for (const created of slots) {
853
+ device.destroyBuffer(created.resolveBuffer);
854
+ device.destroyBuffer(created.readbackBuffer);
855
+ device.destroyQuerySet(created.querySet);
856
+ }
857
+ return slot;
858
+ }
859
+ slots.push(slot.value);
860
+ }
861
+ let disposed = false;
862
+ const mapReadback = sessionOptions.mapReadback;
863
+ let cachedLayout: CaptureLayout | undefined;
864
+ const publishLayout = (layout: CaptureLayout): void => {
865
+ cachedLayout = layout;
866
+ };
867
+ return ok({
868
+ options: normalized.value,
869
+ beginFrame: (identity) => {
870
+ if (disposed) {
871
+ return err(
872
+ reason(
873
+ 'timing-session-disposed',
874
+ 'the timing session is active',
875
+ 'create a new session after renderer recovery or disposal',
876
+ { frameId: identity.frameId },
877
+ ),
878
+ );
879
+ }
880
+ const slot = slots.find((candidate) => candidate.state === 'idle');
881
+ if (slot === undefined) {
882
+ return err(
883
+ reason(
884
+ 'timing-in-flight-exhausted',
885
+ 'a bounded timing slot is idle',
886
+ 'increase maxFramesInFlight or reduce the capture rate',
887
+ { maxFramesInFlight: normalized.value.maxFramesInFlight },
888
+ ),
889
+ );
890
+ }
891
+ slot.state = 'encoding';
892
+ slot.storage.entries.length = 0;
893
+ slot.storage.entryIndexesByExecutionIndex.fill(undefined);
894
+ const layout =
895
+ cachedLayout?.graphGeneration === identity.graphGeneration ? cachedLayout : undefined;
896
+ const state: CaptureState = {
897
+ identity,
898
+ graphGeneration: identity.graphGeneration,
899
+ backendKind: device.caps.backendKind,
900
+ timestampPeriodNanoseconds: period,
901
+ slot,
902
+ entries: slot.storage.entries,
903
+ queryPairs: slot.storage.queryPairs,
904
+ entryIndexesByExecutionIndex: slot.storage.entryIndexesByExecutionIndex,
905
+ layout,
906
+ layoutCandidateValid: true,
907
+ nextQueryIndex: layout?.queryCount ?? 0,
908
+ droppedPassCount: 0,
909
+ timedPassCount: 0,
910
+ terminal: undefined,
911
+ submitted: undefined,
912
+ };
913
+ slot.capture = state;
914
+ const capture = makeCapture(state, device, period, mapReadback, layout, publishLayout);
915
+ return ok(capture);
916
+ },
917
+ dispose: () => {
918
+ if (disposed) return;
919
+ disposed = true;
920
+ for (const slot of slots) {
921
+ if (slot.capture !== undefined && slot.capture.terminal === undefined) {
922
+ slot.capture.terminal = err(
923
+ reason(
924
+ 'timing-session-disposed',
925
+ 'the timing session is active while resources are disposed',
926
+ 'create a new session after renderer recovery or disposal',
927
+ { frameId: slot.capture.identity.frameId },
928
+ ),
929
+ );
930
+ }
931
+ device.destroyBuffer(slot.resolveBuffer);
932
+ device.destroyBuffer(slot.readbackBuffer);
933
+ device.destroyQuerySet(slot.querySet);
934
+ slot.state = 'terminal';
935
+ }
936
+ },
937
+ });
938
+ }