@forgeax/engine-render 0.1.27 → 0.1.29

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 (655) hide show
  1. package/README.md +842 -124
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/dynamic-geometry-host.d.ts +52 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +19 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +3 -3
  60. package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  71. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  72. package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  73. package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  74. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  75. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  76. package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  77. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  78. package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
  79. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  80. package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
  81. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  82. package/dist/components/camera.d.ts +62 -2
  83. package/dist/components/camera.d.ts.map +1 -1
  84. package/dist/components/index.d.ts +1 -0
  85. package/dist/components/index.d.ts.map +1 -1
  86. package/dist/components/instances.d.ts +6 -20
  87. package/dist/components/instances.d.ts.map +1 -1
  88. package/dist/components/scene-instance.d.ts +2 -1
  89. package/dist/components/scene-instance.d.ts.map +1 -1
  90. package/dist/components/screen-space-reflection.d.ts +14 -0
  91. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  92. package/dist/construct-renderer.mjs +13805 -7186
  93. package/dist/construct-renderer.mjs.map +1 -1
  94. package/dist/device/gpu-residency.d.ts +11 -17
  95. package/dist/device/gpu-residency.d.ts.map +1 -1
  96. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  97. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  98. package/dist/dynamic-geometry.d.ts +125 -0
  99. package/dist/dynamic-geometry.d.ts.map +1 -0
  100. package/dist/environment/background.d.ts +6 -0
  101. package/dist/environment/background.d.ts.map +1 -0
  102. package/dist/errors/gpu-driven.d.ts +6 -3
  103. package/dist/errors/gpu-driven.d.ts.map +1 -1
  104. package/dist/errors/render.d.ts +7 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/extract/camera.d.ts.map +1 -1
  107. package/dist/extract/gpu-driven.d.ts +33 -3
  108. package/dist/extract/gpu-driven.d.ts.map +1 -1
  109. package/dist/features/host.d.ts +23 -1
  110. package/dist/features/host.d.ts.map +1 -1
  111. package/dist/features/noise-texture.d.ts +17 -0
  112. package/dist/features/noise-texture.d.ts.map +1 -0
  113. package/dist/features/plan.d.ts +83 -2
  114. package/dist/features/plan.d.ts.map +1 -1
  115. package/dist/features/prepared-gpu-work.d.ts +37 -4
  116. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  117. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  118. package/dist/features/prepared-graphics.d.ts +5 -0
  119. package/dist/features/prepared-graphics.d.ts.map +1 -1
  120. package/dist/features/render-graph-compute.d.ts +5 -3
  121. package/dist/features/render-graph-compute.d.ts.map +1 -1
  122. package/dist/features/render-graph-contribution.d.ts +11 -3
  123. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  124. package/dist/features/render-graph-raster.d.ts +19 -3
  125. package/dist/features/render-graph-raster.d.ts.map +1 -1
  126. package/dist/features/types.d.ts +11 -0
  127. package/dist/features/types.d.ts.map +1 -1
  128. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  129. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  130. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  131. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  132. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  133. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  134. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +144 -23
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  138. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  139. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  140. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  141. package/dist/gpu-scene-schema.d.ts +2 -8
  142. package/dist/gpu-scene-schema.d.ts.map +1 -1
  143. package/dist/gpu-scene.d.ts +14 -0
  144. package/dist/gpu-scene.d.ts.map +1 -1
  145. package/dist/gpu-texture-usage.d.ts +1 -0
  146. package/dist/gpu-texture-usage.d.ts.map +1 -1
  147. package/dist/hdrp-buffers.d.ts +6 -0
  148. package/dist/hdrp-buffers.d.ts.map +1 -1
  149. package/dist/ibl/frame-resources.d.ts +8 -0
  150. package/dist/ibl/frame-resources.d.ts.map +1 -0
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +21 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +8 -7
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +164 -0
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instances-derived-bounds.d.ts +11 -0
  159. package/dist/instances-derived-bounds.d.ts.map +1 -1
  160. package/dist/instances.d.ts +89 -44
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.d.ts +3 -0
  163. package/dist/internal.d.ts.map +1 -1
  164. package/dist/internal.mjs +118 -10
  165. package/dist/internal.mjs.map +1 -1
  166. package/dist/lifecycle.d.ts +2 -1
  167. package/dist/lifecycle.d.ts.map +1 -1
  168. package/dist/material-row.d.ts +5 -0
  169. package/dist/material-row.d.ts.map +1 -0
  170. package/dist/materials.d.ts +15 -0
  171. package/dist/materials.d.ts.map +1 -1
  172. package/dist/pbr-pipeline.d.ts +21 -5
  173. package/dist/pbr-pipeline.d.ts.map +1 -1
  174. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  175. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  176. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  177. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  178. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  179. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  180. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  181. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  182. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  183. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  184. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  185. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  186. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  187. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  188. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  189. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  190. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  191. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  192. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  193. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  194. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  195. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  196. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  197. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  198. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  199. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  200. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  201. package/dist/pipeline/standard-output/types.d.ts +39 -0
  202. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  203. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/standard-post.d.ts +2 -2
  205. package/dist/pipeline/standard-post.d.ts.map +1 -1
  206. package/dist/pipeline-builder.d.ts +2 -1
  207. package/dist/pipeline-builder.d.ts.map +1 -1
  208. package/dist/pipeline-spec-types.d.ts +4 -0
  209. package/dist/pipeline-spec-types.d.ts.map +1 -1
  210. package/dist/pipeline-spec.d.ts +1 -9
  211. package/dist/pipeline-spec.d.ts.map +1 -1
  212. package/dist/points-lines/standard-owner.d.ts +1 -1
  213. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  214. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  215. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  216. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  218. package/dist/record/frame-lighting.d.ts +3 -3
  219. package/dist/record/frame-lighting.d.ts.map +1 -1
  220. package/dist/record/frame-snapshot.d.ts +106 -5
  221. package/dist/record/frame-snapshot.d.ts.map +1 -1
  222. package/dist/record/frame-targets.d.ts.map +1 -1
  223. package/dist/record/frame.d.ts +39 -4
  224. package/dist/record/frame.d.ts.map +1 -1
  225. package/dist/record/main-pass-geometry.d.ts +7 -8
  226. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  227. package/dist/record/main-pass-material.d.ts +38 -1
  228. package/dist/record/main-pass-material.d.ts.map +1 -1
  229. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  230. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  231. package/dist/record/main-pass.d.ts +12 -2
  232. package/dist/record/main-pass.d.ts.map +1 -1
  233. package/dist/record/mesh-ssbo.d.ts +15 -6
  234. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  235. package/dist/record/prepared-material-bindings.d.ts +19 -0
  236. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  237. package/dist/record/recovery-pipeline.d.ts +2 -9
  238. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  239. package/dist/record/render-context.d.ts +76 -26
  240. package/dist/record/render-context.d.ts.map +1 -1
  241. package/dist/record/shadow-pass.d.ts +62 -7
  242. package/dist/record/shadow-pass.d.ts.map +1 -1
  243. package/dist/record/skybox-post-pass.d.ts +1 -1
  244. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  245. package/dist/record/typed-frame-graph.d.ts +35 -7
  246. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  247. package/dist/record/view-ubo.d.ts +1 -0
  248. package/dist/record/view-ubo.d.ts.map +1 -1
  249. package/dist/recovery/render-system-candidate.d.ts +10 -2
  250. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  251. package/dist/recovery/render-system-roots.d.ts +11 -0
  252. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  253. package/dist/recovery/types.d.ts +76 -0
  254. package/dist/recovery/types.d.ts.map +1 -0
  255. package/dist/reflection/record-owner.d.ts +0 -2
  256. package/dist/reflection/record-owner.d.ts.map +1 -1
  257. package/dist/render-contract.d.ts +185 -30
  258. package/dist/render-contract.d.ts.map +1 -1
  259. package/dist/render-graph-primitives.d.ts +1 -0
  260. package/dist/render-graph-primitives.d.ts.map +1 -1
  261. package/dist/render-pipeline.d.ts +64 -5
  262. package/dist/render-pipeline.d.ts.map +1 -1
  263. package/dist/render-system-extract-tail.d.ts +7 -5
  264. package/dist/render-system-extract-tail.d.ts.map +1 -1
  265. package/dist/render-system-extract.d.ts +101 -41
  266. package/dist/render-system-extract.d.ts.map +1 -1
  267. package/dist/render-system-projections.d.ts +22 -0
  268. package/dist/render-system-projections.d.ts.map +1 -0
  269. package/dist/render-system.d.ts +34 -77
  270. package/dist/render-system.d.ts.map +1 -1
  271. package/dist/scene/render-scene-types.d.ts +14 -7
  272. package/dist/scene/render-scene-types.d.ts.map +1 -1
  273. package/dist/scene/render-scene.d.ts +29 -11
  274. package/dist/scene/render-scene.d.ts.map +1 -1
  275. package/dist/scene/shadow-visibility.d.ts +5 -0
  276. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  277. package/dist/ssr/admission.d.ts +70 -1
  278. package/dist/ssr/admission.d.ts.map +1 -1
  279. package/dist/ssr/compose.d.ts +13 -0
  280. package/dist/ssr/compose.d.ts.map +1 -0
  281. package/dist/ssr/errors.d.ts +35 -0
  282. package/dist/ssr/errors.d.ts.map +1 -1
  283. package/dist/ssr/graph.d.ts +60 -0
  284. package/dist/ssr/graph.d.ts.map +1 -0
  285. package/dist/ssr/history.d.ts +97 -0
  286. package/dist/ssr/history.d.ts.map +1 -0
  287. package/dist/ssr/hiz.d.ts +37 -0
  288. package/dist/ssr/hiz.d.ts.map +1 -0
  289. package/dist/ssr/inspection.d.ts +42 -0
  290. package/dist/ssr/inspection.d.ts.map +1 -0
  291. package/dist/ssr/resources.d.ts +58 -0
  292. package/dist/ssr/resources.d.ts.map +1 -0
  293. package/dist/ssr/temporal.d.ts +38 -0
  294. package/dist/ssr/temporal.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  296. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  297. package/dist/systems/skin-palette-types.d.ts +15 -0
  298. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  299. package/dist/systems/transparent-dispatch.d.ts +9 -0
  300. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  301. package/dist/targets/material-source.d.ts +7 -0
  302. package/dist/targets/material-source.d.ts.map +1 -1
  303. package/dist/temporal/frame-coordinator.d.ts +7 -0
  304. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  305. package/dist/temporal/gpu.d.ts +8 -0
  306. package/dist/temporal/gpu.d.ts.map +1 -1
  307. package/dist/temporal/history.d.ts.map +1 -1
  308. package/dist/temporal/index.mjs +6 -6
  309. package/dist/temporal/inspection.d.ts.map +1 -1
  310. package/dist/temporal/standard-scene-data.d.ts +2 -1
  311. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  312. package/dist/temporal/temporal-view.d.ts +2 -0
  313. package/dist/temporal/temporal-view.d.ts.map +1 -1
  314. package/dist/typed-render-graph-primitives.d.ts +35 -1
  315. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  316. package/dist/typed-shadow-passes.d.ts +3 -1
  317. package/dist/typed-shadow-passes.d.ts.map +1 -1
  318. package/dist/volume/temporal.d.ts +6 -0
  319. package/dist/volume/temporal.d.ts.map +1 -1
  320. package/package.json +21 -20
  321. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  322. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  323. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  324. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  325. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  326. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  327. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  328. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  329. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  330. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  331. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  332. package/src/__tests__/factory-contract.integration.test.ts +73 -22
  333. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  334. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  335. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  336. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  337. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  338. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  339. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  340. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  341. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  342. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  343. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  344. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  345. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  346. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  348. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  349. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  350. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  351. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  352. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  353. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  354. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  355. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  356. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  357. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  358. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  359. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  360. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  361. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  362. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  363. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  364. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  365. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  366. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  367. package/src/__tests__/instances-store.unit.test.ts +242 -39
  368. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  369. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  370. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  371. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  372. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  373. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  374. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  375. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  376. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  377. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  378. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  379. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  380. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  381. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  382. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  383. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  384. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  385. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  386. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  387. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  388. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  389. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  390. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  391. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  392. package/src/__tests__/public-surface.test-d.ts +3 -0
  393. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  394. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  395. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  396. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  397. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  398. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  399. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  400. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  401. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  402. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  403. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  404. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  405. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  406. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  407. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  408. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  409. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  410. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  411. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
  412. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  413. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  414. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  415. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  416. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  417. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  418. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  419. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  420. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  421. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  422. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  423. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  424. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  425. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  426. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  427. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  428. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  429. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  430. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  431. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  432. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  433. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  434. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  435. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  436. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  437. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  438. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  439. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  440. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  441. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  442. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  443. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  444. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  445. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  446. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  447. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  448. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  449. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  450. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  451. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  452. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  453. package/src/__tests__/standard-output.browser.test.ts +15 -0
  454. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  455. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  456. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  457. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  458. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  459. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  460. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  461. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  462. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  463. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  464. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  465. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  466. package/src/__tests__/taa-stability-feedback.ts +341 -0
  467. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  468. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  469. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  470. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  472. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  473. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  474. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  475. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  476. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  477. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  478. package/src/assembly/dynamic-geometry-host.ts +887 -0
  479. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  480. package/src/assembly/factory.ts +4 -0
  481. package/src/assembly/gpu-driven-owner.ts +29 -0
  482. package/src/assembly/host-contract.ts +36 -0
  483. package/src/assembly/material/assembly.ts +38 -0
  484. package/src/assembly/material-shader-policy.ts +71 -28
  485. package/src/assembly/recovery/failure-location.ts +62 -0
  486. package/src/assembly/recovery/renderer-recover.ts +19 -17
  487. package/src/assembly/renderer-facade.ts +29 -0
  488. package/src/assembly/renderer-feature-inspection.ts +13 -0
  489. package/src/assembly/renderer-frame-transaction.ts +63 -0
  490. package/src/assembly/renderer-helpers.ts +26 -0
  491. package/src/assembly/renderer-inspection.ts +6 -0
  492. package/src/assembly/shader-prewarm-policy.ts +32 -0
  493. package/src/assembly/skin-palette-owner.ts +21 -0
  494. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  495. package/src/assembly/webgpu-device-loss.ts +75 -0
  496. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  497. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  498. package/src/assembly/webgpu-ready.ts +142 -121
  499. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  500. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  501. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  502. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  503. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  504. package/src/assembly/webgpu-renderer.ts +675 -614
  505. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  506. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  507. package/src/components/camera.ts +287 -7
  508. package/src/components/index.ts +1 -0
  509. package/src/components/instances.ts +45 -18
  510. package/src/components/scene-instance.ts +2 -1
  511. package/src/components/screen-space-reflection.ts +21 -0
  512. package/src/components/sprite-animation.ts +3 -3
  513. package/src/components/sprite-instances.ts +1 -1
  514. package/src/device/gpu-residency.ts +96 -93
  515. package/src/device/mesh-residency-lifetime.ts +73 -0
  516. package/src/dynamic-geometry.ts +736 -0
  517. package/src/environment/background.ts +263 -0
  518. package/src/errors/gpu-driven.ts +39 -5
  519. package/src/errors/render.ts +27 -3
  520. package/src/extract/camera.ts +26 -1
  521. package/src/extract/gpu-driven.ts +185 -10
  522. package/src/features/host.ts +237 -19
  523. package/src/features/noise-texture.ts +91 -0
  524. package/src/features/plan.ts +553 -24
  525. package/src/features/prepared-gpu-work.ts +631 -49
  526. package/src/features/prepared-graphics-store.ts +31 -0
  527. package/src/features/prepared-graphics.ts +5 -0
  528. package/src/features/render-graph-compute.ts +41 -8
  529. package/src/features/render-graph-contribution.ts +141 -3
  530. package/src/features/render-graph-raster.ts +143 -30
  531. package/src/features/types.ts +11 -0
  532. package/src/fullscreen-post-process-pass.ts +3 -0
  533. package/src/gpu-driven/batch-topology.ts +424 -102
  534. package/src/gpu-driven/pbr-program.ts +31 -0
  535. package/src/gpu-driven/prepared-draw.ts +29 -4
  536. package/src/gpu-driven/production-raster.ts +2267 -694
  537. package/src/gpu-driven/shadow-views.ts +539 -0
  538. package/src/gpu-driven/view-gpu.ts +188 -41
  539. package/src/gpu-scene-schema.ts +52 -11
  540. package/src/gpu-scene.ts +186 -99
  541. package/src/gpu-texture-usage.ts +1 -0
  542. package/src/hdrp-buffers.ts +72 -36
  543. package/src/ibl/frame-resources.ts +38 -0
  544. package/src/ibl/skylight-bind-group.ts +5 -4
  545. package/src/index.ts +125 -1
  546. package/src/inspection-types.ts +191 -0
  547. package/src/instances-derived-bounds.ts +258 -34
  548. package/src/instances.ts +459 -150
  549. package/src/internal.ts +22 -0
  550. package/src/lifecycle.ts +2 -0
  551. package/src/material-row.ts +145 -0
  552. package/src/materials.ts +44 -8
  553. package/src/pbr-pipeline.ts +159 -17
  554. package/src/pipeline/standard-forward-lane.ts +52 -24
  555. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  556. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  557. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  558. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  559. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  560. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  561. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  562. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  563. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  564. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  565. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  566. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  567. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  568. package/src/pipeline/standard-output/color-transform.ts +124 -0
  569. package/src/pipeline/standard-output/graph.ts +139 -0
  570. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  571. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  572. package/src/pipeline/standard-output/lut-state.ts +176 -0
  573. package/src/pipeline/standard-output/resources.ts +121 -0
  574. package/src/pipeline/standard-output/types.ts +58 -0
  575. package/src/pipeline/standard-pipeline.ts +192 -59
  576. package/src/pipeline/standard-post.ts +186 -27
  577. package/src/pipeline-builder.ts +16 -6
  578. package/src/pipeline-spec-types.ts +4 -0
  579. package/src/pipeline-spec.ts +37 -29
  580. package/src/points-lines/standard-owner.ts +6 -14
  581. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  582. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  583. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  584. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  585. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  586. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  587. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  588. package/src/record/dynamic-geometry-consumption.ts +59 -0
  589. package/src/record/frame-lighting.ts +12 -11
  590. package/src/record/frame-snapshot.ts +133 -23
  591. package/src/record/frame-targets.ts +9 -0
  592. package/src/record/frame.ts +727 -133
  593. package/src/record/main-pass-geometry.ts +162 -98
  594. package/src/record/main-pass-material.ts +115 -26
  595. package/src/record/main-pass-sprite-draws.ts +18 -107
  596. package/src/record/main-pass.ts +78 -8
  597. package/src/record/material-uniforms.ts +1 -1
  598. package/src/record/mesh-ssbo.ts +59 -15
  599. package/src/record/prepared-material-bindings.ts +234 -0
  600. package/src/record/recovery-pipeline.ts +59 -136
  601. package/src/record/render-context.ts +92 -22
  602. package/src/record/shadow-pass.ts +1231 -209
  603. package/src/record/skybox-post-pass.ts +14 -3
  604. package/src/record/typed-frame-graph.ts +1006 -53
  605. package/src/record/view-ubo.ts +11 -0
  606. package/src/recovery/render-system-candidate.ts +90 -66
  607. package/src/recovery/render-system-roots.ts +113 -0
  608. package/src/recovery/types.ts +77 -0
  609. package/src/reflection/record-owner.ts +26 -54
  610. package/src/render-contract.ts +219 -25
  611. package/src/render-graph-primitives.ts +6 -2
  612. package/src/render-pipeline.ts +70 -1
  613. package/src/render-system-extract-tail.ts +409 -160
  614. package/src/render-system-extract.ts +376 -102
  615. package/src/render-system-projections.ts +241 -0
  616. package/src/render-system.ts +692 -791
  617. package/src/scene/render-scene-types.ts +13 -4
  618. package/src/scene/render-scene.ts +569 -355
  619. package/src/scene/shadow-visibility.ts +21 -0
  620. package/src/ssr/admission.ts +380 -14
  621. package/src/ssr/compose.ts +144 -0
  622. package/src/ssr/errors.ts +48 -0
  623. package/src/ssr/graph.ts +777 -0
  624. package/src/ssr/history.ts +436 -0
  625. package/src/ssr/hiz.ts +124 -0
  626. package/src/ssr/inspection.ts +124 -0
  627. package/src/ssr/resources.ts +202 -0
  628. package/src/ssr/temporal.ts +164 -0
  629. package/src/systems/skin-palette-allocator.ts +495 -148
  630. package/src/systems/skin-palette-types.ts +17 -0
  631. package/src/systems/transparent-dispatch.ts +90 -0
  632. package/src/targets/material-source.ts +34 -0
  633. package/src/temporal/coverage.ts +1 -1
  634. package/src/temporal/frame-coordinator.ts +23 -2
  635. package/src/temporal/gpu.ts +74 -12
  636. package/src/temporal/history.ts +3 -1
  637. package/src/temporal/inspection.ts +2 -1
  638. package/src/temporal/standard-scene-data.ts +3 -1
  639. package/src/temporal/temporal-view.ts +3 -0
  640. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  641. package/src/typed-render-graph-primitives.ts +266 -74
  642. package/src/typed-shadow-passes.ts +122 -12
  643. package/src/volume/temporal.ts +10 -0
  644. package/dist/chunk-ADHHVYLW.mjs.map +0 -1
  645. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  646. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  647. package/dist/chunk-GF523LHF.mjs.map +0 -1
  648. package/dist/chunk-HKXTW355.mjs.map +0 -1
  649. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  650. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  651. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  652. package/dist/chunk-XDP6XE5V.mjs.map +0 -1
  653. package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
  654. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  655. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -0,0 +1,1123 @@
