@woosh/meep-engine 3.22.0 → 3.23.0

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 (267) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts +118 -0
  3. package/src/core/geom/3d/hash-grid/PointHashGrid3.d.ts.map +1 -0
  4. package/src/core/geom/3d/hash-grid/PointHashGrid3.js +283 -0
  5. package/src/core/geom/3d/shape/BoxShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/BoxShape3D.js +6 -17
  7. package/src/core/geom/3d/shape/CapsuleShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/CapsuleShape3D.js +3 -14
  9. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/CylinderShape3D.js +3 -21
  11. package/src/core/geom/3d/shape/PlaneShape3D.d.ts +105 -0
  12. package/src/core/geom/3d/shape/PlaneShape3D.d.ts.map +1 -0
  13. package/src/core/geom/3d/shape/PlaneShape3D.js +251 -0
  14. package/src/core/geom/3d/shape/SphereShape3D.js +3 -3
  15. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  16. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  17. package/src/core/geom/3d/shape/json/type_adapters.d.ts +14 -0
  18. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  19. package/src/core/geom/3d/shape/json/type_adapters.js +15 -0
  20. package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts +51 -0
  21. package/src/core/geom/3d/shape/sdf/sdf3_box.d.ts.map +1 -0
  22. package/src/core/geom/3d/shape/sdf/sdf3_box.js +113 -0
  23. package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts +46 -0
  24. package/src/core/geom/3d/shape/sdf/sdf3_capsule.d.ts.map +1 -0
  25. package/src/core/geom/3d/shape/sdf/sdf3_capsule.js +82 -0
  26. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts +60 -0
  27. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/sdf/sdf3_cylinder.js +126 -0
  29. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts +50 -0
  30. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.d.ts.map +1 -0
  31. package/src/core/geom/3d/shape/sdf/sdf3_degenerate_normal.js +53 -0
  32. package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts +52 -0
  33. package/src/core/geom/3d/shape/sdf/sdf3_plane.d.ts.map +1 -0
  34. package/src/core/geom/3d/shape/sdf/sdf3_plane.js +61 -0
  35. package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts +41 -0
  36. package/src/core/geom/3d/shape/sdf/sdf3_sphere.d.ts.map +1 -0
  37. package/src/core/geom/3d/shape/sdf/sdf3_sphere.js +71 -0
  38. package/src/core/process/ConcurrencyGate.d.ts +103 -0
  39. package/src/core/process/ConcurrencyGate.d.ts.map +1 -0
  40. package/src/core/process/ConcurrencyGate.js +207 -0
  41. package/src/engine/asset/AssetManager.d.ts +10 -0
  42. package/src/engine/asset/AssetManager.d.ts.map +1 -1
  43. package/src/engine/asset/AssetManager.js +10 -0
  44. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +30 -13
  45. package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts +26 -0
  46. package/src/engine/graphics3/pose/collect_entity_playbacks.d.ts.map +1 -0
  47. package/src/engine/graphics3/pose/collect_entity_playbacks.js +40 -0
  48. package/src/engine/graphics3/pose/query_entity_node_world_pose.d.ts.map +1 -1
  49. package/src/engine/graphics3/pose/query_entity_node_world_pose.js +74 -101
  50. package/src/engine/physics/cloth/MEASUREMENTS.md +1287 -4
  51. package/src/engine/physics/cloth/PLAN.md +1967 -1581
  52. package/src/engine/physics/cloth/build/ClothProxy.d.ts +147 -0
  53. package/src/engine/physics/cloth/build/ClothProxy.d.ts.map +1 -0
  54. package/src/engine/physics/cloth/build/ClothProxy.js +207 -0
  55. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts +70 -0
  56. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.d.ts.map +1 -0
  57. package/src/engine/physics/cloth/build/cloth_proxy_from_joints.js +256 -0
  58. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +176 -0
  59. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -0
  60. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +435 -0
  61. package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts +12 -0
  62. package/src/engine/physics/cloth/collider/ClothColliderKind.d.ts.map +1 -0
  63. package/src/engine/physics/cloth/collider/ClothColliderKind.js +38 -0
  64. package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts +120 -0
  65. package/src/engine/physics/cloth/collider/ClothColliderRecord.d.ts.map +1 -0
  66. package/src/engine/physics/cloth/collider/ClothColliderRecord.js +137 -0
  67. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts +25 -0
  68. package/src/engine/physics/cloth/collider/cloth_collider_bounds.d.ts.map +1 -0
  69. package/src/engine/physics/cloth/collider/cloth_collider_bounds.js +190 -0
  70. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts +44 -0
  71. package/src/engine/physics/cloth/collider/cloth_collider_pack.d.ts.map +1 -0
  72. package/src/engine/physics/cloth/collider/cloth_collider_pack.js +173 -0
  73. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts +94 -0
  74. package/src/engine/physics/cloth/collider/cloth_collider_sdf.d.ts.map +1 -0
  75. package/src/engine/physics/cloth/collider/cloth_collider_sdf.js +275 -0
  76. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts +126 -0
  77. package/src/engine/physics/cloth/ecs/ClothCollider.d.ts.map +1 -0
  78. package/src/engine/physics/cloth/ecs/ClothCollider.js +199 -0
  79. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts +9 -0
  80. package/src/engine/physics/cloth/ecs/ClothColliderFlags.d.ts.map +1 -0
  81. package/src/engine/physics/cloth/ecs/ClothColliderFlags.js +32 -0
  82. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts +114 -0
  83. package/src/engine/physics/cloth/ecs/ClothColliderSystem.d.ts.map +1 -0
  84. package/src/engine/physics/cloth/ecs/ClothColliderSystem.js +243 -0
  85. package/src/engine/physics/cloth/ecs/ClothDynamicsFlags.js +66 -57
  86. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +109 -1
  87. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  88. package/src/engine/physics/cloth/ecs/ClothInstance.js +479 -363
  89. package/src/engine/physics/cloth/ecs/ClothRig.d.ts +95 -0
  90. package/src/engine/physics/cloth/ecs/ClothRig.d.ts.map +1 -0
  91. package/src/engine/physics/cloth/ecs/ClothRig.js +140 -0
  92. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +30 -1
  93. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  94. package/src/engine/physics/cloth/ecs/ClothSystem.js +1021 -773
  95. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts +90 -0
  96. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -0
  97. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +374 -0
  98. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts +32 -3
  99. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/ecs/cloth_dynamics_library.js +283 -254
  101. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +59 -0
  102. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -0
  103. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +328 -0
  104. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts +13 -0
  105. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.d.ts.map +1 -0
  106. package/src/engine/physics/cloth/ecs/cloth_reconstruct_pose.js +288 -0
  107. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +58 -0
  108. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -0
  109. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +199 -0
  110. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +8 -63
  111. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  112. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +317 -645
  113. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts +61 -0
  114. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  115. package/src/engine/physics/cloth/ecs/cloth_write_back.js +212 -449
  116. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts +49 -0
  117. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.d.ts.map +1 -0
  118. package/src/engine/physics/cloth/ecs/cloth_write_back_rig.js +115 -0
  119. package/src/engine/physics/cloth/playground/README.md +155 -1
  120. package/src/engine/physics/cloth/playground/collide.html +178 -0
  121. package/src/engine/physics/cloth/playground/collide_build.d.ts +95 -0
  122. package/src/engine/physics/cloth/playground/collide_build.d.ts.map +1 -0
  123. package/src/engine/physics/cloth/playground/collide_build.js +315 -0
  124. package/src/engine/physics/cloth/playground/collide_main.d.ts +2 -0
  125. package/src/engine/physics/cloth/playground/collide_main.d.ts.map +1 -0
  126. package/src/engine/physics/cloth/playground/collide_main.js +720 -0
  127. package/src/engine/physics/cloth/playground/garment.html +182 -0
  128. package/src/engine/physics/cloth/playground/garment_build.d.ts +78 -0
  129. package/src/engine/physics/cloth/playground/garment_build.d.ts.map +1 -0
  130. package/src/engine/physics/cloth/playground/garment_build.js +359 -0
  131. package/src/engine/physics/cloth/playground/garment_main.d.ts +2 -0
  132. package/src/engine/physics/cloth/playground/garment_main.d.ts.map +1 -0
  133. package/src/engine/physics/cloth/playground/garment_main.js +633 -0
  134. package/src/engine/physics/cloth/solver/ClothState.d.ts +208 -5
  135. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  136. package/src/engine/physics/cloth/solver/ClothState.js +302 -8
  137. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +87 -0
  138. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -0
  139. package/src/engine/physics/cloth/solver/cloth_contact_find.js +343 -0
  140. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts +112 -0
  141. package/src/engine/physics/cloth/solver/cloth_self_find.d.ts.map +1 -0
  142. package/src/engine/physics/cloth/solver/cloth_self_find.js +448 -0
  143. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts +4 -1
  144. package/src/engine/physics/cloth/solver/cloth_solve_vertex.d.ts.map +1 -1
  145. package/src/engine/physics/cloth/solver/cloth_solve_vertex.js +16 -1
  146. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +112 -5
  147. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  148. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +117 -6
  149. package/src/engine/physics/cloth/solver/cloth_step.d.ts +40 -9
  150. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  151. package/src/engine/physics/cloth/solver/cloth_step.js +122 -10
  152. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +95 -0
  153. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -0
  154. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +285 -0
  155. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts +129 -0
  156. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.d.ts.map +1 -0
  157. package/src/engine/physics/cloth/solver/constraint/cloth_contact_row.js +501 -0
  158. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts +75 -0
  159. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.d.ts.map +1 -0
  160. package/src/engine/physics/cloth/solver/constraint/cloth_self_row.js +202 -0
  161. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  162. package/src/shade/playground/vgeo_viewer/README.md +18 -0
  163. package/src/shade/playground/vgeo_viewer/main.js +87 -50
  164. package/src/shade/renderer/buffer/table/GPUDatabase.d.ts.map +1 -1
  165. package/src/shade/renderer/buffer/table/GPUDatabase.js +191 -62
  166. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts +94 -2
  167. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  168. package/src/shade/renderer/buffer/table/GPUTypedTable.js +414 -2
  169. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts +14 -0
  170. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.d.ts.map +1 -0
  171. package/src/shade/renderer/buffer/table/GPU_TABLE_PARTIAL_UPLOAD_DESCRIPTOR_WORDS.js +13 -0
  172. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts +17 -0
  173. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.d.ts.map +1 -0
  174. package/src/shade/renderer/buffer/table/gpu_table_partial_upload_shader.js +116 -0
  175. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +9 -4
  176. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  177. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +843 -734
  178. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts +12 -0
  179. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.d.ts.map +1 -1
  180. package/src/shade/renderer/geometry/bvh/GPUGeometryMeshletsBVH.js +13 -0
  181. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +17 -0
  182. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  183. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +185 -30
  184. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +17 -0
  185. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +141 -24
  186. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +204 -64
  187. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +110 -10
  188. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +19 -0
  189. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +19 -6
  190. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -1
  191. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +26 -7
  192. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +173 -14
  193. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  194. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +458 -51
  195. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +49 -13
  196. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -1
  197. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +52 -14
  198. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts +20 -0
  199. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.d.ts.map +1 -0
  200. package/src/shade/renderer/geometry/virtual/format/read/tool/VGEO_MAX_AUDIT_BYTES.js +19 -0
  201. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts +27 -0
  202. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.d.ts.map +1 -0
  203. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_audit_container.js +48 -0
  204. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts +29 -0
  205. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.d.ts.map +1 -0
  206. package/src/shade/renderer/geometry/virtual/format/read/tool/vgeo_install_all_pages.js +60 -0
  207. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +8 -1
  208. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -1
  209. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +32 -2
  210. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +258 -0
  211. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +1 -0
  212. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.js +723 -0
  213. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +49 -0
  214. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +1 -0
  215. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.js +50 -0
  216. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  217. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  218. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts +3 -0
  219. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.d.ts.map +1 -1
  220. package/src/shade/renderer/texture/bindless/GPUBindlessTextureManager.js +121 -21
  221. package/src/shade/renderer/texture/virtual/README.md +6 -4
  222. package/src/shade/renderer/texture/virtual/VirtualTextureManager.d.ts.map +1 -1
  223. package/src/shade/renderer/texture/virtual/VirtualTextureManager.js +7 -0
  224. package/src/shade/renderer/texture/virtual/source/VTSourceTiled.d.ts +3 -3
  225. package/src/shade/renderer/texture/virtual/source/VTSourceTiled.js +3 -3
  226. package/src/engine/ecs/system/SystemExecutionPhase.d.ts +0 -9
  227. package/src/engine/ecs/system/SystemExecutionPhase.d.ts.map +0 -1
  228. package/src/engine/graphics/texture/virtual/NOTES.md +0 -128
  229. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts +0 -6
  230. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.d.ts.map +0 -1
  231. package/src/engine/graphics/texture/virtual/VT_DEFAULT_PAGE_RESOLUTION.js +0 -5
  232. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts +0 -49
  233. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.d.ts.map +0 -1
  234. package/src/engine/graphics/texture/virtual/VirtualTextureTileLoader.js +0 -264
  235. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts +0 -44
  236. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.d.ts.map +0 -1
  237. package/src/engine/graphics/texture/virtual/VirtualTextureUsage.js +0 -228
  238. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts +0 -3
  239. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.d.ts.map +0 -1
  240. package/src/engine/graphics/texture/virtual/VirtualTextureUsageShader.js +0 -73
  241. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts +0 -14
  242. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.d.ts.map +0 -1
  243. package/src/engine/graphics/texture/virtual/debug/ResidencyDebugView.js +0 -85
  244. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts +0 -12
  245. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.d.ts.map +0 -1
  246. package/src/engine/graphics/texture/virtual/debug/UsageDebugView.js +0 -63
  247. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts +0 -12
  248. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.d.ts.map +0 -1
  249. package/src/engine/graphics/texture/virtual/debug/UsagePyramidDebugView.js +0 -243
  250. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts +0 -30
  251. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.d.ts.map +0 -1
  252. package/src/engine/graphics/texture/virtual/tile/VirtualTextureTile.js +0 -39
  253. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts +0 -9
  254. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.d.ts.map +0 -1
  255. package/src/engine/graphics/texture/virtual/tile/compose_finger_print.js +0 -24
  256. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts +0 -9
  257. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.d.ts.map +0 -1
  258. package/src/engine/graphics/texture/virtual/tile/compose_tile_address.js +0 -26
  259. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts +0 -11
  260. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.d.ts.map +0 -1
  261. package/src/engine/graphics/texture/virtual/tile/decompose_finger_print.js +0 -12
  262. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts +0 -7
  263. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.d.ts.map +0 -1
  264. package/src/engine/graphics/texture/virtual/tile/finger_print_to_tile_address.js +0 -16
  265. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts +0 -7
  266. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.d.ts.map +0 -1
  267. package/src/engine/graphics/texture/virtual/tile/tile_address_to_finger_print.js +0 -35
