@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,194 @@
1
+ import type { TextureAsset } from '@forgeax/engine-types';
2
+ import { COOKIE_SLICE_SIZE } from './resources';
3
+
4
+ export interface SpotModifierFactors {
5
+ readonly brdf: number;
6
+ readonly range: number;
7
+ readonly cone: number;
8
+ readonly ies?: number | undefined;
9
+ readonly cookie?: number | undefined;
10
+ readonly shadow: number;
11
+ }
12
+
13
+ export interface IesCoordinates {
14
+ readonly azimuth: number;
15
+ readonly elevation: number;
16
+ }
17
+
18
+ export interface CookieUv {
19
+ readonly u: number;
20
+ readonly v: number;
21
+ }
22
+
23
+ /**
24
+ * The fixed GPU representation consumed by the extended-lighting Cookie
25
+ * array. The matrix is deliberately keyed by Cookie slice rather than by
26
+ * light: it carries only the source texture aspect correction. The shader
27
+ * still owns the light-local basis, roll, and outer-cone projection, so one
28
+ * Cookie can be shared by Spots with different directions or cone angles.
29
+ */
30
+ export interface CookieProjection {
31
+ readonly data: Uint8Array;
32
+ readonly matrix: Float32Array;
33
+ readonly aspect: number;
34
+ }
35
+
36
+ const DEG_TO_RAD = Math.PI / 180;
37
+
38
+ const COOKIE_PROJECTION_CACHE = new WeakMap<TextureAsset, CookieProjection | null>();
39
+
40
+ function srgbToLinear(value: number): number {
41
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
42
+ }
43
+
44
+ function clamp01(value: number): number {
45
+ return Math.min(1, Math.max(0, value));
46
+ }
47
+
48
+ function sourceChannel(asset: TextureAsset, x: number, y: number, channel: number): number {
49
+ const offset = (y * asset.shape.extent.width + x) * 4 + channel;
50
+ const encoded = (asset.data[offset] ?? 0) / 255;
51
+ return channel < 3 && asset.colorSpace === 'srgb' ? srgbToLinear(encoded) : encoded;
52
+ }
53
+
54
+ function sampleSourceChannel(asset: TextureAsset, x: number, y: number, channel: number): number {
55
+ const sourceX = clamp01((x + 0.5) / COOKIE_SLICE_SIZE) * (asset.shape.extent.width - 1);
56
+ const sourceY = clamp01((y + 0.5) / COOKIE_SLICE_SIZE) * (asset.shape.extent.height - 1);
57
+ const x0 = Math.floor(sourceX);
58
+ const y0 = Math.floor(sourceY);
59
+ const x1 = Math.min(asset.shape.extent.width - 1, x0 + 1);
60
+ const y1 = Math.min(asset.shape.extent.height - 1, y0 + 1);
61
+ const tx = sourceX - x0;
62
+ const ty = sourceY - y0;
63
+ const top =
64
+ sourceChannel(asset, x0, y0, channel) * (1 - tx) + sourceChannel(asset, x1, y0, channel) * tx;
65
+ const bottom =
66
+ sourceChannel(asset, x0, y1, channel) * (1 - tx) + sourceChannel(asset, x1, y1, channel) * tx;
67
+ return top * (1 - ty) + bottom * ty;
68
+ }
69
+
70
+ /**
71
+ * Project a filterable 2D RGBA Cookie into the renderer's fixed linear
72
+ * 256x256 array slice. This is a producer-boundary operation and is cached by
73
+ * the immutable TextureAsset object, so the frame path never decodes or
74
+ * resamples the source repeatedly.
75
+ */
76
+ export function prepareCookieProjection(asset: TextureAsset): CookieProjection | undefined {
77
+ if (COOKIE_PROJECTION_CACHE.has(asset)) {
78
+ return COOKIE_PROJECTION_CACHE.get(asset) ?? undefined;
79
+ }
80
+ const supportedFormat = asset.format === 'rgba8unorm' || asset.format === 'rgba8unorm-srgb';
81
+ const validDimensions =
82
+ Number.isSafeInteger(asset.shape.extent.width) &&
83
+ Number.isSafeInteger(asset.shape.extent.height) &&
84
+ asset.shape.extent.width > 0 &&
85
+ asset.shape.extent.height > 0;
86
+ const validFormatPair =
87
+ (asset.format === 'rgba8unorm-srgb' && asset.colorSpace === 'srgb') ||
88
+ (asset.format === 'rgba8unorm' && asset.colorSpace === 'linear');
89
+ const validPayload =
90
+ asset.data.byteLength >= asset.shape.extent.width * asset.shape.extent.height * 4;
91
+ if (!supportedFormat || !validDimensions || !validFormatPair || !validPayload) {
92
+ COOKIE_PROJECTION_CACHE.set(asset, null);
93
+ return undefined;
94
+ }
95
+
96
+ const data = new Uint8Array(COOKIE_SLICE_SIZE * COOKIE_SLICE_SIZE * 4);
97
+ for (let y = 0; y < COOKIE_SLICE_SIZE; y += 1) {
98
+ for (let x = 0; x < COOKIE_SLICE_SIZE; x += 1) {
99
+ const target = (y * COOKIE_SLICE_SIZE + x) * 4;
100
+ data[target] = Math.round(clamp01(sampleSourceChannel(asset, x, y, 0)) * 255);
101
+ data[target + 1] = Math.round(clamp01(sampleSourceChannel(asset, x, y, 1)) * 255);
102
+ data[target + 2] = Math.round(clamp01(sampleSourceChannel(asset, x, y, 2)) * 255);
103
+ // Alpha stays linear and is intentionally preserved: the shader
104
+ // multiplies sampled RGB by this lane as part of Cookie semantics.
105
+ data[target + 3] = Math.round(clamp01(sampleSourceChannel(asset, x, y, 3)) * 255);
106
+ }
107
+ }
108
+
109
+ // Column-major mat4. The shader multiplies the unprojected local
110
+ // (x/depth, y/depth) pair by this aspect-only matrix, then applies the
111
+ // Spot's outer-cone tangent and the [0,1] translation. Keeping outerCone
112
+ // out of this slice-keyed matrix allows one Cookie to be shared by lights.
113
+ const matrix = new Float32Array(16);
114
+ matrix[0] = asset.shape.extent.height / asset.shape.extent.width;
115
+ matrix[5] = 1;
116
+ matrix[10] = 1;
117
+ matrix[15] = 1;
118
+ const projection = {
119
+ data,
120
+ matrix,
121
+ aspect: asset.shape.extent.width / asset.shape.extent.height,
122
+ };
123
+ COOKIE_PROJECTION_CACHE.set(asset, projection);
124
+ return projection;
125
+ }
126
+
127
+ /** Create the deterministic identity payload used for unused Cookie slices. */
128
+ export function createCookieProjectionMatrixData(count: number): Float32Array {
129
+ const data = new Float32Array(count * 16);
130
+ for (let index = 0; index < count; index += 1) {
131
+ const base = index * 16;
132
+ data[base] = 1;
133
+ data[base + 5] = 1;
134
+ data[base + 10] = 1;
135
+ data[base + 15] = 1;
136
+ }
137
+ return data;
138
+ }
139
+
140
+ function normalized(vector: ArrayLike<number>): [number, number, number] | undefined {
141
+ const x = vector[0] ?? 0;
142
+ const y = vector[1] ?? 0;
143
+ const z = vector[2] ?? 0;
144
+ const length = Math.hypot(x, y, z);
145
+ return length > 0 && Number.isFinite(length) ? [x / length, y / length, z / length] : undefined;
146
+ }
147
+
148
+ export function spotModifierProduct(factors: SpotModifierFactors): number {
149
+ return (
150
+ factors.brdf *
151
+ factors.range *
152
+ factors.cone *
153
+ (factors.ies ?? 1) *
154
+ (factors.cookie ?? 1) *
155
+ factors.shadow
156
+ );
157
+ }
158
+
159
+ export function projectIesCoordinates(
160
+ toPoint: ArrayLike<number>,
161
+ rollDeg: number,
162
+ ): IesCoordinates | undefined {
163
+ const direction = normalized(toPoint);
164
+ if (direction === undefined || direction[2] >= 0) return undefined;
165
+ const roll = rollDeg * DEG_TO_RAD;
166
+ const rolledX = direction[0] * Math.cos(roll) - direction[1] * Math.sin(roll);
167
+ const rolledY = direction[0] * Math.sin(roll) + direction[1] * Math.cos(roll);
168
+ return {
169
+ azimuth: (Math.atan2(rolledY, rolledX) + Math.PI * 2) % (Math.PI * 2),
170
+ elevation: Math.acos(Math.min(1, Math.max(-1, -direction[2]))) / Math.PI,
171
+ };
172
+ }
173
+
174
+ export function projectCookieUv(
175
+ toPoint: ArrayLike<number>,
176
+ rollDeg: number,
177
+ aspect: number,
178
+ outerConeDeg = 45,
179
+ ): CookieUv | undefined {
180
+ const direction = normalized(toPoint);
181
+ if (direction === undefined || direction[2] >= 0 || !Number.isFinite(aspect) || aspect <= 0)
182
+ return undefined;
183
+ const cone = Math.atan2(Math.hypot(direction[0], direction[1]), -direction[2]);
184
+ const outerCone = outerConeDeg * DEG_TO_RAD;
185
+ if (cone > outerCone) return undefined;
186
+ const roll = rollDeg * DEG_TO_RAD;
187
+ const x = direction[0] * Math.cos(roll) - direction[1] * Math.sin(roll);
188
+ const y = direction[0] * Math.sin(roll) + direction[1] * Math.cos(roll);
189
+ const scale = Math.tan(outerCone);
190
+ return {
191
+ u: 0.5 + (x / -direction[2] / scale / aspect) * 0.5,
192
+ v: 0.5 + (y / -direction[2] / scale) * 0.5,
193
+ };
194
+ }
@@ -0,0 +1,92 @@
1
+ import type { LightResourceUnavailableError } from '../../errors/render';
2
+ import {
3
+ type LightInspection,
4
+ lightInspectionIdentity,
5
+ projectLightInspection,
6
+ } from '../../inspection-types';
7
+ import type { ExtendedLightingResourceCandidate } from './resources';
8
+
9
+ export type ExtendedLightingResourceStatus = 'empty' | 'candidate' | 'accepted';
10
+
11
+ export interface ExtendedLightingState {
12
+ readonly enabled: boolean;
13
+ readonly generation: number;
14
+ readonly status: ExtendedLightingResourceStatus;
15
+ readonly candidate: ExtendedLightingResourceCandidate | undefined;
16
+ readonly accepted: ExtendedLightingResourceCandidate | undefined;
17
+ readonly lastKnownGood: ExtendedLightingResourceCandidate | undefined;
18
+ readonly resourceCount: number;
19
+ readonly descriptorBytes: number;
20
+ readonly uploadCount: number;
21
+ readonly passCount: number;
22
+ readonly bindGroupCount: number;
23
+ readonly failure: LightResourceUnavailableError | undefined;
24
+ readonly failureKeys: readonly string[];
25
+ }
26
+
27
+ export function createExtendedLightingState(generation: number): ExtendedLightingState {
28
+ return {
29
+ enabled: false,
30
+ generation,
31
+ status: 'empty',
32
+ candidate: undefined,
33
+ accepted: undefined,
34
+ lastKnownGood: undefined,
35
+ resourceCount: 0,
36
+ descriptorBytes: 0,
37
+ uploadCount: 0,
38
+ passCount: 0,
39
+ bindGroupCount: 0,
40
+ failure: undefined,
41
+ failureKeys: [],
42
+ };
43
+ }
44
+
45
+ export function promoteExtendedLightingCandidate(
46
+ state: ExtendedLightingState,
47
+ candidate: ExtendedLightingResourceCandidate | undefined,
48
+ ): ExtendedLightingState {
49
+ if (candidate === undefined) return state;
50
+ return {
51
+ ...state,
52
+ enabled: true,
53
+ generation: candidate.generation,
54
+ status: 'accepted',
55
+ candidate: undefined,
56
+ accepted: candidate,
57
+ lastKnownGood: candidate,
58
+ resourceCount: 4,
59
+ descriptorBytes: candidate.descriptorBytes,
60
+ uploadCount: candidate.uploadCount,
61
+ };
62
+ }
63
+
64
+ export function recordExtendedLightingFailure(
65
+ state: ExtendedLightingState,
66
+ failure: LightResourceUnavailableError,
67
+ ): ExtendedLightingState {
68
+ const key = `${failure.detail.entity}:${failure.detail.feature}:${failure.detail.generation}`;
69
+ if (state.failureKeys.includes(key)) return state;
70
+ return {
71
+ ...state,
72
+ failure,
73
+ failureKeys: [...state.failureKeys, key],
74
+ };
75
+ }
76
+
77
+ export function projectExtendedLightingInspection(state: ExtendedLightingState): LightInspection {
78
+ const identity = (candidate: ExtendedLightingResourceCandidate | undefined) =>
79
+ candidate === undefined
80
+ ? undefined
81
+ : lightInspectionIdentity(candidate.topology, candidate.generation);
82
+ return projectLightInspection({
83
+ generation: state.generation,
84
+ candidate: identity(state.candidate),
85
+ accepted: identity(state.accepted),
86
+ lastKnownGood: identity(state.lastKnownGood),
87
+ failure: state.failure?.code,
88
+ failureKeys: state.failureKeys,
89
+ resourceCount: state.resourceCount,
90
+ uploadBytes: state.descriptorBytes,
91
+ });
92
+ }
@@ -0,0 +1,177 @@
1
+ import type { Buffer, QuerySet, RhiCommandEncoder, RhiDevice } from '@forgeax/engine-rhi';
2
+ import { ok } from '@forgeax/engine-types';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { GPU_BUFFER_USAGE_MAP_READ } from '../../gpu-usage';
5
+ import { GpuTimingCapture } from '../gpu-timing';
6
+
7
+ function timestampDevice(readbackBytes: ArrayBuffer): {
8
+ readonly device: RhiDevice;
9
+ readonly encoder: RhiCommandEncoder;
10
+ readonly resolveQuerySet: ReturnType<typeof vi.fn>;
11
+ readonly readbackBuffer: Buffer;
12
+ readonly getMappedRange: ReturnType<typeof vi.fn>;
13
+ readonly destroyBuffer: ReturnType<typeof vi.fn>;
14
+ readonly destroyQuerySet: ReturnType<typeof vi.fn>;
15
+ } {
16
+ const querySet = {} as QuerySet;
17
+ const resolveBuffer = {} as Buffer;
18
+ const getMappedRange = vi.fn(() => ok(readbackBytes));
19
+ const unmap = vi.fn();
20
+ const readbackBuffer = {
21
+ mapAsync: vi.fn(async (mode: number) => {
22
+ expect(mode).toBe(GPU_BUFFER_USAGE_MAP_READ);
23
+ return ok({
24
+ getMappedRange,
25
+ unmap,
26
+ } as never);
27
+ }),
28
+ } as unknown as Buffer;
29
+ let bufferIndex = 0;
30
+ const resolveQuerySet = vi.fn(() => ok(undefined));
31
+ const destroyBuffer = vi.fn(() => ok(undefined));
32
+ const destroyQuerySet = vi.fn(() => ok(undefined));
33
+ const encoder = {
34
+ resolveQuerySet,
35
+ copyBufferToBuffer: vi.fn(),
36
+ } as unknown as RhiCommandEncoder;
37
+ const device = {
38
+ caps: {
39
+ backendKind: 'webgpu',
40
+ compute: true,
41
+ storageBuffer: true,
42
+ storageTexture: true,
43
+ timestampQuery: true,
44
+ timestampPeriodNanoseconds: 1,
45
+ indirectDrawing: true,
46
+ textureCompressionBc: false,
47
+ textureCompressionEtc2: false,
48
+ textureCompressionAstc: false,
49
+ multiDrawIndirect: false,
50
+ pushConstants: false,
51
+ textureBindingArray: false,
52
+ samplerAliasing: true,
53
+ firstInstanceIndirect: true,
54
+ rgba16floatRenderable: true,
55
+ rg11b10ufloatRenderable: true,
56
+ float32Filterable: true,
57
+ maxColorAttachments: 8,
58
+ },
59
+ createQuerySet: vi.fn(() => ok(querySet)),
60
+ createBuffer: vi.fn(() => {
61
+ bufferIndex += 1;
62
+ return ok(bufferIndex === 1 ? resolveBuffer : readbackBuffer);
63
+ }),
64
+ destroyQuerySet,
65
+ destroyBuffer,
66
+ queue: { onSubmittedWorkDone: vi.fn(async () => undefined) },
67
+ } as unknown as RhiDevice;
68
+ return {
69
+ device,
70
+ encoder,
71
+ resolveQuerySet,
72
+ readbackBuffer,
73
+ getMappedRange,
74
+ destroyBuffer,
75
+ destroyQuerySet,
76
+ };
77
+ }
78
+
79
+ function writeTimestampPair(bytes: ArrayBuffer, index: number, begin: bigint, end: bigint): void {
80
+ const view = new DataView(bytes);
81
+ view.setBigUint64(index * 16, begin, true);
82
+ view.setBigUint64(index * 16 + 8, end, true);
83
+ }
84
+
85
+ describe('GpuTimingCapture pass descriptor contract', () => {
86
+ it('resolves real pass pairs without calling command-encoder writeTimestamp', async () => {
87
+ const bytes = new ArrayBuffer(64 * 8);
88
+ writeTimestampPair(bytes, 0, 100n, 200n);
89
+ writeTimestampPair(bytes, 1, 210n, 450n);
90
+ const { device, encoder, resolveQuerySet } = timestampDevice(bytes);
91
+ const created = GpuTimingCapture.create(device);
92
+ expect(created.ok).toBe(true);
93
+ if (!created.ok || created.value === undefined) return;
94
+
95
+ const first = created.value.beginPass('volume-inject', 'compute', 3);
96
+ const second = created.value.beginPass('volume-integrate', 'raster', 9);
97
+ expect(first).toMatchObject({ beginningOfPassWriteIndex: 0, endOfPassWriteIndex: 1 });
98
+ expect(second).toMatchObject({ beginningOfPassWriteIndex: 2, endOfPassWriteIndex: 3 });
99
+ expect((encoder as unknown as { writeTimestamp?: unknown }).writeTimestamp).toBeUndefined();
100
+
101
+ expect(created.value.resolve(encoder).ok).toBe(true);
102
+ expect(resolveQuerySet).toHaveBeenCalledWith(expect.anything(), 0, 4, expect.anything(), 0);
103
+ created.value.markSubmitted();
104
+ await expect(created.value.observation()).resolves.toEqual({
105
+ status: 'ready',
106
+ unit: 'ms',
107
+ frameMs: 0.00035,
108
+ totalMs: 0.00035,
109
+ passes: [
110
+ { name: 'volume-inject', milliseconds: 0.0001 },
111
+ { name: 'volume-integrate', milliseconds: 0.00024 },
112
+ ],
113
+ });
114
+ });
115
+
116
+ it('fails closed when a producer already owns the pass timestamp pair', async () => {
117
+ const { device, encoder } = timestampDevice(new ArrayBuffer(64 * 8));
118
+ const created = GpuTimingCapture.create(device);
119
+ expect(created.ok).toBe(true);
120
+ if (!created.ok || created.value === undefined) return;
121
+ expect(created.value.beginPass('volume-inject', 'compute', 0)).toBeDefined();
122
+ created.value.markOwnerConflict('volume-inject');
123
+ expect(created.value.resolve(encoder).ok).toBe(true);
124
+ created.value.markSubmitted();
125
+ await expect(created.value.observation()).resolves.toEqual({
126
+ status: 'unavailable',
127
+ reason: "timestamp pass 'volume-inject' already has producer-owned timestampWrites",
128
+ });
129
+ });
130
+
131
+ it('disposes query and buffer resources when readback mapping fails', async () => {
132
+ const { device, encoder, readbackBuffer, destroyBuffer, destroyQuerySet } = timestampDevice(
133
+ new ArrayBuffer(64 * 8),
134
+ );
135
+ const mapAsync = readbackBuffer.mapAsync as unknown as ReturnType<typeof vi.fn>;
136
+ mapAsync.mockResolvedValue({
137
+ ok: false,
138
+ error: { code: 'webgpu-runtime-error' },
139
+ });
140
+ const created = GpuTimingCapture.create(device);
141
+ expect(created.ok).toBe(true);
142
+ if (!created.ok || created.value === undefined) return;
143
+
144
+ expect(created.value.beginPass('volume-inject', 'compute', 0)).toBeDefined();
145
+ expect(created.value.resolve(encoder).ok).toBe(true);
146
+ created.value.markSubmitted();
147
+ await expect(created.value.observation()).resolves.toEqual({
148
+ status: 'unavailable',
149
+ reason: 'timestamp readback failed: webgpu-runtime-error',
150
+ });
151
+ expect(destroyBuffer).toHaveBeenCalledTimes(2);
152
+ expect(destroyQuerySet).toHaveBeenCalledTimes(1);
153
+ });
154
+
155
+ it('disposes query and buffer resources when mapped range access fails', async () => {
156
+ const { device, encoder, getMappedRange, destroyBuffer, destroyQuerySet } = timestampDevice(
157
+ new ArrayBuffer(64 * 8),
158
+ );
159
+ getMappedRange.mockReturnValue({
160
+ ok: false,
161
+ error: { code: 'webgpu-runtime-error' },
162
+ });
163
+ const created = GpuTimingCapture.create(device);
164
+ expect(created.ok).toBe(true);
165
+ if (!created.ok || created.value === undefined) return;
166
+
167
+ expect(created.value.beginPass('volume-inject', 'compute', 0)).toBeDefined();
168
+ expect(created.value.resolve(encoder).ok).toBe(true);
169
+ created.value.markSubmitted();
170
+ await expect(created.value.observation()).resolves.toEqual({
171
+ status: 'unavailable',
172
+ reason: 'timestamp mapping failed: webgpu-runtime-error',
173
+ });
174
+ expect(destroyBuffer).toHaveBeenCalledTimes(2);
175
+ expect(destroyQuerySet).toHaveBeenCalledTimes(1);
176
+ });
177
+ });
@@ -132,7 +132,6 @@ describe('temporal GPU payloads', () => {
132
132
  { point: [], spot: [projector] } as never,
133
133
  [projector],
134
134
  undefined,
135
- undefined,
136
135
  0,
137
136
  );
138
137
  const payload = new Float32Array(uploads[0]?.buffer ?? new ArrayBuffer(0));
@@ -165,6 +164,29 @@ describe('temporal GPU payloads', () => {
165
164
  expect(payload[230]).toBe(0);
166
165
  });
167
166
 
167
+ it('keeps the accepted previous camera in the temporal ABI', () => {
168
+ const { queue, uploads } = recordingQueue();
169
+ const acceptedCamera = [7, 8, 9] as const;
170
+ const temporal = { ...temporalView(), previousCameraPosition: acceptedCamera };
171
+ writeViewUbo(
172
+ queue,
173
+ {} as Buffer,
174
+ camera(),
175
+ {
176
+ kind: 'directional',
177
+ direction: vec3.create(0, -1, 0),
178
+ color: vec3.create(1, 1, 1),
179
+ intensity: 1,
180
+ },
181
+ { point: [], spot: [] } as never,
182
+ [],
183
+ temporal as never,
184
+ );
185
+
186
+ const payload = new Float32Array(uploads[0]?.buffer ?? new ArrayBuffer(0));
187
+ expect(Array.from(payload.slice(232, 236))).toEqual([...acceptedCamera, 1]);
188
+ });
189
+
168
190
  it('seeds previousWorld on the first frame and uses the submitted snapshot later', () => {
169
191
  const first = translatedMatrix(4);
170
192
  const previous = translatedMatrix(2);
@@ -19,6 +19,7 @@ const captureDescriptor = {
19
19
 
20
20
  type CaptureOverrides = {
21
21
  readonly target?: boolean;
22
+ readonly onSubmitted?: () => void;
22
23
  readonly descriptor?: {
23
24
  readonly format?: 'rgba16float' | 'rgba8unorm';
24
25
  readonly size?: { readonly width: number; readonly height: number };
@@ -74,7 +75,14 @@ function executeCapture(overrides: CaptureOverrides = {}) {
74
75
  errorRegistry: { fire: (error: unknown) => errors.push(error) },
75
76
  device: { queue: { submit: () => ok(undefined) } },
76
77
  } as never;
77
- const submitted = executeCompiledFrameGraph(internals, frameState, {} as never, encoder);
78
+ const submitted = executeCompiledFrameGraph(
79
+ internals,
80
+ frameState,
81
+ {} as never,
82
+ encoder,
83
+ undefined,
84
+ overrides.onSubmitted === undefined ? undefined : { onSubmitted: overrides.onSubmitted },
85
+ );
78
86
  return { submitted, errors, copies };
79
87
  }
80
88
 
@@ -142,6 +150,17 @@ describe('typed frame graph feature plan authority', () => {
142
150
  expect(result.copies).toHaveLength(1);
143
151
  });
144
152
 
153
+ it('keeps the submit hook on the successful commit path', () => {
154
+ let calls = 0;
155
+ const result = executeCapture({
156
+ onSubmitted: () => {
157
+ calls += 1;
158
+ },
159
+ });
160
+ expect(result.submitted).toBe(true);
161
+ expect(calls).toBe(1);
162
+ });
163
+
145
164
  it.each([
146
165
  [
147
166
  'missing same-submit copy / untouched sentinel',