1
+ import { FixedUpdate, World } from '@forgeax/engine-ecs';
2
+ import { rhi } from '@forgeax/engine-rhi-null';
3
+ import { Transform } from '@forgeax/engine-scene';
4
+ import type { Handle, MeshAsset } from '@forgeax/engine-types';
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import { createDynamicGeometryHost } from '../assembly/dynamic-geometry-host';
7
+ import { MeshFilter } from '../components/mesh-filter';
8
+ import { MeshRenderer } from '../components/mesh-renderer';
9
+ import { GpuResidencyCache } from '../device/gpu-residency';
10
+ import { MeshResidencyLifetime } from '../device/mesh-residency-lifetime';
11
+
12
+ function residencyLease(handle: number, invalidated: number[]) {
13
+ const lifetime = new MeshResidencyLifetime(() => invalidated.push(handle));
14
+ return lifetime.retain(() => lifetime.retire());
15
+ }
16
+
17
+ import { createDynamicGeometryLifecycle } from '../dynamic-geometry';
18
+
19
+ const mesh = (): MeshAsset => ({
20
+ kind: 'mesh',
21
+ vertices: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]),
22
+ indices: new Uint16Array([0, 1, 2]),
23
+ attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
24
+ aabb: new Float32Array([0, 0, 0, 1, 1, 0]),
25
+ submeshes: [
26
+ { indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
27
+ ],
28
+ materialSlots: [{ slotName: 'default', sourceKey: 'default' }],
29
+ });
30
+
31
+ describe('dynamic geometry ECS publication host', () => {
32
+ it.each([
33
+ 'supersession',
34
+ 'cancel',
35
+ 'duplicate-cancel',
36
+ 'detach',
37
+ ] as const)('keeps unpublished submitted allocations bounded through %s', async (mode) => {
38
+ const world = new World();
39
+ const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
40
+ const store = new GpuResidencyCache();
41
+ store.configureGpuDevice(
42
+ device,
43
+ undefined,
44
+ () => {
45
+ throw new Error('unused cubemap');
46
+ },
47
+ device.caps,
48
+ );
49
+ const lifecycle = createDynamicGeometryLifecycle();
50
+ const host = createDynamicGeometryHost({
51
+ lifecycle,
52
+ attachedWorlds: new Set([world]),
53
+ getGpuStore: () => store,
54
+ currentGeneration: () => 1,
55
+ isConsumedByRenderFrame: () => false,
56
+ });
57
+ const original = world.allocSharedRef('MeshAsset', mesh());
58
+ const entity = world
59
+ .spawn(
60
+ { component: Transform, data: {} },
61
+ { component: MeshFilter, data: { assetHandle: original } },
62
+ { component: MeshRenderer, data: { materials: [] } },
63
+ )
64
+ .unwrap();
65
+ let finish!: () => void;
66
+ const pending = new Promise<void>((resolve) => {
67
+ finish = resolve;
68
+ });
69
+ const handles: Handle<'MeshAsset', 'shared'>[] = [];
70
+ let latest: import('../dynamic-geometry').DynamicGeometryCandidate | undefined;
71
+ for (let revision = 1; revision <= 40; revision++) {
72
+ const source = mesh();
73
+ const handle = world.allocSharedRef('MeshAsset', source);
74
+ const prepared = host.prepareDynamicGeometry({
75
+ world,
76
+ entity,
77
+ mesh: source,
78
+ meshHandle: handle,
79
+ revision,
80
+ });
81
+ if (!prepared.ok) {
82
+ expect(revision).toBe(33);
83
+ expect(prepared.error.code).toBe('dynamic-geometry-budget-exceeded');
84
+ world.sharedRefs.release(handle).unwrap();
85
+ break;
86
+ }
87
+ latest = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 }).unwrap();
88
+ handles.push(handle);
89
+ store.trackMeshSubmission(pending);
90
+ world.sharedRefs.release(handle).unwrap();
91
+ if (mode === 'cancel' || mode === 'duplicate-cancel') {
92
+ host.cancelDynamicGeometry(latest).unwrap();
93
+ if (mode === 'duplicate-cancel') expect(host.cancelDynamicGeometry(latest).ok).toBe(false);
94
+ } else if (mode === 'detach') {
95
+ host.invalidateDynamicGeometryWorld(world);
96
+ host.invalidateDynamicGeometryWorld(world);
97
+ world.set(entity, MeshFilter, { assetHandle: original }).unwrap();
98
+ }
99
+ }
100
+ expect(handles).toHaveLength(32);
101
+ expect(lifecycle.inspect().meshBytes).toBeGreaterThan(0);
102
+ expect(handles.every((handle) => world.sharedRefs.resolve(handle).ok)).toBe(true);
103
+ if (mode === 'supersession' && latest !== undefined)
104
+ host.cancelDynamicGeometry(latest).unwrap();
105
+ finish();
106
+ await pending;
107
+ await vi.waitFor(() => expect(lifecycle.inspect().meshBytes).toBe(0));
108
+ store.destroyAll();
109
+ });
110
+
111
+ it('keeps the old ECS binding visible while staging and restores it on cancellation', () => {
112
+ const world = new World();
113
+ const oldMesh = mesh();
114
+ const oldHandle = world.allocSharedRef('MeshAsset', oldMesh);
115
+ const nextMesh = mesh();
116
+ nextMesh.vertices[0] = 4;
117
+ const nextHandle = world.allocSharedRef('MeshAsset', nextMesh);
118
+ const entity = world
119
+ .spawn(
120
+ { component: Transform, data: {} },
121
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
122
+ { component: MeshRenderer, data: { materials: [] } },
123
+ )
124
+ .unwrap();
125
+ // Simulate an asset producer releasing its allocation while the ECS
126
+ // component remains the only visible binding. Candidate preparation must
127
+ // retain that old binding before the ECS swap so cancellation can restore
128
+ // a live handle instead of reporting success with a stale MeshFilter.
129
+ expect(world.sharedRefs.release(oldHandle).ok).toBe(true);
130
+ const invalidated: number[] = [];
131
+ const host = createDynamicGeometryHost({
132
+ lifecycle: createDynamicGeometryLifecycle(),
133
+ attachedWorlds: new Set([world]),
134
+ getGpuStore: () =>
135
+ ({
136
+ ensureResident: () => ({ ok: true as const, value: {} }),
137
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
138
+ invalidateMesh: (handle: number) => invalidated.push(handle),
139
+ }) as unknown as GpuResidencyCache,
140
+ currentGeneration: () => 1,
141
+ isConsumedByRenderFrame: () => true,
142
+ });
143
+
144
+ const prepared = host.prepareDynamicGeometry({
145
+ world,
146
+ entity,
147
+ mesh: nextMesh,
148
+ meshHandle: nextHandle,
149
+ revision: 2,
150
+ });
151
+ expect(prepared.ok).toBe(true);
152
+ expect(world.sharedRefs.refcount(nextHandle)).toBe(2);
153
+ expect(world.sharedRefs.resolve(oldHandle).ok).toBe(true);
154
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(oldHandle);
155
+ if (!prepared.ok) return;
156
+
157
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
158
+ expect(accepted.ok).toBe(true);
159
+ expect(world.sharedRefs.refcount(nextHandle)).toBe(3);
160
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(nextHandle);
161
+ if (!accepted.ok) return;
162
+
163
+ expect(host.cancelDynamicGeometry(accepted.value).ok).toBe(true);
164
+ expect(world.sharedRefs.refcount(nextHandle)).toBe(1);
165
+ expect(world.sharedRefs.resolve(oldHandle).ok).toBe(true);
166
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(oldHandle);
167
+ expect(invalidated).toEqual([nextHandle]);
168
+ });
169
+
170
+ it('refuses an external binding change instead of clobbering the live ECS state', () => {
171
+ const world = new World();
172
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
173
+ const nextMesh = mesh();
174
+ const nextHandle = world.allocSharedRef('MeshAsset', nextMesh);
175
+ const externalHandle = world.allocSharedRef('MeshAsset', mesh());
176
+ const entity = world
177
+ .spawn(
178
+ { component: Transform, data: {} },
179
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
180
+ { component: MeshRenderer, data: { materials: [] } },
181
+ )
182
+ .unwrap();
183
+ const invalidated: number[] = [];
184
+ const host = createDynamicGeometryHost({
185
+ lifecycle: createDynamicGeometryLifecycle(),
186
+ attachedWorlds: new Set([world]),
187
+ getGpuStore: () =>
188
+ ({
189
+ ensureResident: () => ({ ok: true as const, value: {} }),
190
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
191
+ invalidateMesh: (handle: number) => invalidated.push(handle),
192
+ }) as unknown as GpuResidencyCache,
193
+ currentGeneration: () => 1,
194
+ isConsumedByRenderFrame: () => true,
195
+ });
196
+ const prepared = host.prepareDynamicGeometry({
197
+ world,
198
+ entity,
199
+ mesh: nextMesh,
200
+ meshHandle: nextHandle,
201
+ revision: 2,
202
+ });
203
+ expect(prepared.ok).toBe(true);
204
+ if (!prepared.ok) return;
205
+ world.set(entity, MeshFilter, { assetHandle: externalHandle }).unwrap();
206
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
207
+ expect(accepted).toMatchObject({ ok: false, error: { code: 'dynamic-geometry-invalid' } });
208
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(externalHandle);
209
+ expect(host.cancelDynamicGeometry(prepared.value).ok).toBe(true);
210
+ expect(invalidated).toEqual([nextHandle]);
211
+ });
212
+
213
+ it('refuses cancellation after an accepted candidate is replaced by a foreign binding', () => {
214
+ const world = new World();
215
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
216
+ const nextMesh = mesh();
217
+ nextMesh.vertices[0] = 9;
218
+ const nextHandle = world.allocSharedRef('MeshAsset', nextMesh);
219
+ const foreignHandle = world.allocSharedRef('MeshAsset', mesh());
220
+ const entity = world
221
+ .spawn(
222
+ { component: Transform, data: {} },
223
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
224
+ { component: MeshRenderer, data: { materials: [] } },
225
+ )
226
+ .unwrap();
227
+ const invalidated: number[] = [];
228
+ const host = createDynamicGeometryHost({
229
+ lifecycle: createDynamicGeometryLifecycle(),
230
+ attachedWorlds: new Set([world]),
231
+ getGpuStore: () =>
232
+ ({
233
+ ensureResident: () => ({ ok: true as const, value: {} }),
234
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
235
+ invalidateMesh: (handle: number) => invalidated.push(handle),
236
+ }) as unknown as GpuResidencyCache,
237
+ currentGeneration: () => 1,
238
+ isConsumedByRenderFrame: () => true,
239
+ });
240
+ const prepared = host.prepareDynamicGeometry({
241
+ world,
242
+ entity,
243
+ mesh: nextMesh,
244
+ meshHandle: nextHandle,
245
+ revision: 2,
246
+ });
247
+ expect(prepared.ok).toBe(true);
248
+ if (!prepared.ok) return;
249
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
250
+ expect(accepted.ok).toBe(true);
251
+ if (!accepted.ok) return;
252
+
253
+ expect(world.set(entity, MeshFilter, { assetHandle: foreignHandle }).ok).toBe(true);
254
+ expect(host.cancelDynamicGeometry(accepted.value)).toMatchObject({
255
+ ok: false,
256
+ error: { code: 'dynamic-geometry-invalid' },
257
+ });
258
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(foreignHandle);
259
+ // The old binding remains retained while cancellation is refused, so the
260
+ // host can restore it after the foreign owner reconciles the entity.
261
+ expect(world.sharedRefs.resolve(oldHandle).ok).toBe(true);
262
+ expect(invalidated).toEqual([]);
263
+
264
+ expect(world.set(entity, MeshFilter, { assetHandle: nextHandle }).ok).toBe(true);
265
+ expect(host.cancelDynamicGeometry(accepted.value).ok).toBe(true);
266
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(oldHandle);
267
+ expect(invalidated).toEqual([nextHandle]);
268
+ });
269
+
270
+ it('rejects forged owner credentials before cancellation or retirement can touch ECS', () => {
271
+ const world = new World();
272
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
273
+ const nextMesh = mesh();
274
+ nextMesh.vertices[0] = 8;
275
+ const nextHandle = world.allocSharedRef('MeshAsset', nextMesh);
276
+ const entity = world
277
+ .spawn(
278
+ { component: Transform, data: {} },
279
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
280
+ { component: MeshRenderer, data: { materials: [] } },
281
+ )
282
+ .unwrap();
283
+ const invalidated: number[] = [];
284
+ const lifecycle = createDynamicGeometryLifecycle();
285
+ const host = createDynamicGeometryHost({
286
+ lifecycle,
287
+ attachedWorlds: new Set([world]),
288
+ getGpuStore: () =>
289
+ ({
290
+ ensureResident: () => ({ ok: true as const, value: {} }),
291
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
292
+ invalidateMesh: (handle: number) => invalidated.push(handle),
293
+ }) as unknown as GpuResidencyCache,
294
+ currentGeneration: () => 1,
295
+ isConsumedByRenderFrame: () => true,
296
+ });
297
+ const prepared = host.prepareDynamicGeometry({
298
+ world,
299
+ entity,
300
+ mesh: nextMesh,
301
+ meshHandle: nextHandle,
302
+ revision: 1,
303
+ });
304
+ expect(prepared.ok).toBe(true);
305
+ if (!prepared.ok) return;
306
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
307
+ expect(accepted.ok).toBe(true);
308
+ if (!accepted.ok) return;
309
+ const forged = Object.freeze({ ...accepted.value, owner: {} });
310
+
311
+ expect(host.cancelDynamicGeometry(forged)).toMatchObject({
312
+ ok: false,
313
+ error: { code: 'dynamic-geometry-receipt-mismatch' },
314
+ });
315
+ expect(host.retireDynamicGeometry(forged)).toMatchObject({
316
+ ok: false,
317
+ error: { code: 'dynamic-geometry-receipt-mismatch' },
318
+ });
319
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(nextHandle);
320
+ expect(host.dynamicGeometryReceipt(accepted.value)).toBeUndefined();
321
+ expect(lifecycle.inspect()).toMatchObject({ accepted: 1, published: 0 });
322
+ expect(host.cancelDynamicGeometry(accepted.value).ok).toBe(true);
323
+ expect(invalidated).toEqual([nextHandle]);
324
+ });
325
+
326
+ it('derives an omitted publication tick from the attached World FixedTime', () => {
327
+ const invalidated: number[] = [];
328
+ const world = new World();
329
+ expect(
330
+ world.addSystem(FixedUpdate, { name: 'advance-fixed-time', queries: [], fn: () => {} }).ok,
331
+ ).toBe(true);
332
+ expect(world.update(1 / 60).ok).toBe(true);
333
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
334
+ const nextMesh = mesh();
335
+ nextMesh.vertices[0] = 7;
336
+ const nextHandle = world.allocSharedRef('MeshAsset', nextMesh);
337
+ const entity = world
338
+ .spawn(
339
+ { component: Transform, data: {} },
340
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
341
+ { component: MeshRenderer, data: { materials: [] } },
342
+ )
343
+ .unwrap();
344
+ const host = createDynamicGeometryHost({
345
+ lifecycle: createDynamicGeometryLifecycle(),
346
+ attachedWorlds: new Set([world]),
347
+ getGpuStore: () =>
348
+ ({
349
+ ensureResident: () => ({ ok: true as const, value: {} }),
350
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
351
+ }) as unknown as GpuResidencyCache,
352
+ currentGeneration: () => 1,
353
+ isConsumedByRenderFrame: () => true,
354
+ });
355
+ const prepared = host.prepareDynamicGeometry({
356
+ world,
357
+ entity,
358
+ mesh: nextMesh,
359
+ meshHandle: nextHandle,
360
+ revision: 1,
361
+ });
362
+ expect(prepared.ok).toBe(true);
363
+ if (!prepared.ok) return;
364
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 1 });
365
+ expect(accepted.ok).toBe(true);
366
+ if (!accepted.ok) return;
367
+
368
+ const receipts = host.publishDynamicGeometry({ frameId: 1, deviceGeneration: 1 }, [world]);
369
+ expect(receipts).toHaveLength(1);
370
+ expect(receipts[0]).toMatchObject({ fixedStep: 1, publicationFixedStep: 1 });
371
+ });
372
+
373
+ it('keeps real RhiNull residency behind a pending receipt across topology replacement', async () => {
374
+ const adapter = (await rhi.requestAdapter()).unwrap();
375
+ const device = (await adapter.requestDevice()).unwrap();
376
+ const store = new GpuResidencyCache();
377
+ store.configureGpuDevice(
378
+ device,
379
+ undefined,
380
+ () => {
381
+ throw new Error('cubemap registration is not part of this test');
382
+ },
383
+ device.caps,
384
+ );
385
+ const invalidated: number[] = [];
386
+ const invalidateMesh = store.invalidateMesh.bind(store);
387
+ vi.spyOn(store, 'invalidateMesh').mockImplementation((handle, worldId = 0) => {
388
+ invalidated.push(handle);
389
+ invalidateMesh(handle, worldId);
390
+ });
391
+ const world = new World();
392
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
393
+ const firstMesh = mesh();
394
+ firstMesh.vertices[0] = 2;
395
+ const firstHandle = world.allocSharedRef('MeshAsset', firstMesh);
396
+ const entity = world
397
+ .spawn(
398
+ { component: Transform, data: {} },
399
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
400
+ { component: MeshRenderer, data: { materials: [] } },
401
+ )
402
+ .unwrap();
403
+ const host = createDynamicGeometryHost({
404
+ lifecycle: createDynamicGeometryLifecycle(),
405
+ attachedWorlds: new Set([world]),
406
+ getGpuStore: () => store,
407
+ currentGeneration: () => 1,
408
+ isConsumedByRenderFrame: () => true,
409
+ });
410
+ const first = host.prepareDynamicGeometry({
411
+ world,
412
+ entity,
413
+ mesh: firstMesh,
414
+ meshHandle: firstHandle,
415
+ revision: 1,
416
+ topologyRevision: 1,
417
+ });
418
+ expect(first.ok).toBe(true);
419
+ if (!first.ok) return;
420
+ const firstAccepted = host.acceptDynamicGeometry(first.value, { world, fixedStep: 0 });
421
+ expect(firstAccepted.ok).toBe(true);
422
+ if (!firstAccepted.ok) return;
423
+ let completeFirst!: () => void;
424
+ const firstCompleted = new Promise<void>((resolve) => {
425
+ completeFirst = resolve;
426
+ });
427
+ expect(
428
+ host.publishDynamicGeometry(
429
+ { frameId: 1, deviceGeneration: 1, completed: firstCompleted },
430
+ [world],
431
+ 0,
432
+ ),
433
+ ).toHaveLength(1);
434
+ expect(store.getMeshGpuHandles(firstHandle, world)).toBeDefined();
435
+ let completeSecond!: () => void;
436
+ const secondCompleted = new Promise<void>((resolve) => {
437
+ completeSecond = resolve;
438
+ });
439
+ expect(
440
+ host.publishDynamicGeometry(
441
+ { frameId: 2, deviceGeneration: 1, completed: secondCompleted },
442
+ [world],
443
+ 0,
444
+ ),
445
+ ).toHaveLength(1);
446
+ expect(host.dynamicGeometryReceipt(firstAccepted.value)).toMatchObject({ frameId: 2 });
447
+ expect(host.retireDynamicGeometry(firstAccepted.value)).toMatchObject({
448
+ ok: false,
449
+ error: { code: 'dynamic-geometry-invalid' },
450
+ });
451
+ expect(host.dynamicGeometryReceipt(firstAccepted.value)).toBeDefined();
452
+
453
+ const secondMesh = mesh();
454
+ secondMesh.vertices[0] = 3;
455
+ const secondHandle = world.allocSharedRef('MeshAsset', secondMesh);
456
+ const second = host.prepareDynamicGeometry({
457
+ world,
458
+ entity,
459
+ mesh: secondMesh,
460
+ meshHandle: secondHandle,
461
+ revision: 2,
462
+ topologyRevision: 2,
463
+ });
464
+ expect(second.ok).toBe(true);
465
+ if (!second.ok) return;
466
+ const secondAccepted = host.acceptDynamicGeometry(second.value, { world, fixedStep: 0 });
467
+ expect(secondAccepted.ok).toBe(true);
468
+ if (!secondAccepted.ok) return;
469
+ expect(host.dynamicGeometryReceipt(firstAccepted.value)).toBeDefined();
470
+
471
+ expect(host.retireDynamicGeometry(firstAccepted.value).ok).toBe(true);
472
+ expect(invalidated).toEqual([]);
473
+ completeFirst();
474
+ await Promise.resolve();
475
+ expect(invalidated).toEqual([]);
476
+ completeSecond();
477
+ await Promise.resolve();
478
+ expect(invalidated).toEqual([firstHandle]);
479
+ expect(host.cancelDynamicGeometry(secondAccepted.value).ok).toBe(true);
480
+ });
481
+
482
+ it('tracks a live entity after another entity advances topology', async () => {
483
+ const adapter = (await rhi.requestAdapter()).unwrap();
484
+ const device = (await adapter.requestDevice()).unwrap();
485
+ const store = new GpuResidencyCache();
486
+ store.configureGpuDevice(
487
+ device,
488
+ undefined,
489
+ () => {
490
+ throw new Error('cubemap registration is not part of this test');
491
+ },
492
+ device.caps,
493
+ );
494
+ const invalidated: number[] = [];
495
+ const invalidateMesh = store.invalidateMesh.bind(store);
496
+ vi.spyOn(store, 'invalidateMesh').mockImplementation((handle, worldId = 0) => {
497
+ invalidated.push(handle);
498
+ invalidateMesh(handle, worldId);
499
+ });
500
+ const world = new World();
501
+ const oldA = world.allocSharedRef('MeshAsset', mesh());
502
+ const candidateAMesh = mesh();
503
+ candidateAMesh.vertices[0] = 10;
504
+ const candidateA = world.allocSharedRef('MeshAsset', candidateAMesh);
505
+ const oldB = world.allocSharedRef('MeshAsset', mesh());
506
+ const candidateBMesh = mesh();
507
+ candidateBMesh.vertices[0] = 11;
508
+ const candidateB = world.allocSharedRef('MeshAsset', candidateBMesh);
509
+ const entityA = world
510
+ .spawn(
511
+ { component: Transform, data: {} },
512
+ { component: MeshFilter, data: { assetHandle: oldA } },
513
+ { component: MeshRenderer, data: { materials: [] } },
514
+ )
515
+ .unwrap();
516
+ const entityB = world
517
+ .spawn(
518
+ { component: Transform, data: {} },
519
+ { component: MeshFilter, data: { assetHandle: oldB } },
520
+ { component: MeshRenderer, data: { materials: [] } },
521
+ )
522
+ .unwrap();
523
+ const host = createDynamicGeometryHost({
524
+ lifecycle: createDynamicGeometryLifecycle(),
525
+ attachedWorlds: new Set([world]),
526
+ getGpuStore: () => store,
527
+ currentGeneration: () => 1,
528
+ isConsumedByRenderFrame: () => true,
529
+ });
530
+ const preparedA = host.prepareDynamicGeometry({
531
+ world,
532
+ entity: entityA,
533
+ mesh: candidateAMesh,
534
+ meshHandle: candidateA,
535
+ revision: 1,
536
+ topologyRevision: 1,
537
+ });
538
+ expect(preparedA.ok).toBe(true);
539
+ if (!preparedA.ok) return;
540
+ const acceptedA = host.acceptDynamicGeometry(preparedA.value, { world, fixedStep: 0 });
541
+ expect(acceptedA.ok).toBe(true);
542
+ if (!acceptedA.ok) return;
543
+
544
+ let completeFirst!: () => void;
545
+ const firstCompleted = new Promise<void>((resolve) => {
546
+ completeFirst = resolve;
547
+ });
548
+ expect(
549
+ host.publishDynamicGeometry(
550
+ { frameId: 1, deviceGeneration: 1, completed: firstCompleted },
551
+ [world],
552
+ 0,
553
+ ),
554
+ ).toHaveLength(1);
555
+
556
+ const preparedB = host.prepareDynamicGeometry({
557
+ world,
558
+ entity: entityB,
559
+ mesh: candidateBMesh,
560
+ meshHandle: candidateB,
561
+ revision: 2,
562
+ topologyRevision: 2,
563
+ });
564
+ expect(preparedB.ok).toBe(true);
565
+ if (!preparedB.ok) return;
566
+ const acceptedB = host.acceptDynamicGeometry(preparedB.value, { world, fixedStep: 0 });
567
+ expect(acceptedB.ok).toBe(true);
568
+ if (!acceptedB.ok) return;
569
+
570
+ let completeSecond!: () => void;
571
+ const secondCompleted = new Promise<void>((resolve) => {
572
+ completeSecond = resolve;
573
+ });
574
+ const frameTwoReceipts = host.publishDynamicGeometry(
575
+ { frameId: 2, deviceGeneration: 1, completed: secondCompleted },
576
+ [world],
577
+ 0,
578
+ );
579
+ expect(frameTwoReceipts.map((receipt) => receipt.candidateId)).toEqual(
580
+ expect.arrayContaining([acceptedA.value.candidateId, acceptedB.value.candidateId]),
581
+ );
582
+ expect(host.dynamicGeometryReceipt(acceptedA.value)).toMatchObject({ frameId: 2 });
583
+ expect(store.getMeshGpuHandles(candidateA, world)).toBeDefined();
584
+
585
+ expect(world.set(entityA, MeshFilter, { assetHandle: oldA }).ok).toBe(true);
586
+ expect(host.retireDynamicGeometry(acceptedA.value).ok).toBe(true);
587
+ expect(invalidated).toEqual([]);
588
+ completeFirst();
589
+ await Promise.resolve();
590
+ expect(invalidated).toEqual([]);
591
+ completeSecond();
592
+ await Promise.resolve();
593
+ expect(invalidated).toContain(candidateA);
594
+
595
+ expect(world.set(entityB, MeshFilter, { assetHandle: oldB }).ok).toBe(true);
596
+ expect(host.retireDynamicGeometry(acceptedB.value).ok).toBe(true);
597
+ expect(invalidated).toContain(candidateB);
598
+ });
599
+
600
+ it('keeps shared residency for another candidate while a receipt is pending', async () => {
601
+ const adapter = (await rhi.requestAdapter()).unwrap();
602
+ const device = (await adapter.requestDevice()).unwrap();
603
+ const store = new GpuResidencyCache();
604
+ store.configureGpuDevice(
605
+ device,
606
+ undefined,
607
+ () => {
608
+ throw new Error('cubemap registration is not part of this test');
609
+ },
610
+ device.caps,
611
+ );
612
+ const invalidated: number[] = [];
613
+ const invalidateMesh = store.invalidateMesh.bind(store);
614
+ vi.spyOn(store, 'invalidateMesh').mockImplementation((handle, worldId = 0) => {
615
+ invalidated.push(handle);
616
+ invalidateMesh(handle, worldId);
617
+ });
618
+ const world = new World();
619
+ const oldA = world.allocSharedRef('MeshAsset', mesh());
620
+ const oldB = world.allocSharedRef('MeshAsset', mesh());
621
+ const sharedMesh = mesh();
622
+ sharedMesh.vertices[0] = 12;
623
+ const sharedHandle = world.allocSharedRef('MeshAsset', sharedMesh);
624
+ const entityA = world
625
+ .spawn(
626
+ { component: Transform, data: {} },
627
+ { component: MeshFilter, data: { assetHandle: oldA } },
628
+ { component: MeshRenderer, data: { materials: [] } },
629
+ )
630
+ .unwrap();
631
+ const entityB = world
632
+ .spawn(
633
+ { component: Transform, data: {} },
634
+ { component: MeshFilter, data: { assetHandle: oldB } },
635
+ { component: MeshRenderer, data: { materials: [] } },
636
+ )
637
+ .unwrap();
638
+ const host = createDynamicGeometryHost({
639
+ lifecycle: createDynamicGeometryLifecycle(),
640
+ attachedWorlds: new Set([world]),
641
+ getGpuStore: () => store,
642
+ currentGeneration: () => 1,
643
+ isConsumedByRenderFrame: () => true,
644
+ });
645
+ const preparedA = host.prepareDynamicGeometry({
646
+ world,
647
+ entity: entityA,
648
+ mesh: sharedMesh,
649
+ meshHandle: sharedHandle,
650
+ revision: 1,
651
+ });
652
+ expect(preparedA.ok).toBe(true);
653
+ if (!preparedA.ok) return;
654
+ const acceptedA = host.acceptDynamicGeometry(preparedA.value, { world, fixedStep: 0 });
655
+ expect(acceptedA.ok).toBe(true);
656
+ if (!acceptedA.ok) return;
657
+ let completeA!: () => void;
658
+ const pendingA = new Promise<void>((resolve) => {
659
+ completeA = resolve;
660
+ });
661
+ expect(
662
+ host.publishDynamicGeometry(
663
+ { frameId: 1, deviceGeneration: 1, completed: pendingA },
664
+ [world],
665
+ 0,
666
+ ),
667
+ ).toHaveLength(1);
668
+
669
+ const preparedB = host.prepareDynamicGeometry({
670
+ world,
671
+ entity: entityB,
672
+ mesh: sharedMesh,
673
+ meshHandle: sharedHandle,
674
+ revision: 2,
675
+ });
676
+ expect(preparedB.ok).toBe(true);
677
+ if (!preparedB.ok) return;
678
+ expect(world.set(entityA, MeshFilter, { assetHandle: oldA }).ok).toBe(true);
679
+ expect(host.retireDynamicGeometry(acceptedA.value).ok).toBe(true);
680
+
681
+ // B only owns a prepared candidate. Cancelling it releases B's lease but
682
+ // cannot evict H while A's submitted frame still owns the cache entry.
683
+ expect(host.cancelDynamicGeometry(preparedB.value).ok).toBe(true);
684
+ expect(store.getMeshGpuHandles(sharedHandle, world)).toBeDefined();
685
+ expect(invalidated).toEqual([]);
686
+
687
+ completeA();
688
+ await Promise.resolve();
689
+ expect(store.getMeshGpuHandles(sharedHandle, world)).toBeUndefined();
690
+ expect(invalidated).toEqual([sharedHandle]);
691
+ });
692
+
693
+ it('keeps pending retirements inside the count and byte budget', async () => {
694
+ const adapter = (await rhi.requestAdapter()).unwrap();
695
+ const device = (await adapter.requestDevice()).unwrap();
696
+ const store = new GpuResidencyCache();
697
+ store.configureGpuDevice(
698
+ device,
699
+ undefined,
700
+ () => {
701
+ throw new Error('cubemap registration is not part of this test');
702
+ },
703
+ device.caps,
704
+ );
705
+ const invalidated: number[] = [];
706
+ const invalidateMesh = store.invalidateMesh.bind(store);
707
+ vi.spyOn(store, 'invalidateMesh').mockImplementation((handle, worldId = 0) => {
708
+ invalidated.push(handle);
709
+ invalidateMesh(handle, worldId);
710
+ });
711
+ const lifecycle = createDynamicGeometryLifecycle();
712
+ const world = new World();
713
+ const liveHandle = world.allocSharedRef('MeshAsset', mesh());
714
+ const entity = world
715
+ .spawn(
716
+ { component: Transform, data: {} },
717
+ { component: MeshFilter, data: { assetHandle: liveHandle } },
718
+ { component: MeshRenderer, data: { materials: [] } },
719
+ )
720
+ .unwrap();
721
+ const host = createDynamicGeometryHost({
722
+ lifecycle,
723
+ attachedWorlds: new Set([world]),
724
+ getGpuStore: () => store,
725
+ currentGeneration: () => 1,
726
+ isConsumedByRenderFrame: () => true,
727
+ });
728
+ const completions: Array<() => void> = [];
729
+ const handles: Handle<'MeshAsset', 'shared'>[] = [];
730
+ let candidateBytes = 0;
731
+ for (let revision = 1; revision <= 32; revision += 1) {
732
+ const candidateMesh = mesh();
733
+ candidateMesh.vertices[0] = revision;
734
+ const handle = world.allocSharedRef('MeshAsset', candidateMesh);
735
+ const prepared = host.prepareDynamicGeometry({
736
+ world,
737
+ entity,
738
+ mesh: candidateMesh,
739
+ meshHandle: handle,
740
+ revision,
741
+ });
742
+ expect(prepared.ok).toBe(true);
743
+ if (!prepared.ok) return;
744
+ candidateBytes = prepared.value.meshBytes;
745
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
746
+ expect(accepted.ok).toBe(true);
747
+ if (!accepted.ok) return;
748
+ let complete!: () => void;
749
+ const pending = new Promise<void>((resolve) => {
750
+ complete = resolve;
751
+ });
752
+ completions.push(complete);
753
+ expect(
754
+ host.publishDynamicGeometry(
755
+ { frameId: revision, deviceGeneration: 1, completed: pending },
756
+ [world],
757
+ 0,
758
+ ),
759
+ ).toHaveLength(1);
760
+ expect(world.set(entity, MeshFilter, { assetHandle: liveHandle }).ok).toBe(true);
761
+ expect(host.retireDynamicGeometry(accepted.value).ok).toBe(true);
762
+ handles.push(handle);
763
+ }
764
+
765
+ const blockedMesh = mesh();
766
+ const blockedHandle = world.allocSharedRef('MeshAsset', blockedMesh);
767
+ expect(
768
+ host.prepareDynamicGeometry({
769
+ world,
770
+ entity,
771
+ mesh: blockedMesh,
772
+ meshHandle: blockedHandle,
773
+ revision: 33,
774
+ }),
775
+ ).toMatchObject({
776
+ ok: false,
777
+ error: { code: 'dynamic-geometry-budget-exceeded' },
778
+ });
779
+ expect(world.sharedRefs.release(blockedHandle).ok).toBe(true);
780
+ expect(lifecycle.inspect()).toMatchObject({
781
+ prepared: 0,
782
+ accepted: 0,
783
+ published: 0,
784
+ retired: 32,
785
+ meshBytes: candidateBytes * 32,
786
+ });
787
+ expect(
788
+ handles.filter((handle) => store.getMeshGpuHandles(handle, world) !== undefined),
789
+ ).toHaveLength(32);
790
+
791
+ completions.forEach((complete) => {
792
+ complete();
793
+ });
794
+ await Promise.resolve();
795
+ await vi.waitFor(() => expect(lifecycle.inspect().meshBytes).toBe(0));
796
+ expect(invalidated).toHaveLength(32);
797
+ expect(handles.every((handle) => store.getMeshGpuHandles(handle, world) === undefined)).toBe(
798
+ true,
799
+ );
800
+ });
801
+
802
+ it('supersedes an older un-published candidate after a newer ECS swap', () => {
803
+ const world = new World();
804
+ const oldHandle = world.allocSharedRef('MeshAsset', mesh());
805
+ const firstMesh = mesh();
806
+ firstMesh.vertices[0] = 2;
807
+ const firstHandle = world.allocSharedRef('MeshAsset', firstMesh);
808
+ const secondMesh = mesh();
809
+ secondMesh.vertices[0] = 3;
810
+ const secondHandle = world.allocSharedRef('MeshAsset', secondMesh);
811
+ const entity = world
812
+ .spawn(
813
+ { component: Transform, data: {} },
814
+ { component: MeshFilter, data: { assetHandle: oldHandle } },
815
+ { component: MeshRenderer, data: { materials: [] } },
816
+ )
817
+ .unwrap();
818
+ const invalidated: number[] = [];
819
+ const host = createDynamicGeometryHost({
820
+ lifecycle: createDynamicGeometryLifecycle(),
821
+ attachedWorlds: new Set([world]),
822
+ getGpuStore: () =>
823
+ ({
824
+ ensureResident: () => ({ ok: true as const, value: {} }),
825
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
826
+ invalidateMesh: (handle: number) => invalidated.push(handle),
827
+ }) as unknown as GpuResidencyCache,
828
+ currentGeneration: () => 1,
829
+ isConsumedByRenderFrame: () => true,
830
+ });
831
+ const first = host.prepareDynamicGeometry({
832
+ world,
833
+ entity,
834
+ mesh: firstMesh,
835
+ meshHandle: firstHandle,
836
+ revision: 1,
837
+ });
838
+ expect(first.ok).toBe(true);
839
+ if (!first.ok) return;
840
+ expect(host.acceptDynamicGeometry(first.value, { world, fixedStep: 0 }).ok).toBe(true);
841
+ const second = host.prepareDynamicGeometry({
842
+ world,
843
+ entity,
844
+ mesh: secondMesh,
845
+ meshHandle: secondHandle,
846
+ revision: 2,
847
+ });
848
+ expect(second.ok).toBe(true);
849
+ if (!second.ok) return;
850
+ const accepted = host.acceptDynamicGeometry(second.value, { world, fixedStep: 0 });
851
+ expect(accepted.ok).toBe(true);
852
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(secondHandle);
853
+ expect(host.cancelDynamicGeometry(first.value)).toMatchObject({
854
+ ok: false,
855
+ error: { code: 'dynamic-geometry-candidate-not-found' },
856
+ });
857
+ expect(invalidated).toContain(firstHandle);
858
+ });
859
+
860
+ it('preserves another entity candidate when a newer topology is accepted', () => {
861
+ const world = new World();
862
+ const firstLive = world.allocSharedRef('MeshAsset', mesh());
863
+ const firstCandidateMesh = mesh();
864
+ firstCandidateMesh.vertices[0] = 5;
865
+ const firstCandidateHandle = world.allocSharedRef('MeshAsset', firstCandidateMesh);
866
+ const secondLive = world.allocSharedRef('MeshAsset', mesh());
867
+ const secondCandidateMesh = mesh();
868
+ secondCandidateMesh.vertices[0] = 6;
869
+ const secondCandidateHandle = world.allocSharedRef('MeshAsset', secondCandidateMesh);
870
+ const firstEntity = world
871
+ .spawn(
872
+ { component: Transform, data: {} },
873
+ { component: MeshFilter, data: { assetHandle: firstLive } },
874
+ { component: MeshRenderer, data: { materials: [] } },
875
+ )
876
+ .unwrap();
877
+ const secondEntity = world
878
+ .spawn(
879
+ { component: Transform, data: {} },
880
+ { component: MeshFilter, data: { assetHandle: secondLive } },
881
+ { component: MeshRenderer, data: { materials: [] } },
882
+ )
883
+ .unwrap();
884
+ const invalidated: number[] = [];
885
+ const host = createDynamicGeometryHost({
886
+ lifecycle: createDynamicGeometryLifecycle(),
887
+ attachedWorlds: new Set([world]),
888
+ getGpuStore: () =>
889
+ ({
890
+ ensureResident: () => ({ ok: true as const, value: {} }),
891
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
892
+ invalidateMesh: (handle: number) => invalidated.push(handle),
893
+ }) as unknown as GpuResidencyCache,
894
+ currentGeneration: () => 1,
895
+ isConsumedByRenderFrame: () => true,
896
+ });
897
+ const first = host.prepareDynamicGeometry({
898
+ world,
899
+ entity: firstEntity,
900
+ mesh: firstCandidateMesh,
901
+ meshHandle: firstCandidateHandle,
902
+ revision: 1,
903
+ topologyRevision: 1,
904
+ });
905
+ expect(first.ok).toBe(true);
906
+ if (!first.ok) return;
907
+ const second = host.prepareDynamicGeometry({
908
+ world,
909
+ entity: secondEntity,
910
+ mesh: secondCandidateMesh,
911
+ meshHandle: secondCandidateHandle,
912
+ revision: 2,
913
+ topologyRevision: 2,
914
+ });
915
+ expect(second.ok).toBe(true);
916
+ if (!second.ok) return;
917
+ expect(host.acceptDynamicGeometry(second.value, { world, fixedStep: 0 }).ok).toBe(true);
918
+ expect(invalidated).not.toContain(firstCandidateHandle);
919
+ expect(host.cancelDynamicGeometry(first.value).ok).toBe(true);
920
+ expect(invalidated).toContain(firstCandidateHandle);
921
+ expect(world.sharedRefs.refcount(firstCandidateHandle)).toBe(1);
922
+ });
923
+
924
+ it('requires a live MeshFilter/MeshRenderer binding and withholds receipts until draw consumption', async () => {
925
+ const world = new World();
926
+ const sourceMesh = mesh();
927
+ const meshHandle = world.allocSharedRef('MeshAsset', sourceMesh);
928
+ const entity = world
929
+ .spawn(
930
+ { component: Transform, data: {} },
931
+ { component: MeshFilter, data: { assetHandle: meshHandle } },
932
+ { component: MeshRenderer, data: { materials: [] } },
933
+ )
934
+ .unwrap();
935
+ const attachedWorlds = new Set([world]);
936
+ const lifecycle = createDynamicGeometryLifecycle();
937
+ const invalidatedMeshes: Array<{ readonly handle: number; readonly world: World }> = [];
938
+ let consumedByFrame = false;
939
+ const gpuStore = {
940
+ ensureResident: () => ({ ok: true as const, value: {} }),
941
+ retainMeshResidency: (handle: number) => {
942
+ const lifetime = new MeshResidencyLifetime(() => invalidatedMeshes.push({ handle, world }));
943
+ return lifetime.retain(() => lifetime.retire());
944
+ },
945
+ invalidateMesh: (handle: number, invalidationWorld: World) => {
946
+ invalidatedMeshes.push({ handle, world: invalidationWorld });
947
+ },
948
+ } as unknown as GpuResidencyCache;
949
+ const host = createDynamicGeometryHost({
950
+ lifecycle,
951
+ attachedWorlds,
952
+ getGpuStore: () => gpuStore,
953
+ currentGeneration: () => 1,
954
+ isConsumedByRenderFrame: () => consumedByFrame,
955
+ });
956
+
957
+ const prepared = host.prepareDynamicGeometry({
958
+ world,
959
+ entity,
960
+ mesh: sourceMesh,
961
+ meshHandle,
962
+ revision: 1,
963
+ topologyRevision: 1,
964
+ });
965
+ expect(prepared.ok).toBe(true);
966
+ if (!prepared.ok) return;
967
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
968
+ expect(accepted.ok).toBe(true);
969
+ if (!accepted.ok) return;
970
+
971
+ const replacementHandle = world.allocSharedRef('MeshAsset', mesh());
972
+ expect(world.set(entity, MeshFilter, { assetHandle: replacementHandle }).ok).toBe(true);
973
+ expect(host.publishDynamicGeometry({ frameId: 1, deviceGeneration: 1 }, [world], 0)).toEqual(
974
+ [],
975
+ );
976
+ expect(host.dynamicGeometryReceipt(accepted.value)).toBeUndefined();
977
+
978
+ expect(world.set(entity, MeshFilter, { assetHandle: meshHandle }).ok).toBe(true);
979
+ consumedByFrame = true;
980
+ let complete!: () => void;
981
+ const completed = new Promise<void>((resolve) => {
982
+ complete = resolve;
983
+ });
984
+ const receipts = host.publishDynamicGeometry(
985
+ { frameId: 2, deviceGeneration: 1, completed },
986
+ [world],
987
+ 0,
988
+ );
989
+ expect(receipts).toHaveLength(1);
990
+ expect(host.dynamicGeometryReceipt(accepted.value)).toMatchObject({ frameId: 2 });
991
+
992
+ const topologyMesh = mesh();
993
+ topologyMesh.vertices[0] = 8;
994
+ const topologyHandle = world.allocSharedRef('MeshAsset', topologyMesh);
995
+ const topologyCandidate = host.prepareDynamicGeometry({
996
+ world,
997
+ entity,
998
+ mesh: topologyMesh,
999
+ meshHandle: topologyHandle,
1000
+ revision: 2,
1001
+ topologyRevision: 2,
1002
+ });
1003
+ expect(topologyCandidate.ok).toBe(true);
1004
+ if (!topologyCandidate.ok) return;
1005
+ expect(host.acceptDynamicGeometry(topologyCandidate.value, { world, fixedStep: 0 }).ok).toBe(
1006
+ true,
1007
+ );
1008
+ // Topology invalidates temporal history, but the receipt fence remains
1009
+ // queryable until explicit retirement and FrameReceipt completion.
1010
+ expect(host.dynamicGeometryReceipt(accepted.value)).toMatchObject({ frameId: 2 });
1011
+ expect(host.cancelDynamicGeometry(accepted.value)).toMatchObject({
1012
+ ok: false,
1013
+ error: { code: 'dynamic-geometry-candidate-state' },
1014
+ });
1015
+ expect(world.get(entity, MeshFilter).unwrap().assetHandle).toBe(topologyHandle);
1016
+ // Retiring while the accepted handle is still live is safe but cannot
1017
+ // evict that binding. The consumer first swaps its ECS binding away, then
1018
+ // the FrameReceipt completion fence permits GPU destruction.
1019
+ expect(world.set(entity, MeshFilter, { assetHandle: replacementHandle }).ok).toBe(true);
1020
+ expect(host.retireDynamicGeometry(accepted.value).ok).toBe(true);
1021
+ expect(invalidatedMeshes).toEqual([]);
1022
+ complete();
1023
+ await Promise.resolve();
1024
+ expect(invalidatedMeshes).toEqual([{ handle: meshHandle, world }]);
1025
+ expect(host.dynamicGeometryReceipt(accepted.value)).toBeUndefined();
1026
+ });
1027
+
1028
+ it('rejects a candidate without an actual ECS render entity before GPU admission', () => {
1029
+ const invalidated: number[] = [];
1030
+ const world = new World();
1031
+ const sourceMesh = mesh();
1032
+ const meshHandle = world.allocSharedRef('MeshAsset', sourceMesh);
1033
+ const host = createDynamicGeometryHost({
1034
+ lifecycle: createDynamicGeometryLifecycle(),
1035
+ attachedWorlds: new Set([world]),
1036
+ getGpuStore: () =>
1037
+ ({
1038
+ ensureResident: () => ({ ok: true as const, value: {} }),
1039
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
1040
+ }) as unknown as GpuResidencyCache,
1041
+ currentGeneration: () => 1,
1042
+ isConsumedByRenderFrame: () => false,
1043
+ });
1044
+
1045
+ expect(
1046
+ host.prepareDynamicGeometry({
1047
+ world,
1048
+ mesh: sourceMesh,
1049
+ meshHandle,
1050
+ revision: 1,
1051
+ }),
1052
+ ).toMatchObject({ ok: false, error: { code: 'dynamic-geometry-invalid' } });
1053
+
1054
+ const entity = world
1055
+ .spawn(
1056
+ { component: Transform, data: {} },
1057
+ { component: MeshFilter, data: { assetHandle: meshHandle } },
1058
+ { component: MeshRenderer, data: { materials: [] } },
1059
+ )
1060
+ .unwrap();
1061
+ expect(
1062
+ host.prepareDynamicGeometry({ world, entity, mesh: sourceMesh, revision: 2 }),
1063
+ ).toMatchObject({ ok: false, error: { code: 'dynamic-geometry-gpu-not-ready' } });
1064
+ });
1065
+
1066
+ it('requires active PhysicsWorld admission, not an already committed publication', () => {
1067
+ const invalidated: number[] = [];
1068
+ const world = new World();
1069
+ const sourceMesh = mesh();
1070
+ const meshHandle = world.allocSharedRef('MeshAsset', sourceMesh);
1071
+ const entity = world
1072
+ .spawn(
1073
+ { component: Transform, data: {} },
1074
+ { component: MeshFilter, data: { assetHandle: meshHandle } },
1075
+ { component: MeshRenderer, data: { materials: [] } },
1076
+ )
1077
+ .unwrap();
1078
+ let publication: { readonly revision: number; readonly fixedStep: number } | undefined;
1079
+ let admission: typeof publication;
1080
+ world.insertResource('PhysicsWorld', {
1081
+ getDerivedPublication: () => publication,
1082
+ getDerivedAdmission: () => admission,
1083
+ });
1084
+ const host = createDynamicGeometryHost({
1085
+ lifecycle: createDynamicGeometryLifecycle(),
1086
+ attachedWorlds: new Set([world]),
1087
+ getGpuStore: () =>
1088
+ ({
1089
+ ensureResident: () => ({ ok: true as const, value: {} }),
1090
+ retainMeshResidency: (handle: number) => residencyLease(handle, invalidated),
1091
+ }) as unknown as GpuResidencyCache,
1092
+ currentGeneration: () => 1,
1093
+ isConsumedByRenderFrame: () => true,
1094
+ });
1095
+ const prepared = host.prepareDynamicGeometry({
1096
+ world,
1097
+ entity,
1098
+ physicsEntity: entity,
1099
+ mesh: sourceMesh,
1100
+ meshHandle,
1101
+ revision: 1,
1102
+ });
1103
+ expect(prepared.ok).toBe(true);
1104
+ if (!prepared.ok) return;
1105
+ expect(host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 })).toMatchObject({
1106
+ ok: false,
1107
+ error: { code: 'dynamic-geometry-ordering-required' },
1108
+ });
1109
+ publication = { revision: 1, fixedStep: 0 };
1110
+ expect(host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 })).toMatchObject({
1111
+ ok: false,
1112
+ error: { code: 'dynamic-geometry-ordering-required' },
1113
+ });
1114
+ admission = publication;
1115
+ const accepted = host.acceptDynamicGeometry(prepared.value, { world, fixedStep: 0 });
1116
+ admission = undefined;
1117
+ expect(accepted.ok).toBe(true);
1118
+ if (!accepted.ok) return;
1119
+ expect(
1120
+ host.publishDynamicGeometry({ frameId: 1, deviceGeneration: 1 }, [world], 0),
1121
+ ).toHaveLength(1);
1122
+ });
1123
+ });