@@ -5,7 +5,6 @@ import { VGEO_NO_CHILD } from "../cluster/VGEO_NO_CHILD.js";
5
5
  import { VGEO_SAME_PAGE } from "../cluster/VGEO_SAME_PAGE.js";
6
6
  import { VGEO_FRAME_OFFSET } from "../frame/VGEO_FRAME_OFFSET.js";
7
7
  import { vgeo_read_frame_extent } from "../frame/vgeo_read_frame_extent.js";
8
- import { vgeo_validate_container } from "../vgeo_validate_container.js";
9
8
  import { VGeoReadOptions } from "./VGeoReadOptions.js";
10
9
  import { vgeo_read_directory } from "./vgeo_read_directory.js";
11
10
  import { vgeo_read_header, vgeo_resident_head_size, VGEO_MIN_HEAD_BYTES } from "./vgeo_read_header.js";
@@ -17,6 +16,12 @@ import { vgeo_read_page } from "./vgeo_read_page.js";
17
16
  */
18
17
  const LE = true;
19
18
 
19
+ /**
20
+ * What {@link VGeoContainerReader.release_head} leaves behind, shared because it is empty.
21
+ * @type {Uint8Array}
22
+ */
23
+ const NO_HEAD = new Uint8Array(0);
24
+
20
25
  /**
21
26
  * A `.vgeo` container, read a page at a time.
22
27
  *
@@ -42,11 +47,19 @@ const LE = true;
42
47
  * in for one that did not, and draws both over the same surface. That is what activation is for, and
43
48
  * it is why installing a page does bookkeeping rather than only filling a map.
44
49
  *
50
+ * **Mechanics, not policy — and the line is deliberate.** Which page to fetch next, which to drop,
51
+ * how many bytes one asset may hold, how many requests may be in flight across a thousand of them:
52
+ * none of that is decided here, because none of it can be decided by one reader looking at one file.
53
+ * `VGeoResidencyManager` is where it is decided and readers register with one; §10's eviction bullet
54
+ * is the reason for the split rather than an apology for it. What *is* here is everything that is
55
+ * about the bytes — the fetch, the retries, the quarantine a page that will not arrive ends up in,
56
+ * and the accounting a budget owner spends against.
57
+ *
45
58
  * **Validation is not on this path.** The mandatory reject rules of §4 and §10 are applied
46
59
  * unconditionally by {@link vgeo_read_header} and {@link vgeo_read_page} — a file whose magic,
47
60
  * versions, encoding or codec this build does not understand is refused rather than misread. The
48
- * whole-file audit is {@link validate}, it reads every byte of the file, and it is off by default;
49
- * see {@link VGeoReadOptions.validate}.
61
+ * whole-file audit is `vgeo_audit_container` in `tool/`, it reads every byte of the file, and
62
+ * nothing on the runtime path can reach it.
50
63
  *
51
64
  * @author Alex Goldring
52
65
  * @copyright Company Named Limited (c) 2026
@@ -89,15 +102,36 @@ export class VGeoContainerReader {
89
102
  #pending = new Map();
90
103
 
91
104
  /**
92
- * The speculative head prefix, kept.
105
+ * The abort for each fetch in flight, by page index — one per fetch, so a want that went stale
106
+ * takes down its own request and no other. See {@link cancel}.
107
+ * @type {Map<number, AbortController>}
108
+ */
109
+ #aborts = new Map();
110
+
111
+ /**
112
+ * Pages that could not be read, and why, by page index.
113
+ *
114
+ * §10's quarantine. A page that has failed its bounded retries is not asked for again: at a
115
+ * content-addressed URL the failure is deterministic, so asking again asks the same question,
116
+ * and a traversal that keeps wanting the page would keep re-issuing it forever. What makes this
117
+ * survivable rather than a loss is the clamped cut — a quarantined page's parent stays selected
118
+ * and keeps covering that surface correctly, indefinitely.
119
+ *
120
+ * @type {Map<number, string>}
121
+ */
122
+ #quarantined = new Map();
123
+
124
+ /**
125
+ * The speculative head prefix, while it is still paying for itself.
93
126
  *
94
- * Coarse-first layout is what makes this worth keeping rather than parsing and dropping: a
95
- * prefix that overshot the root frame overshot into the pages the traversal is most likely to
96
- * want next, and serving those from here costs no request at all.
127
+ * Coarse-first layout is what makes a prefix worth fetching at all: one that overshot the root
128
+ * frame overshot into the pages the traversal is most likely to want next, and serving those
129
+ * from here costs no request. What it is *not* worth is keeping afterwards — see
130
+ * {@link release_head}, which runs by itself the first time a read falls outside it.
97
131
  *
98
132
  * @type {Uint8Array}
99
133
  */
100
- #head = new Uint8Array(0);
134
+ #head = NO_HEAD;
101
135
 
102
136
  /**
103
137
  * @type {number}
@@ -135,6 +169,16 @@ export class VGeoContainerReader {
135
169
  * the page starts its counters from the truth. Evicting a page moves its counters back here for
136
170
  * the same reason: the parents making those references are still resident and still selectable.
137
171
  *
172
+ * **It is bounded by the residency frontier and not by the session's history**, which is worth
173
+ * writing down because it has the shape of an index that leaks. An entry exists only while some
174
+ * resident, selectable cluster refers into a page that is absent, and the two halves are
175
+ * symmetric — eviction and deactivation release exactly what install and activation counted.
176
+ * Measured on a 20,000-page container with the 3,280 pages of its eight coarse bands pinned
177
+ * resident: 6,561 entries after pinning, and still 6,561 after touring all 6,561 pages of the
178
+ * band below them a window at a time and evicting each window. The frontier, unchanged, rather
179
+ * than one entry per page visited. Dropping the pinned pages too leaves 3.
180
+ * {@link pending_reference_page_count} is that number.
181
+ *
138
182
  * @type {Map<number, Map<number, number>>}
139
183
  */
140
184
  #pending_refs = new Map();
@@ -173,6 +217,16 @@ export class VGeoContainerReader {
173
217
 
174
218
  /**
175
219
  * Decoded bytes currently held. The number a residency budget is spent against.
220
+ *
221
+ * It is exactly the decoded page images — the same number the directory's `decoded_size` column
222
+ * and every child-table entry promise *before* a page is fetched, which is what lets a budget
223
+ * owner admit a page rather than discover its cost after paying for it. It is deliberately not
224
+ * what the pages cost in the heap: the reader's own per-page bookkeeping — the `VGeoPage`, its
225
+ * cluster and child tables, the two activation arrays, the map entries — measured 6.4–11.9 KB
226
+ * per page on top of the image. That is 1.2% of a 544 KB page at the sizes design §5 targets and
227
+ * 294% of a 2 KB one, so a budget sized against very small pages has to allow for it and one
228
+ * sized against the design's page size is inside its own rounding.
229
+ *
176
230
  * @returns {number}
177
231
  */
178
232
  get resident_bytes() {
@@ -186,9 +240,49 @@ export class VGeoContainerReader {
186
240
  return this.#pending.size;
187
241
  }
188
242
 
243
+ /**
244
+ * How many not-yet-resident pages the reverse-reference index is holding counters for.
245
+ *
246
+ * Diagnostic, and the number a leak in that index would show up in: an entry exists only while
247
+ * some resident, selectable cluster refers into a page that is not here, so this tracks the
248
+ * residency frontier and must not track how many pages a session has visited. See
249
+ * {@link #pending_refs} for what that was measured at.
250
+ *
251
+ * @returns {number}
252
+ */
253
+ get pending_reference_page_count() {
254
+ return this.#pending_refs.size;
255
+ }
256
+
257
+ /**
258
+ * How many bytes of speculative head prefix are still held — zero once {@link release_head} has
259
+ * run, which is the normal state after the first read that falls outside the prefix.
260
+ *
261
+ * @returns {number}
262
+ */
263
+ get head_bytes() {
264
+ return this.#head.byteLength;
265
+ }
266
+
267
+ /**
268
+ * How many pages have failed their retries and will not be asked for again.
269
+ * @returns {number}
270
+ */
271
+ get quarantined_page_count() {
272
+ return this.#quarantined.size;
273
+ }
274
+
189
275
  /**
190
276
  * The root page, which is pinned for the life of the reader — §10 makes it the one page that is
191
277
  * never evictable, because every traversal starts by walking its parentless groups.
278
+ *
279
+ * What the pin costs is worth writing down, because "pinned for life, times a thousand assets"
280
+ * is the shape of a problem and this one is not: build invariant §11.7 puts exactly one root
281
+ * cluster in the file, and the assembler gives the root page the complete topmost level — so the
282
+ * root page *is* that one cluster. Measured on two torus builds, of 7.7K and 42K source
283
+ * triangles and 8 and 12 levels: 1,732 B and 1,472 B decoded. A thousand of them is under 2 MB,
284
+ * and no builder-side cap on root size is needed to keep it that way.
285
+ *
192
286
  * @returns {VGeoPage}
193
287
  */
194
288
  get root() {
@@ -196,13 +290,28 @@ export class VGeoContainerReader {
196
290
  }
197
291
 
198
292
  /**
199
- * Every resident page, in page-index order.
293
+ * Every resident page, sorted by page index.
294
+ *
295
+ * A snapshot, and it costs one: an array of every resident page plus a sort of it — 0.29 ms per
296
+ * access measured at 20,000 resident pages. So this is for a tool, a panel or a test, and **not
297
+ * for anything that runs per frame**. {@link resident_pages} is the same set in arrival order
298
+ * and allocates nothing, which is what a per-frame sweep should read.
299
+ *
200
300
  * @returns {VGeoPage[]}
201
301
  */
202
302
  get pages() {
203
303
  return Array.from(this.#pages.values()).sort((a, b) => a.index - b.index);
204
304
  }
205
305
 
306
+ /**
307
+ * Every resident page, in arrival order, allocating nothing.
308
+ *
309
+ * @returns {IterableIterator<VGeoPage>}
310
+ */
311
+ resident_pages() {
312
+ return this.#pages.values();
313
+ }
314
+
206
315
  /**
207
316
  * Open a container: header, level table, root page.
208
317
  *
@@ -243,7 +352,8 @@ export class VGeoContainerReader {
243
352
  §10's always-resident set: the header, the level table, and the root frame — in that order,
244
353
  contiguously, and bounded by two fields of the fixed header. Whatever the prefix already
245
354
  covered, one range brings the rest, so a cold start is two requests at worst and one
246
- whenever the prefix was generous. Nothing is fetched twice: the head only grows at its tail.
355
+ whenever the prefix was generous. Nothing is fetched twice: the head only grows at its tail,
356
+ and it only grows here, once, before anything else has read a byte of the file.
247
357
  */
248
358
  await this.#grow_head(vgeo_resident_head_size(this.#head));
249
359
 
@@ -311,6 +421,38 @@ export class VGeoContainerReader {
311
421
  return this.#pages.has(page_index);
312
422
  }
313
423
 
424
+ /**
425
+ * How many of a page's child pages are resident.
426
+ *
427
+ * §10's eviction rule is written in terms of this: within a residency tier, a page is evictable
428
+ * only while it has no resident child pages in that tier, checked against its own child table.
429
+ * `SAME_PAGE` references never pin and are in no child table by construction (§11.2), so the
430
+ * table is the whole answer. The *rule* is a policy and lives with the budget owner; this
431
+ * lookup is about one page's own bytes and lives here.
432
+ *
433
+ * @param {number} page_index
434
+ * @returns {number} zero for a page that is not resident, and for one no child of which is
435
+ */
436
+ resident_child_count(page_index) {
437
+ const page = this.#pages.get(page_index);
438
+
439
+ if (page === undefined) {
440
+ return 0;
441
+ }
442
+
443
+ const children = page.child_page_index;
444
+
445
+ let count = 0;
446
+
447
+ for (let i = 0; i < children.length; i++) {
448
+ if (this.#pages.has(children[i])) {
449
+ count++;
450
+ }
451
+ }
452
+
453
+ return count;
454
+ }
455
+
314
456
  /**
315
457
  * Whether a group may be selected from — §10's activation invariant, and **not** the same
316
458
  * question as {@link is_resident}.
@@ -570,16 +712,75 @@ export class VGeoContainerReader {
570
712
  }
571
713
  }
572
714
 
715
+ /**
716
+ * Whether a page has failed its retries and been quarantined.
717
+ *
718
+ * @param {number} page_index
719
+ * @returns {boolean}
720
+ */
721
+ is_quarantined(page_index) {
722
+ return this.#quarantined.has(page_index);
723
+ }
724
+
725
+ /**
726
+ * Why a page was quarantined, or `undefined` if it was not.
727
+ *
728
+ * §10 asks for the quarantine *and* for telemetry, because a page that stopped arriving is the
729
+ * difference between a coarse asset and a broken deployment, and on screen the two are the same
730
+ * picture.
731
+ *
732
+ * @param {number} page_index
733
+ * @returns {string|undefined}
734
+ */
735
+ quarantine_reason(page_index) {
736
+ return this.#quarantined.get(page_index);
737
+ }
738
+
739
+ /**
740
+ * Every quarantined page and its reason.
741
+ * @returns {IterableIterator<[number, string]>}
742
+ */
743
+ quarantine() {
744
+ return this.#quarantined.entries();
745
+ }
746
+
747
+ /**
748
+ * Let a quarantined page be asked for again.
749
+ *
750
+ * **When** is a policy and is not decided here. A frame checksum that failed at a
751
+ * content-addressed URL will fail again and lifting its quarantine buys nothing; a run of 503s
752
+ * from one edge node is a different thing, and a budget owner that has watched other pages of
753
+ * other assets arrive since has evidence this reader does not have. So the reader quarantines
754
+ * for its own lifetime and offers this, and `VGeoResidencyManager` is where the decision to use
755
+ * it lives.
756
+ *
757
+ * @param {number} [page_index] omit to lift every quarantine
758
+ * @returns {number} how many pages were released
759
+ */
760
+ clear_quarantine(page_index = undefined) {
761
+ if (page_index === undefined) {
762
+ const released = this.#quarantined.size;
763
+
764
+ this.#quarantined.clear();
765
+
766
+ return released;
767
+ }
768
+
769
+ return this.#quarantined.delete(page_index) ? 1 : 0;
770
+ }
771
+
573
772
  /**
574
773
  * Fetch and install one page, from the exact range something already resident named.
575
774
  *
576
775
  * Concurrent calls for the same page share one fetch; a page already resident is returned
577
- * without one.
776
+ * without one; a page in quarantine is refused without one.
578
777
  *
579
778
  * @param {number} page_index
580
779
  * @param {number} frame_offset absolute
581
780
  * @param {number} fetch_size
582
781
  * @returns {Promise<VGeoPage>}
782
+ * @throws {Error} asynchronously — when the page is already quarantined, when it fails every
783
+ * attempt and is quarantined by doing so, or when {@link cancel} abandoned it
583
784
  */
584
785
  install(page_index, frame_offset, fetch_size) {
585
786
  const resident = this.#pages.get(page_index);
@@ -594,8 +795,21 @@ export class VGeoContainerReader {
594
795
  return in_flight;
595
796
  }
596
797
 
597
- const fetch = this.#fetch_page(page_index, frame_offset, fetch_size)
598
- .finally(() => this.#pending.delete(page_index));
798
+ const quarantined = this.#quarantined.get(page_index);
799
+
800
+ if (quarantined !== undefined) {
801
+ return Promise.reject(new Error(`page ${page_index} is quarantined: ${quarantined}`));
802
+ }
803
+
804
+ const abort = new AbortController();
805
+
806
+ this.#aborts.set(page_index, abort);
807
+
808
+ const fetch = this.#fetch_page(page_index, frame_offset, fetch_size, abort.signal)
809
+ .finally(() => {
810
+ this.#pending.delete(page_index);
811
+ this.#aborts.delete(page_index);
812
+ });
599
813
 
600
814
  this.#pending.set(page_index, fetch);
601
815
 
@@ -603,13 +817,97 @@ export class VGeoContainerReader {
603
817
  }
604
818
 
605
819
  /**
820
+ * Abandon a fetch that is still in flight.
821
+ *
822
+ * **What a want that goes stale mid-flight should do, decided:** abort it while it can still be
823
+ * aborted, and install it once it cannot. The camera moved, nothing wants the page now, and the
824
+ * request is worth closing — but once the bytes have landed, refusing to install them is the one
825
+ * choice that pays for the fetch *and* throws the page away. So an abort that arrives too late
826
+ * does nothing, the page installs, and it enters residency untouched — which under LRU on
827
+ * last-touched (design §6) makes it the first candidate for eviction if the camera really is
828
+ * done with it, and free if it is not.
829
+ *
830
+ * The pending promise rejects, so a caller awaiting it sees the abort rather than a page. A
831
+ * cancelled page is **not** quarantined: nothing was wrong with it.
832
+ *
833
+ * @param {number} page_index
834
+ * @returns {boolean} whether there was a fetch to abandon
835
+ */
836
+ cancel(page_index) {
837
+ const abort = this.#aborts.get(page_index);
838
+
839
+ if (abort === undefined) {
840
+ return false;
841
+ }
842
+
843
+ abort.abort(new Error(`page ${page_index}: the fetch was cancelled`));
844
+
845
+ return true;
846
+ }
847
+
848
+ /**
849
+ * Abandon every fetch in flight.
850
+ * @returns {number} how many were abandoned
851
+ */
852
+ cancel_all() {
853
+ let cancelled = 0;
854
+
855
+ for (const page_index of Array.from(this.#aborts.keys())) {
856
+ if (this.cancel(page_index)) {
857
+ cancelled++;
858
+ }
859
+ }
860
+
861
+ return cancelled;
862
+ }
863
+
864
+ /**
865
+ * One page, retried a bounded number of times and then quarantined — §10.
866
+ *
606
867
  * @param {number} page_index
607
868
  * @param {number} frame_offset
608
869
  * @param {number} fetch_size
870
+ * @param {AbortSignal} signal
609
871
  * @returns {Promise<VGeoPage>}
610
872
  */
611
- async #fetch_page(page_index, frame_offset, fetch_size) {
612
- const bytes = await this.#read(frame_offset, fetch_size);
873
+ async #fetch_page(page_index, frame_offset, fetch_size, signal) {
874
+ const retries = this.#options.fetch_retries;
875
+
876
+ for (let attempt = 0; ; attempt++) {
877
+ try {
878
+ return await this.#read_page(page_index, frame_offset, fetch_size, signal);
879
+ } catch (e) {
880
+ if (signal.aborted) {
881
+ // cancelled rather than failed: nothing is wrong with this page
882
+ throw signal.reason ?? e;
883
+ }
884
+
885
+ const reason = String(e?.message ?? e);
886
+
887
+ if (attempt >= retries) {
888
+ this.#quarantined.set(page_index, reason);
889
+
890
+ throw new Error(
891
+ `page ${page_index} is quarantined after ${attempt + 1} attempt(s): ${reason}`
892
+ );
893
+ }
894
+
895
+ await delay(this.#options.fetch_retry_backoff_ms * (1 << attempt), signal);
896
+ }
897
+ }
898
+ }
899
+
900
+ /**
901
+ * One attempt: the bytes, the checks, the decode, and the residency it becomes.
902
+ *
903
+ * @param {number} page_index
904
+ * @param {number} frame_offset
905
+ * @param {number} fetch_size
906
+ * @param {AbortSignal} signal
907
+ * @returns {Promise<VGeoPage>}
908
+ */
909
+ async #read_page(page_index, frame_offset, fetch_size, signal) {
910
+ const bytes = await this.#read(frame_offset, fetch_size, signal);
613
911
 
614
912
  if (this.#options.verify_frames) {
615
913
  this.#verify_frame(bytes, page_index);
@@ -652,25 +950,58 @@ export class VGeoContainerReader {
652
950
  * clamped cluster projected — so a caller that stops early has fetched the pages that would fix
653
951
  * the most visible coarseness, not an arbitrary prefix of the list.
654
952
  *
953
+ * **A page that will not arrive does not take the batch down.** One flaky edge node, or one
954
+ * corrupt frame out of a thousand assets' worth of pages, must cost exactly that page: it is
955
+ * retried, then quarantined, then skipped, and what comes back is what did install. *Which*
956
+ * pages failed is durable rather than a per-call return — see {@link quarantine} — because a
957
+ * failure that lived only in one call's result is one the next call would repeat.
958
+ *
959
+ * The concurrency bound here is this reader's own. A runtime holding many assets wants a bound
960
+ * across all of them and should install through `VGeoResidencyManager`, which admits every
961
+ * registered reader's fetches through one scheduler: eight readers calling this at the default
962
+ * were measured issuing 48 concurrent range requests.
963
+ *
655
964
  * @param {VGeoPageWant[]} wants
656
965
  * @param {number} [limit] most pages to install; the rest are left for the next traversal to
657
966
  * want again, which it will, from a cut taken with everything this call did install
967
+ * @param {AbortSignal} [signal] stops the batch taking new wants, and abandons what is in flight
658
968
  * @returns {Promise<VGeoPage[]>} the pages installed, in completion order
659
969
  */
660
- async install_wants(wants, limit = wants.length) {
970
+ async install_wants(wants, limit = wants.length, signal = undefined) {
661
971
  // Sorted before it is cut, not after: a limit applied to the caller's order would install an
662
972
  // arbitrary prefix and the ranking would have bought nothing.
663
973
  const queue = wants.slice().sort((a, b) => b.priority - a.priority).slice(0, limit);
664
974
 
665
975
  const installed = [];
666
976
 
977
+ /**
978
+ * Pages *this* batch has on the wire, so that aborting it abandons those and not whatever
979
+ * another caller started. A signal belongs to one want list; {@link cancel_all} belongs to
980
+ * the reader, and using the second for the first would make two concurrent batches able to
981
+ * cancel each other.
982
+ * @type {Set<number>}
983
+ */
984
+ const mine = new Set();
985
+
667
986
  let next = 0;
668
987
 
669
988
  const worker = async () => {
670
989
  while (next < queue.length) {
990
+ if (signal !== undefined && signal.aborted) {
991
+ return;
992
+ }
993
+
671
994
  const want = queue[next++];
672
995
 
673
- installed.push(await this.install_want(want));
996
+ mine.add(want.page_index);
997
+
998
+ try {
999
+ installed.push(await this.install_want(want));
1000
+ } catch {
1001
+ // quarantined, or cancelled — either way, not this batch's business
1002
+ } finally {
1003
+ mine.delete(want.page_index);
1004
+ }
674
1005
  }
675
1006
  };
676
1007
 
@@ -682,7 +1013,19 @@ export class VGeoContainerReader {
682
1013
  workers.push(worker());
683
1014
  }
684
1015
 
685
- await Promise.all(workers);
1016
+ const abandon = () => {
1017
+ for (const page_index of mine) {
1018
+ this.cancel(page_index);
1019
+ }
1020
+ };
1021
+
1022
+ signal?.addEventListener('abort', abandon, { once: true });
1023
+
1024
+ try {
1025
+ await Promise.all(workers);
1026
+ } finally {
1027
+ signal?.removeEventListener('abort', abandon);
1028
+ }
686
1029
 
687
1030
  return installed;
688
1031
  }
@@ -692,7 +1035,8 @@ export class VGeoContainerReader {
692
1035
  *
693
1036
  * The eviction *rule* — a page is evictable only while it has no resident child pages, and the
694
1037
  * root is pinned (§10) — is a residency policy and belongs to whatever is managing the budget,
695
- * not here. What this does is the dropping.
1038
+ * not here. What this does is the dropping; {@link resident_child_count} is the lookup the rule
1039
+ * is written in terms of.
696
1040
  *
697
1041
  * @param {number} page_index
698
1042
  * @returns {boolean} whether a page was there to drop
@@ -796,47 +1140,38 @@ export class VGeoContainerReader {
796
1140
  /**
797
1141
  * Install every page the directory lists.
798
1142
  *
799
- * The opposite of what this reader is for, and exactly what a tool wants when the question is
800
- * about the file rather than about a view of it — the pages no cut reaches are the ones a bug
801
- * hides in.
802
- *
1143
+ * @deprecated it reads the whole file, and a renderer holding a reader must not be able to reach
1144
+ * that from the reader. Call `vgeo_install_all_pages` in `tool/` instead, which is a tool
1145
+ * and says so. Kept as a shim because the playground and the specs are what it is for.
803
1146
  * @param {function(number, number): void} [on_progress] called with pages installed and total
804
1147
  * @returns {Promise<void>}
805
1148
  */
806
1149
  async install_all_pages(on_progress = undefined) {
807
- const directory = await this.read_directory();
808
-
809
- let done = 0;
810
-
811
- for (let index = 0; index < directory.page_count; index++) {
812
- await this.install(index, directory.frame_offset[index], directory.fetch_size[index]);
1150
+ const { vgeo_install_all_pages } = await import('./tool/vgeo_install_all_pages.js');
813
1151
 
814
- done++;
815
-
816
- if (on_progress !== undefined) {
817
- on_progress(done, directory.page_count);
818
- }
819
- }
1152
+ await vgeo_install_all_pages(this, on_progress);
820
1153
  }
821
1154
 
822
1155
  /**
823
1156
  * The whole-file audit — every build invariant of §11, every checksum, and the directory against
824
1157
  * what a frame scan reconstructs.
825
1158
  *
826
- * **This reads the entire file**, which is the one thing the rest of this class exists to avoid,
827
- * and it is why it is a separate call and off by default. It answers a question about whoever
828
- * produced the file; nothing about reading one depends on it having been asked.
829
- *
1159
+ * @deprecated it reads the entire file into one `ArrayBuffer`, which is the one thing the rest
1160
+ * of this class exists to avoid and which a multi-GB asset does not survive. Call
1161
+ * `vgeo_audit_container` in `tool/` instead, which refuses a file too large to audit this
1162
+ * way rather than attempting it. Kept as a shim for the playground and the specs.
830
1163
  * @returns {Promise<VGeoDefect[]>} empty is the only good answer
831
1164
  */
832
1165
  async validate() {
833
- const bytes = await this.#source.read(0, this.#source.byte_length);
834
-
835
- const whole = bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength
836
- ? bytes.buffer
837
- : bytes.slice().buffer;
1166
+ /*
1167
+ Imported here rather than at the top of the file on purpose: a static import would put
1168
+ `vgeo_validate_container` — and the whole-file scan it carries — in the same module graph as
1169
+ every runtime that opens a container. The dynamic import is what makes "a runtime path
1170
+ cannot reach the audit" true of the bundle and not only of the documentation.
1171
+ */
1172
+ const { vgeo_audit_container } = await import('./tool/vgeo_audit_container.js');
838
1173
 
839
- return vgeo_validate_container(BinaryBuffer.fromArrayBuffer(whole));
1174
+ return vgeo_audit_container(this);
840
1175
  }
841
1176
 
842
1177
  /**
@@ -844,21 +1179,55 @@ export class VGeoContainerReader {
844
1179
  *
845
1180
  * @param {number} offset
846
1181
  * @param {number} length
1182
+ * @param {AbortSignal} [signal]
847
1183
  * @returns {Promise<Uint8Array>}
848
1184
  */
849
- #read(offset, length) {
850
- if (offset + length <= this.#head.byteLength) {
851
- return Promise.resolve(this.#head.subarray(offset, offset + length));
1185
+ #read(offset, length, signal = undefined) {
1186
+ const head = this.#head;
1187
+
1188
+ if (offset + length <= head.byteLength) {
1189
+ return Promise.resolve(head.subarray(offset, offset + length));
1190
+ }
1191
+
1192
+ /*
1193
+ The prefix has stopped paying for itself. File order is coarse-first, so a read past the end
1194
+ of the head is a traversal that has walked past the band the head covered, and the pages
1195
+ inside it that were going to be free already were. Holding it from here on holds bytes
1196
+ nothing will read: measured, a 256 KiB prefix over a 22 MB container covered 105 of its
1197
+ 4,000 pages, and once those installed the whole prefix served nothing — while staying
1198
+ outside `resident_bytes` and outside anything `evict` could reach. Across a thousand open
1199
+ assets that was a quarter of a gigabyte no budget could see.
1200
+ */
1201
+ if (head.byteLength > 0) {
1202
+ this.release_head();
852
1203
  }
853
1204
 
854
- return this.#source.read(offset, length);
1205
+ return this.#source.read(offset, length, signal);
1206
+ }
1207
+
1208
+ /**
1209
+ * Drop the speculative head prefix.
1210
+ *
1211
+ * Nothing depends on it afterwards: the header and level table are parsed into {@link header}
1212
+ * before anything else runs, the root page owns its own decoded bytes, and every later read
1213
+ * names a range the source can serve. A page whose frame lay inside the prefix costs one request
1214
+ * if it is wanted after this — the same request it would have cost had the prefix been smaller.
1215
+ *
1216
+ * @returns {number} bytes released
1217
+ */
1218
+ release_head() {
1219
+ const released = this.#head.byteLength;
1220
+
1221
+ this.#head = NO_HEAD;
1222
+
1223
+ return released;
855
1224
  }
856
1225
 
857
1226
  /**
858
1227
  * Extend the kept head prefix to `to` bytes, fetching only the part that is missing.
859
1228
  *
860
- * Only ever at the tail, which is what keeps a cold start from paying for the same bytes twice:
861
- * the header is read once even when the prefix that carried it stopped short of the level table.
1229
+ * Called exactly once, from {@link #open}, before anything else has read a byte — so the copy it
1230
+ * makes happens once per reader, and it never re-copies a prefix that is already in use.
862
1231
  *
863
1232
  * @param {number} to
864
1233
  * @returns {Promise<void>}
@@ -915,3 +1284,41 @@ export class VGeoContainerReader {
915
1284
  }
916
1285
  }
917
1286
  }
1287
+
1288
+ /**
1289
+ * Wait, unless the caller stops wanting to.
1290
+ *
1291
+ * The timer is cleared on abort rather than left to fire, because a backoff between retries of a
1292
+ * page nobody wants any more is exactly the wait a cancelled fetch should not sit through.
1293
+ *
1294
+ * @param {number} ms
1295
+ * @param {AbortSignal} signal
1296
+ * @returns {Promise<void>}
1297
+ */
1298
+ function delay(ms, signal) {
1299
+ if (ms <= 0) {
1300
+ return Promise.resolve();
1301
+ }
1302
+
1303
+ // Checked rather than relied on: `addEventListener` on a signal that has already aborted never
1304
+ // fires, so a wait started on one would run to term with nobody left who wanted it.
1305
+ if (signal.aborted) {
1306
+ return Promise.reject(signal.reason ?? new Error('the wait was cancelled'));
1307
+ }
1308
+
1309
+ return new Promise((resolve, reject) => {
1310
+ const on_abort = () => {
1311
+ clearTimeout(timer);
1312
+
1313
+ reject(signal.reason ?? new Error('the wait was cancelled'));
1314
+ };
1315
+
1316
+ const timer = setTimeout(() => {
1317
+ signal.removeEventListener('abort', on_abort);
1318
+
1319
+ resolve();
1320
+ }, ms);
1321
+
1322
+ signal.addEventListener('abort', on_abort, { once: true });
1323
+ });
1324
+ }