@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
@@ -1,10 +1,20 @@
1
1
  # Virtual Geometry — Design
2
2
 
3
- Status: **draft, pre-implementation**. This document pins
4
- only what is certain; everything else is listed explicitly under *Open*. Runtime home is
3
+ Status: **builder, container and reader implemented; the GPU runtime is not**. This document
4
+ pins only what is certain; everything else is listed explicitly under *Open*. Runtime home is
5
5
  this directory (`shade/renderer/geometry/virtual/`, mirroring `texture/virtual/`); builder
6
6
  lives with `core/geom/3d/topology/struct/binary/` and `core/graph/metis/`.
7
7
 
8
+ What exists, in the order a newcomer meets it: `build/` turns a `Geometry` into a cluster DAG,
9
+ `format/` writes and validates the `.vgeo` container ([VGEO_FORMAT.md](./VGEO_FORMAT.md)),
10
+ `format/read/` reads one back a page at a time with residency and activation, and
11
+ `shade/playground/vgeo_viewer/` puts a clamped CPU cut of a *streaming* container on screen.
12
+ What does not exist is everything §7 describes: the selection buffer, the compute cut pass, the
13
+ VRAM page pool, and the seam through which VG instances enter the meshlet pipeline. The
14
+ playground draws through Shade's direct geometry path instead, which is exactly why it proves
15
+ the DAG and not the pipeline. §10 says which milestone that leaves us in, and the order the work
16
+ actually arrived in.
17
+
8
18
  ## 1. What and why
9
19
 
10
20
  Cluster-DAG continuous LOD (Nanite-family): the builder turns a source mesh into a DAG of
@@ -28,10 +38,14 @@ hands-off principle argues for anyway.
28
38
  - **Mesh structure**: `BinaryTopology` (BMesh-style, binary pools, float32 coordinates),
29
39
  with `bt_mesh_validate`, `bt_merge_vertices_by_distance` (float32 weld quantum floor;
30
40
  its contract requires a short-edge kill/collapse pre-pass — §8), boundary queries.
31
- - **Simplifier**: `bt_mesh_simplify(mesh, target_face_count, restricted_vertices)` —
32
- greedy quadric edge collapse, optimal placement, pinned vertices neither moved nor
33
- removed. Required extensions (all builder-side, none exist yet):
34
- 1. report the max/accumulated quadric residual of a pass (§4);
41
+ - **Simplifier**: `bt_mesh_simplify(mesh, target_face_count, restricted_vertices,
42
+ protected_faces)` — greedy quadric edge collapse, optimal placement, pinned vertices neither
43
+ moved nor removed, protected faces never destroyed. Required extensions (all builder-side;
44
+ three of the four have since landed, and the list is kept because the reasoning is what makes
45
+ the fourth findable):
46
+ 1. ~~report the max/accumulated quadric residual of a pass (§4)~~ **done**. It returns the
47
+ object-space distance the surface moved, measured *before* the cleanup pass — see item 2,
48
+ where that ordering turned out to be most of the fix;
35
49
  2. ~~boundary constraint quadrics~~ **done**. Open and non-manifold edges now carry a
36
50
  constraint plane through the edge perpendicular to its face, so a border can no
37
51
  longer retreat at a recorded cost of zero — which it could, and which the round loop
@@ -43,19 +57,27 @@ hands-off principle argues for anyway.
43
57
  object-space distance rather than a raw residual. A flat sheet still reports exactly
44
58
  zero, which is the other half of the contract. The same mechanism later serves UV
45
59
  seams (§9);
46
- 3. triangle-inversion (flip) rejection — absent today, and pin-forced placement along
47
- locked group perimeters is exactly where greedy QEM folds over;
48
- 4. group-boundary **polyline** preservation — pinning vertices does not by itself
49
- protect the boundary *edges* between them (face-kill paths can strip them); this is
50
- a guarantee to enforce and validate, not a property to assume.
60
+ 3. triangle-inversion (flip) rejection — **still open, and the only one of the four that
61
+ is**. Pin-forced placement along locked group perimeters is exactly where greedy QEM folds
62
+ over. Nothing in `bt_mesh_simplify` tests the sign of a face normal before or after a
63
+ collapse, and no build validation catches a fold, so this is a real gap rather than a
64
+ latent one;
65
+ 4. ~~group-boundary **polyline** preservation~~ **done**. Pinning vertices does not by itself
66
+ protect the boundary *edges* between them: a collapse elsewhere in the fan can take away
67
+ the only face holding one, and the cleanup pass then removes it. `protected_faces` names
68
+ the faces carrying a group's boundary polyline, and `vgeo_build_levels` re-walks every
69
+ boundary edge after the call and throws if one is gone — a missing boundary edge is a crack
70
+ between two LODs that never closes, so the build stops rather than shipping it.
51
71
  - **Partitioner**: the ported METIS — `metis_partition_kway` (CSR in, deterministic via
52
72
  fixed seed, single balance constraint, **unit vertex weights, no contiguity
53
73
  guarantee**) and `cluster_mesh_metis(result, mesh, patch_size)` (face clustering with a
54
74
  hard size ceiling, minimized edge cut, dense renumbering). Disconnected parts are
55
75
  possible and matter (§3).
56
- - **Bounds**: `Miniball` (minimal bounding sphere, iteration-capped);
57
- `compute_triangle_cluster_normal_bounding_cone` is legacy-`TopoMesh`-typed and needs a
58
- `BinaryTopology`/index port before §8 can use it.
76
+ - **Bounds**: `Miniball` (minimal bounding sphere, iteration-capped). That is the whole list —
77
+ **normal cones are not part of this design**. Backface cone culling is declined outright (§9,
78
+ plan §5), so `cone_reserved` stays zero, and
79
+ `bt_face_cluster_compute_normal_bounding_cone` — the `BinaryTopology` port of the old
80
+ `TopoMesh` cone builder — has no consumer and no prospect of one.
59
81
  - **Renderer formats (the real seam)**: meshlets capped at 128 triangles / 128 vertices
60
82
  (8-bit local indices); `MESHLET_METADATA_STRUCT`; the draw record
61
83
  (`MESHLET_DEFINITION_STRUCT` = meshlet index + mesh); bucket pipeline issuing one
@@ -66,13 +88,19 @@ hands-off principle argues for anyway.
66
88
  cluster-local position quantization is new work spanning encode, metadata struct, and
67
89
  every decode shader, not a finished capability.
68
90
  - **Renderer managers (need a VG layer, not reuse as-is)**: `GPUMeshletManager` pools
69
- metadata+data via OffsetAllocator, but its compaction/growth is a CPU-side rebuild —
70
- new buffer pair (transient ~2× VRAM), CPU-retained metadata mirrors, full re-upload;
71
- the GPU intra-copy + address-patch path exists only for clone flushes. Downstream,
72
- meshlet-address migration is an acknowledged unhandled TODO in `GPUGeometryManager`.
73
- Mesh→meshlet expansion is inlined per material bucket at four call sites and assumes
74
- contiguous static per-geometry meshlet ranges — VG's scattered, dynamic residency does
75
- not fit those assumptions (§6, §7 state what replaces them).
91
+ metadata+data via OffsetAllocator. **Growth no longer re-packs**: it appends, files the new
92
+ tail as free space through `OffsetAllocator#grow`, copies each old buffer into the larger one
93
+ at offset 0, and moves no record — so no address is patched and no `changed` fires.
94
+ **Compaction still is a CPU-side rebuild** — new buffer pair (transient ~2× VRAM),
95
+ CPU-retained metadata mirrors, full re-upload — and it is what closes the holes growth leaves
96
+ behind, which under VG's churn is the case that matters rather than the one that got cheaper.
97
+ Downstream, meshlet-address migration is still an acknowledged unhandled TODO in
98
+ `GPUGeometryManager` (it logs a warning and continues); it is now reachable only through
99
+ compaction, which narrows the hole without closing it.
100
+ Mesh→meshlet expansion is one shared FrameGraph helper — `graph_expand_meshes_to_meshlets`,
101
+ called from four rasterization paths — and it reads a geometry's `meshlets_address` and
102
+ `meshlets_count` as one contiguous static range. VG's scattered, dynamic residency does not
103
+ fit that (§6, §7 state what replaces them).
76
104
  - **Readback**: `graph_read_buffer` (MAP_READ scratch from the general buffer pool,
77
105
  `mapAsync`, ≥1 frame latency). The virtual-texture system is the shipped precedent for
78
106
  the whole feedback loop and pins its contract: in-flight readbacks capped, feedback
@@ -84,7 +112,7 @@ Pinned:
84
112
 
85
113
  - **Leaf clusters = meshlets** (≤ 128 triangles), from `cluster_mesh_metis` on the source
86
114
  mesh, followed by a **connected-components split** (metis parts can be disconnected;
87
- disconnected clusters/groups inflate spheres and cones and force the exact LOD-lockstep
115
+ disconnected clusters/groups inflate spheres and force the exact LOD-lockstep
88
116
  over-rendering this design criticizes fixed-32 grouping for).
89
117
  - **The round loop** (not a level sweep): hold a *frontier* of every cluster nothing has
90
118
  merged yet; select a **band** of it by error; partition the band's *cluster adjacency
@@ -238,6 +266,17 @@ Pinned:
238
266
  *before* it is fetched. Below page granularity, each page carries a tree over its own groups
239
267
  (format §6.1–6.2). Cost, measured: 0.3%–1.6% of asset size.
240
268
 
269
+ *What exists.* `playground/vgeo_viewer/select_cut.js` is this rule on the CPU, against a
270
+ partially resident container: descend from the root's parentless groups, refine where
271
+ `project(parent) > τ`, take the cluster where it does not, and stop where the child group is
272
+ not selectable — recording the want that would unblock it. It is the reference the GPU pass
273
+ has to agree with, and it is where the clamp was found to need *selectable* rather than
274
+ *resident* (§6). What it does **not** do is use either accelerator this section just
275
+ described: it descends group by group, and reads neither the child-table traversal metric nor
276
+ the page node tree. Both are written by the builder and checked by the validator, and nothing
277
+ reads either — the largest piece of built-and-unused machinery in the system, and the cut pass
278
+ is what it was built for.
279
+
241
280
  *Per view.* **Virtual geometry is view-dependent by construction.** A cut is selected at one
242
281
  camera's distances and pixel budget; it is the wrong cut for a shadow view, whose light may
243
282
  sit a hundred metres behind the camera, and wrong again for each cascade. What is shared
@@ -300,14 +339,18 @@ Pinned:
300
339
  in the file (coarse-first order is topological); non-root pages hold clusters of a
301
340
  single level band. Build-validated.
302
341
  - **Compression must not break random access.** Transparent HTTP compression does not
303
- compose with range requests, so compression is per-page *inside* the container. Codec
304
- open; `raw` is a valid v1 codec — but note quantization is *new work* (§2), so raw v1
305
- pages are position-heavy.
342
+ compose with range requests, so compression is per-page *inside* the container. Two codecs
343
+ are defined and both shipped — `raw` and a bare LZ4 block — chosen **per frame** rather than
344
+ per file, so a page that would not shrink is stored verbatim and says so. Measured, LZ4 is
345
+ worth 1.12x–1.18x on the Stanford models, and the reason it is not more is the reason to stop
346
+ here: the payload is float32 positions and packed attribute words, and it does not compress.
347
+ Quantization (§2) is the lever, not the codec.
306
348
  - **Coarse-first layout**: pages ordered so a plain prefix read of the file yields a
307
349
  complete coarse asset — progressive load without a feedback loop.
308
350
 
309
- Open: page size; page-internal codec; per-attribute quantization widths (cluster-local
310
- grids); relationship to `MeshletGeometrySerializationAdapter` (extend vs parallel).
351
+ Open: page size; a denser page codec than LZ4, if quantization leaves one worth having;
352
+ per-attribute quantization widths (cluster-local grids); relationship to
353
+ `MeshletGeometrySerializationAdapter` (extend vs parallel).
311
354
 
312
355
  ## 6. Streaming and residency
313
356
 
@@ -319,8 +362,16 @@ Pinned:
319
362
  - **The VRAM pool's hard ceiling is the adapter's `maxStorageBufferBindingSize`**, not
320
363
  free configuration — pools bind as single storage buffers (spec default 128 MiB; Shade
321
364
  requests adapter max, but low-end adapters stay low). Growth must clamp against device
322
- limits with a defined failure policy (today's `grow()` doesn't check); the low-limit
323
- fallback is a smaller pool + higher τ. Multi-buffer sharding is explicit future work.
365
+ limits with a defined failure policy; the low-limit fallback is a smaller pool + higher τ.
366
+ Multi-buffer sharding is explicit future work.
367
+ `GPUMeshletManager.grow` clamps now, having not before — but against **`maxBufferSize`**,
368
+ which is the wrong limit for an arena that binds as one storage buffer. Spec defaults are
369
+ 256 MiB and 128 MiB respectively, so a buffer between the two is creatable and not bindable.
370
+ `GPUDatabase` clamps against the binding limit; `GPUMeshletManager` and
371
+ `GPUGeometrySDFManager` clamp against `maxBufferSize`. A VG pool has to pick deliberately,
372
+ and the reason this is worth stating rather than assuming is that `createBuffer` answers an
373
+ over-limit size with an *invalid buffer* rather than an exception — the failure is silent at
374
+ the point it happens and loud several frames later.
324
375
  - **The VRAM paging mechanism is new work**, not `GPUMeshletManager` as-is (§2): v1
325
376
  intent is **fixed-size page-slot allocation** (no compaction, no CPU metadata mirror,
326
377
  no transient 2× rebuild), plus propagation of address migration to every consumer —
@@ -337,11 +388,52 @@ Pinned:
337
388
  recheck, never a fetch. With the §4 clamped rule, any miss (or resident-but-inactive
338
389
  group) has a coarser stand-in that *is selected* — streaming latency degrades
339
390
  quality, never correctness. No holes, no double-draw, in any arrival order.
391
+ **Implemented**, in `VGeoContainerReader`: a live-reference counter per resident group, a
392
+ reverse index for pages not yet installed, cascades on both install and evict, and
393
+ `is_selectable(page, group)`. The double-draw half of that guarantee is not theoretical —
394
+ clamping on residency instead drew two levels over the same surface on 355 of 576 camera
395
+ positions of a 19-page bunny, up to 10.7% of drawn triangles in a doubly-covered region, and
396
+ it reads as corrupt geometry rather than as a missing page, which is what makes it hard to
397
+ attribute. 21 of that asset's 362 non-root groups have parents in more than one page, which
398
+ is all it takes.
340
399
  - **Feedback carries touches as well as wants.** Wants = pages whose refinement was
341
400
  blocked; touches = pages actually selected this frame. Eviction is LRU on
342
401
  **last-touched** with a protect window ≥ feedback latency — LRU on last-*wanted* would
343
402
  freeze stamps at fetch time and evict exactly what is on screen (the classic
344
403
  fallback-chain bug; the VT system already keys on touch for this reason).
404
+ **Implemented**, in `virtual/residency/`: `VGeoResidencyManager` holds the RAM tier's byte budget
405
+ across every registered reader, `touch(reader, page)` takes the stamp, `advance_frame()` moves the
406
+ protect window, and `evict_to_budget()` drops on the format §10 rule ordered by coldest touch.
407
+ It is a sibling of `format/` and not part of it, because a container cannot answer a question
408
+ about a scene. The reader stayed mechanics: it reports `resident_bytes` and enforces nothing, and
409
+ the manager is written entirely against its public surface.
410
+ The parenthetical above is load-bearing and should be followed rather than taken on trust:
411
+ `VTPhysicalCache` is the working implementation of this same rule, with a `#last_touch`
412
+ `Uint32Array` per slot, `touch`/`last_touch`, a lock flag for slots that may never be evicted,
413
+ and `find_eviction_candidate(current_frame, protect_window)` whose doc gives the window the
414
+ identical justification. The similarity is therefore convergent *by instruction* — this bullet
415
+ told the implementation to key on touch because VT does — and the only real difference is that
416
+ VT budgets a count of fixed-size slots, which is what lets it pick by linear scan at ≤ ~1k
417
+ slots. Budgeting **bytes** across **several** readers, and admitting a page against that budget
418
+ before the fetch out of the `decoded_size` its parent already carries, is the part with no
419
+ precedent in this tree; so are per-page retry-into-quarantine and per-fetch `AbortSignal`.
420
+ - **The fetch cap has to be shared, and per-reader caps do not compose.**
421
+ `VGeoReadOptions.max_concurrent_fetches` bounds one reader, which is all a reader can see: four
422
+ readers at a cap of four each were measured issuing sixteen concurrent range requests, eight at
423
+ the default six issuing forty-eight. `VGeoResidencyManager` holds one total across every reader
424
+ as one priority queue, so a want's priority — pixels of projected error, comparable across assets
425
+ by construction — decides globally rather than within whichever reader asked first.
426
+ A shared cap is not itself a new idea here: `AssetManager.load_concurrency` bounds concurrent
427
+ top-level loads for the same reason and orders its queue through a negated priority into a
428
+ `BinaryHeap`, and `VirtualTextureManager` bounds `max_concurrent_loads` at 32. Writing that
429
+ negation a second time is what moved the queue out of this directory: it is
430
+ `core/process/ConcurrencyGate.js` now — a free-standing "run at most N of these at a time,
431
+ highest priority first" — and the `VGeoFetchScheduler` that shipped first was folded into it.
432
+ The other callers kept their own queues, each recording why where its cap is declared. What none
433
+ of them can do is compare two containers' wants — VT sorts its queue coarse-mip first, a proxy
434
+ that only orders within one texture, and `AssetRequestScope.final_priority` is hierarchical
435
+ rather than a common scale. The cut hands this one, so the global order is the half that is
436
+ actually new.
345
437
  - **Wants are occlusion-aware in v1** (HZB test in the want path, or previous-frame
346
438
  feedback à la VT — which is occlusion-aware by construction). Frustum-only wants plus
347
439
  error-priority would page in and pin entire occluded interiors, breaking both budgets.
@@ -364,36 +456,55 @@ Pinned:
364
456
  child pages are resident in that tier (checked against its own child table —
365
457
  leaves-first order emerges from the rule).
366
458
 
367
- Open: page store API choice; prefetch heuristics (camera velocity); RAM eviction details;
368
- priority scoring and hysteresis constants.
459
+ Open: page store API choice; prefetch heuristics (camera velocity); priority scoring and hysteresis
460
+ constants; the in-flight pin, which is now blocked on a `VGeoPageWant` that names the page it was
461
+ discovered *through* rather than on nobody owning the budget (format §10 — it is an efficiency item,
462
+ because activation is safe at every arrival order). RAM eviction is no longer open; the numbers in
463
+ `VGeoResidencyOptions` are defaults to be overridden by whoever knows the device, not measurements.
369
464
 
370
465
  ## 7. Runtime pipeline (per frame)
371
466
 
372
467
  1. Instance-level culling — existing.
373
- 2. **VG cut pass (new, compute)**: evaluates the §4 rule over the **VG selection buffer**
374
- — a VG-owned, densely maintained table of resident clusters (two (sphere, error)
375
- pairs, geometry id, own-page and child-page/group indices, child-group selectable
376
- bit — the page indices are what touch and want emission stamp), plus per-geometry
377
- resident-cluster index tables; expansion over visible instances via prefix sums.
378
- Emits meshlet draw records (existing record type) and wants/touches. VG carries
379
- geometry only — materials are orthogonal and engine-side: a VG instance binds one
380
- geometry to one material through its mesh exactly like any other geometry (a
381
- multi-material source splits into one geometry per material upstream of the
382
- builder), so the mesh-keyed material buckets downstream stay unchanged. Cost is
383
- `O(Σ over visible instances of resident(geometry))` — per-instance, not per-pool;
384
- the existing metadata buffer is *not* iterable for this (freed slices hold garbage,
385
- and it carries no error pairs), which is why the selection buffer exists.
468
+ 2. **VG cut pass (new, compute)**: a §4 traversal, one per view. It descends the page DAG
469
+ and, inside a page, that page's node tree, from a queue seeded with the root cluster of
470
+ each visible instance; a node or cluster fine enough for τ terminates the descent, and so
471
+ does one whose child group is not selectable — which emits a want. It emits meshlet draw
472
+ records (existing record type) and wants/touches. VG carries geometry only — materials are
473
+ orthogonal and engine-side: a VG instance binds one geometry to one material through its
474
+ mesh exactly like any other geometry (a multi-material source splits into one geometry per
475
+ material upstream of the builder), so the mesh-keyed material buckets downstream stay
476
+ unchanged.
477
+
478
+ What it reads is the **VG selection buffer** — a VG-owned, densely maintained table of
479
+ resident clusters (two (sphere, error) pairs, geometry id, own-page and child-page/group
480
+ indices, child-group selectable bit — the page indices are what touch and want emission
481
+ stamp), plus per-geometry resident-cluster index tables. The existing metadata buffer is
482
+ *not* usable for this (freed slices hold garbage, and it carries no error pairs), which is
483
+ why the selection buffer exists.
484
+
485
+ Superseded: this step previously specified a **flat** evaluation of the rule over every
486
+ row of that buffer, at a cost of `O(Σ over visible instances of resident(geometry))`. §4
487
+ replaced it, and the reason is worth keeping: that cost is the resident set rather than the
488
+ cut, and it is the resident set that grows without bound at the scale this design targets.
489
+ The buffer survives the change; the sweep over it does not.
386
490
  3. Existing pipeline: HZB meshlet filtering (two-pass, per view — this is where frustum
387
491
  and occlusion happen), material bucket sort, indirect draws, G-buffer, deferred
388
492
  shading. The *record formats* flow unchanged; the *injection point* is new work — the
389
- current mesh→meshlet expansion is inlined per material bucket and assumes contiguous
390
- static meshlet ranges, so VG instances enter through their own expansion step (exact
391
- seam fixed in M0/M1).
392
-
393
- Open: flat evaluation vs hierarchy-accelerated selection once resident counts are large
394
- (decide on measurement — prior-implementation data says selection is cheap; triangle
395
- throughput and screen-size uniformity dominate); shadow views (v1 reuses the
396
- one cut per view per §4, sharing setup rather than results); DRS τ semantics.
493
+ current mesh→meshlet expansion (`graph_expand_meshes_to_meshlets`, one helper shared by
494
+ four rasterization paths) reads one contiguous static meshlet range per geometry, so VG
495
+ instances enter through their own expansion step. M0 was where that seam was to be fixed
496
+ and it was not (§10); it is the first thing M1 has to do.
497
+
498
+ Open: shadow views (v1 takes the one cut per view of §4, sharing setup rather than results,
499
+ and a proxy where a view does not justify a cut); DRS τ semantics; the traversal's GPU shape —
500
+ persistent threads over a queue, per nvpro, against what WebGPU without 64-bit atomics actually
501
+ makes affordable.
502
+
503
+ Settled since: **flat evaluation versus a hierarchy is no longer a measurement to take.** §4
504
+ pins the traversal, and the hierarchy it descends is already written into every built asset
505
+ (plan §2.3). The prior-implementation data that made flat evaluation look adequate — selection
506
+ cheap, triangle throughput and screen-size uniformity dominant — was taken at that
507
+ implementation's scale, and is not evidence about millions of resident clusters.
397
508
 
398
509
  ## 8. Builder pipeline (offline)
399
510
 
@@ -402,9 +513,12 @@ Pinned:
402
513
  - Stages: load → short-edge pre-pass (`bt_mesh_kill_short_edges` on soups / edge collapse
403
514
  on closed surfaces — required by the weld contract) → weld
404
515
  (`bt_merge_vertices_by_distance`; float32 quantum floor constrains tolerance) →
405
- `bt_mesh_validate` → level-0 clustering (+ CC split) → the §3 level loop → per-cluster
406
- encode (miniball sphere, normal cone, quantized attributes) → page assignment
407
- (level-banded, DAG + spatial locality, acyclicity check) → container emit.
516
+ `bt_mesh_validate` → level-0 clustering (+ CC split) → the §3 round loop → per-cluster
517
+ encode → page assignment (level-banded, DAG + spatial locality, acyclicity check) →
518
+ container emit. The encode is the miniball sphere, the AABB, and `SHADE_MESHLET_V1`
519
+ attributes at their shipped widths (format §8): `cone_reserved` is zero because cone culling
520
+ is declined (§9), and positions are raw float32 because quantization is a `payload_encoding`
521
+ that does not exist yet.
408
522
  - **The weld tolerance is scale-relative**, a fraction of each geometry's bounding-box
409
523
  diagonal (`bt_mesh_relative_merge_distance`), resolved once off the source's bounds
410
524
  before any pass has removed anything. A conversion is many geometries at many sizes and
@@ -446,13 +560,19 @@ canonicalizing them is a preprocessor's job).
446
560
 
447
561
  ## 9. Non-goals (v1)
448
562
 
563
+ - **Backface normal-cone culling — declined outright, not deferred.** The two-pass HZB already
564
+ rejects what a cone would, on evidence rather than on a bound; see plan §5 for the mechanism
565
+ and the reports behind it. `cone_reserved` stays a zeroed reserved field (format §7). This is
566
+ the one entry here that is not a "v1" qualifier.
449
567
  - Skinned / morphing / WPO meshes through the VG path (static geometry only).
450
568
  - Compute/software rasterization — WebGPU lacks portable 64-bit atomics; the hardware
451
569
  raster path through the existing pipeline is the only raster path. Deferred G-buffer
452
570
  shading already bounds the overshading cost of small triangles.
453
571
  - Ray tracing over the VG cut (`blas_from_meshlets` exists for static meshes; VG×RT is
454
572
  future work).
455
- - One cut per view, and proxy meshes for views that do not justify one (§4; plan §3.3).
573
+ - *Sharing* one cut between views. §4 pins a cut per view and a proxy mesh where a view does
574
+ not justify one (plan §3.2, §3.3); what v1 does not attempt is a single cut made to serve the
575
+ camera and a shadow cascade at once, which was this document's earlier position and is wrong.
456
576
  - Attribute-quadric placement and UV-seam constraint planes (the boundary-plane-penalty
457
577
  mechanism §2 requires for open boundaries is the same one UV seams will use; v1
458
578
  targets position/normal-fidelity content).
@@ -461,20 +581,40 @@ canonicalizing them is a preprocessor's job).
461
581
 
462
582
  ## 10. Milestones
463
583
 
464
- - **M0 — DAG proof**: builder (in-memory, no format) → flat CPU-side cut → existing
465
- meshlet pipeline draws it in a playground; the VG expansion seam is fixed here.
584
+ These have not been taken in order. The format and the RAM-tier pager were built before the
585
+ GPU cut, because the container is what the builder had to produce anyway and a reader is what
586
+ proves it; the consequence is that M0's one *engine*-facing criterion — the expansion seam —
587
+ is the piece still missing, and M1 is where the work now starts.
588
+
589
+ - **M0 — DAG proof** — *the DAG half is done; the engine half is not*: builder (in-memory, no
590
+ format) → flat CPU-side cut → existing meshlet pipeline draws it in a playground; the VG
591
+ expansion seam is fixed here.
466
592
  Acceptance: crack-free under τ sweep and flythrough (incl. group-boundary polyline and
467
593
  flip validation); drawn-triangle count tracks resolution, not source size; §3 build
468
594
  validations green.
595
+ **Done**: the builder; a CPU cut (`playground/vgeo_viewer/select_cut.js`) that is a traversal
596
+ rather than the flat pass planned here, and that runs against a partially resident container,
597
+ which was M2's job; τ-sweep watertightness as a spec (`build/vgeo_cut.spec.js` counts edges
598
+ used by one triangle instead of two, at a spread of thresholds, and wants zero); the
599
+ group-boundary polyline guarantee, enforced by the builder rather than merely validated.
600
+ **Not done**: the expansion seam — the playground draws through Shade's direct geometry path
601
+ (a `DynamicMesh` from raw attributes, no meshlet encode), so nothing has yet entered the
602
+ meshlet pipeline through a VG-shaped door. Flip validation is not done either (§2, extension
603
+ 3).
469
604
  - **M1 — GPU cut**: selection buffer + compute cut pass; wants/touches readback wired
470
605
  (still fully resident). Acceptance: parity with M0, selection cost measured
471
606
  (flat-vs-hierarchy decision data).
472
- - **M2 — format + streaming**: `.vgeo` container, pager, residency tiers with the §6
473
- invariants (page-slot pool, address-migration propagation, occlusion-aware wants,
474
- touch-LRU, in-flight pinning), budgets, eviction. Position quantization lands here
475
- (encode + struct + decode shaders). Acceptance: cold-load TTFP = header + root page
476
- regardless of asset size; streaming never produces holes; page size/codec chosen on
477
- measurement.
607
+ - **M2 — format + streaming** — *the RAM tier landed early*: `.vgeo` container, pager,
608
+ residency tiers with the §6 invariants (page-slot pool, address-migration propagation,
609
+ occlusion-aware wants, touch-LRU, in-flight pinning), budgets, eviction. Position
610
+ quantization lands here (encode + struct + decode shaders). Acceptance: cold-load TTFP =
611
+ header + root page regardless of asset size; streaming never produces holes; page
612
+ size/codec chosen on measurement.
613
+ **Done ahead of M1**: the container and its validator; the RAM-tier pager with residency,
614
+ activation and eviction (`format/read/`); TTFP, which is header + level table + root page and
615
+ measured to hold on a 27M-triangle asset; the page codec, chosen on measurement (§5).
616
+ **Still M2's**: the VRAM page-slot pool, address-migration propagation, occlusion-aware
617
+ wants, touch-LRU, in-flight pinning, budgets, and position quantization.
478
618
  - **M3 — hardening**: prefetch, disk page store decision, budget defaults, low-limit
479
619
  adapters (τ fallback), stress on CAD-class input; browser builder-harness proof
480
620
  (build a mid-size asset in-tab).
@@ -6,8 +6,10 @@ This file holds what is *next*, and — more usefully — the reasoning and the
6
6
  ideas that are accepted but not yet scheduled, so that picking one up does not mean
7
7
  rediscovering it.
8
8
 
9
- Current scope is **the format and the builder**. There is no runtime yet; everything under
10
- §3 waits on one.
9
+ Current scope is **the GPU runtime**. The builder, the container and an incremental reader are
10
+ built — design §10 says which milestone that leaves us in and in what order the work arrived —
11
+ so §3 no longer waits on "a runtime" as a whole. Each item there now names what is actually
12
+ blocking it.
11
13
 
12
14
  Items carry their provenance. `[M §x]` is a section of
13
15
  [MICRON_COMPARISON_2026_08_20.md](./MICRON_COMPARISON_2026_08_20.md), the review of the
@@ -38,8 +40,49 @@ accepted-and-waiting one.
38
40
  an error this file previously made.
39
41
  - **Boundary constraint quadrics** — see §4.1, which was the blocking item.
40
42
  - **A partitioning fallback** `[M §3.3]`, `[V §3.2]` — see §2.1.
41
-
42
- ## 2. Next, in the current scope
43
+ - **An incremental reader, and residency with it** — `format/read/`. A container opens in its
44
+ header, level table and root frame; after that a page arrives only because something asked for
45
+ it, from exactly the byte range the page pointing at it wrote down. `VGeoContainerReader`
46
+ holds residency, activation and eviction; `VGeoByteSource` is the seam — an `ArrayBuffer`, a
47
+ dropped `Blob`, or HTTP ranges that refuse a `200` where they asked for a range. The
48
+ whole-file audit is a tool in `format/read/tool/` and nothing on the runtime path can reach it;
49
+ format §4/§5's reject rules always run.
50
+ VGEO_FORMAT §10 marks, requirement by requirement, what this covers and what it leaves to a
51
+ budget owner.
52
+ - **The reader hardened for 1000 assets, and the budget owner it was waiting for** —
53
+ `virtual/residency/`. Measured first, and three of the audit's suspicions did not survive the
54
+ measurement, which is the more useful half of the result: the per-page allocation the reader adds
55
+ on top of a decoded page is 6.4–11.9 KB, which is 1.2% of a 544 KB page and not worth a rewrite;
56
+ the reverse-reference index tracks the residency frontier rather than the session's history, so it
57
+ does not leak; and the pinned root page is one cluster by invariant §11.7, measured at 1,472 and
58
+ 1,732 B, so a thousand of them is under 2 MB and no builder-side cap is wanted. What *was* real:
59
+ a 256 KiB head prefix retained for the reader's life outside every budget (released now, and the
60
+ default cut to 64 KiB), no retry/backoff/quarantine so one bad page threw (now §10's full
61
+ sequence, with telemetry), no cancellation at all (now a per-read `AbortSignal` end to end), and a
62
+ whole-file audit and install-every-page sweep reachable from a reader a renderer holds (now tools,
63
+ with a 1 GiB bound and a message instead of an allocator failure). Above the reader,
64
+ `VGeoResidencyManager` owns the byte budget, the touch stamps, the eviction order and — through
65
+ one `core/process/ConcurrencyGate` — one fetch cap across every reader, because a per-reader cap
66
+ does not compose, measured at sixteen concurrent requests from four readers capped at four.
67
+ - **Activation, and the double-draw it prevents** — format §10's selectable invariant, as one
68
+ live-reference counter per resident group with cascades on install and evict. The clamp reads
69
+ *selectable*, not *resident*: a group whose parents are split across pages must not be refined
70
+ into through the parents that happen to have arrived. Measured on a 19-page bunny, 21 of whose
71
+ 362 non-root groups have split parents: 355 of 576 camera positions double-drew, up to 10.7%
72
+ of drawn triangles, before. Zero after. It reads as corrupt geometry rather than as a missing
73
+ page, which is why the tool that found it could not name it.
74
+ - **A CPU cut against a partially resident container** — `playground/vgeo_viewer/`.
75
+ `select_cut.js` is the runtime's rule rather than a debug approximation of it; the viewer is
76
+ what makes clamping, streaming and eviction watchable, and `__state()` is what makes it
77
+ assertable from a headless browser. It draws through Shade's direct geometry path, not the
78
+ meshlet pipeline — so it proves the DAG and the format, and proves nothing about the engine
79
+ seam.
80
+
81
+ ## 2. Next
82
+
83
+ §2.1 and §2.2 both landed. They are kept because their notes are the reasoning behind decisions
84
+ that are now only visible in code, and rediscovering them is the expensive part. **§2.3 is the
85
+ open one**, and it is the one to read if you are about to start on the cut pass.
43
86
 
44
87
  ### 2.1 A partitioning fallback for when METIS fails `[M §3.3]`, `[V §3.2]` — LANDED
45
88
 
@@ -161,10 +204,38 @@ instance count first and from fan-out immediately after — measured out-degree
161
204
  the middle bands of the 98k sphere. It bites only for a single instance in close-up, and only
162
205
  for the first few bands, which are also the cheapest to walk.
163
206
 
164
- ## 3. Waiting on a runtime
207
+ ### 2.3 Nothing reads the traversal metric or the node tree
208
+
209
+ §2.2 built both, and the format specifies both: every child-table entry carries the child page's
210
+ LOD-metric sphere and error range (VGEO_FORMAT §5, §6.2), and every page carries a fanout-4 tree
211
+ over its own groups (§6.1). The writer emits them, `vgeo_validate_container` checks them
212
+ (§11.10–11.11) with corruption tests behind both checks, and **nothing reads either**.
213
+ `VGeoPage` records `node_count` and stops; `select_cut.js` descends group by group.
214
+
215
+ This is not a defect to fix on its own account. Building a consumer for its own sake would fix
216
+ the interface before the pass that has to live with it, and the CPU traversal does not need the
217
+ acceleration — it walks a resident page set on a machine with branches. It is written down here
218
+ for two reasons: because "the validator checks it" reads exactly like "something uses it", and
219
+ because the cost is already paid — 0.3%–1.6% of asset size, on disk, in every asset already
220
+ built, earning nothing.
221
+
222
+ What it buys when the pass arrives, and what to hold it to:
165
223
 
166
- Accepted, documented here so the reasoning is not lost, and out of scope until there is
167
- something to run.
224
+ - **Before the fetch.** A page can be frustum- and LOD-culled from its parent's child-table
225
+ entry, so a want list is exactly the pages that passed the test and are not resident, rather
226
+ than everything a descent happened to reach. Today the only way to learn a page was not wanted
227
+ is to fetch and decode it, which is backwards.
228
+ - **Below the page.** A node discards a region rather than a cluster, and gives the top of the
229
+ descent width it does not otherwise have.
230
+
231
+ Neither is measurable until there is a GPU traversal to measure, which is the honest reason it
232
+ has waited.
233
+
234
+ ## 3. Waiting on a GPU runtime
235
+
236
+ Accepted, documented here so the reasoning is not lost. "Waiting on a runtime" used to mean
237
+ waiting on all of it; a CPU traversal and a RAM-tier pager now exist (§1), so what each item is
238
+ actually blocked on is named individually.
168
239
 
169
240
  ### 3.1 Object-space refinement `[M §2.7]`
170
241
 
@@ -174,6 +245,11 @@ and run every cull and error test in **object space** against untransformed clus
174
245
  One matrix multiply per instance instead of transforming every cluster's bounds to world
175
246
  space. With thousands of instances sharing one geometry that difference is the entire cost.
176
247
 
248
+ **Blocked on**: instances. The CPU traversal already tests in object space — but only because
249
+ the viewer draws one geometry at identity and never had to decide, so the rule is written the
250
+ right way round and the multiply that makes it pay has never been exercised. This becomes real
251
+ work at the same moment the cut pass does.
252
+
177
253
  ### 3.2 Proxy geometry from a DAG cut `[M §2.6]`
178
254
 
179
255
  `Micron/format/micron_build_proxy_geometry.js`: refine from the root with a heap keyed on
@@ -220,8 +296,10 @@ free-gap list maintained on the GPU —
220
296
  `shaders/stream_allocator_unload_groups.comp.glsl`.
221
297
 
222
298
  The mirror-image problem on our side is recorded in design §2: `GPUMeshletManager`'s
223
- compaction and growth is a CPU-side rebuild costing ~2× VRAM transiently, and
224
- meshlet-address migration is an acknowledged unhandled TODO in `GPUGeometryManager`.
299
+ *compaction* is a CPU-side rebuild costing ~2× VRAM transiently, and meshlet-address migration
300
+ is an acknowledged unhandled TODO in `GPUGeometryManager`. Growth has stopped being a rebuild
301
+ since this was written — it appends through `OffsetAllocator#grow` and moves no record — which
302
+ narrows the problem to exactly the case VG churn produces: closing holes, not adding room.
225
303
 
226
304
  ## 4. Was blocking
227
305
 
@@ -274,12 +352,34 @@ attacked:
274
352
  mantissa bits of positions and UVs ahead of its lossless group compression. It keeps the
275
353
  float32 layout, the metadata struct and every decode shader exactly as they are, and simply
276
354
  makes the bytes compress. A fraction of the work of quantization for a real share of the
277
- benefit, and it composes with a page codec, which VGEO_FORMAT §12 still lists as open.
355
+ benefit, and it composes with the page codec, which has since shipped: LZ4 finds only
356
+ 1.12x–1.18x on today's payloads *because* float32 positions give an LZ matcher nothing, and
357
+ dropping mantissa bits is how you give it something.
278
358
  - **Cluster-local position quantization**, as design §2 describes — invasive, spanning
279
359
  encode, the metadata struct and every decode shader.
280
360
 
281
361
  ## 5. Considered and declined
282
362
 
363
+ - **Backface normal-cone culling** `[M §2.3]` — the `cone_reserved` field, a producer for it,
364
+ and the cull pass that would read it. **Declined: the HZB already rejects what a cone would,
365
+ and it does it on evidence rather than on a bound.** On a closed surface backfacing *is*
366
+ occluded, and §7's two-pass HZB sees that; what a cone adds over it is pre-emption by one
367
+ pass, bought with per-cluster data and a per-cluster test in the cull pass, every frame and
368
+ per view. It pays best where cones are tight — flat clusters, which are also the cheapest
369
+ to raster — and degrades exactly where a cut spends its clusters, because a group's cone
370
+ widens toward hemispherical as the levels coarsen. Micron paid for tight cones by scoring
371
+ merges on cone angle (`[M §2.3]`), which is the real price: grouping stops being a purely
372
+ spatial criterion in order to feed a cull that the HZB was already doing.
373
+
374
+ This is not our finding alone, which is why it is a decline rather than an experiment: Epic
375
+ did not ship it in Nanite, meshoptimizer's author argues against the payoff despite the
376
+ library exposing cone data, and Remedy report the same for Northlight. Taken together with
377
+ the mechanism above, there is nothing left to measure.
378
+
379
+ Consequences, so that nobody re-derives them: `cone_reserved` stays a zeroed reserved u32
380
+ (format §7) — reclaiming 4 bytes of 112 is not worth a `format_version` move, and the page
381
+ codec compresses a constant-zero column away. `bt_face_cluster_compute_normal_bounding_cone`
382
+ has no consumer and no prospect of one.
283
383
  - **Fixed-size residency slots** `[M §2.5]`. Micron gives every resident patch one slot of
284
384
  `MICRON_PATCH_SIZE_MAX × 3` vertices with occupancy in a BitSet: no allocator, no
285
385
  fragmentation, no address migration, at the cost of internal fragmentation proportional to
@@ -14,6 +14,11 @@ The build side is a fork of meshoptimizer's single-header `clusterlod.h`
14
14
  > scheduled but not yet built, all live in
15
15
  > [VIRTUAL_GEOMETRY_PLAN.md](./VIRTUAL_GEOMETRY_PLAN.md) — that file is the one to act on.
16
16
  > This one is kept as the analysis behind those decisions.
17
+ >
18
+ > It is a **snapshot dated in its filename** and is not maintained against the code. Statements
19
+ > about meep that have since gone stale are corrected inline in block quotes like this one,
20
+ > rather than edited away — the argument that was made at the time is the reason the decision
21
+ > was taken, and rewriting it hides that.
17
22
 
18
23
  ## 1. Where it confirms decisions already made
19
24
 
@@ -154,6 +159,13 @@ cases where it currently gives up — and those cases are real: the boundary-qua
154
159
  design §2 means open geometry reduces at recorded-zero error today, which is worse than
155
160
  reducing at an inflated one.
156
161
 
162
+ > **Since this was written**: boundary constraint quadrics landed (plan §4.1), so that closing
163
+ > argument is spent — open geometry no longer reduces at recorded-zero error, and the choice is
164
+ > no longer between an honest inflated error and a dishonest zero. The case for a middle rung
165
+ > survives it, but it is now the plain one: a group that cannot be reduced is carried forward
166
+ > untouched, and enough of those is what makes a build fail to converge and throw. Plan §2.1
167
+ > records that the sloppy-mode escalation is *safe* to build now for the same reason.
168
+
157
169
  ### 3.3 A minimum cluster size
158
170
 
159
171
  `meshopt_buildMeshletsFlex` takes `min_triangles` as well as `max_triangles`, so a cluster
@@ -163,6 +175,13 @@ meep's splitter caps and never floors. Measured fill on the current builder runs
163
175
  tris/cluster at the leaves down to 67 at the coarse levels. A floor, enforced by merging
164
176
  under-filled pieces the way `pack_pieces` already does inside a group, would close that.
165
177
 
178
+ > **Since this was written**: the leaf half landed (plan §1). A group of leaves with room to
179
+ > spare between them is *re-cut* into fewer and fuller leaves rather than simplified —
180
+ > `try_repack` in `vgeo_build_levels.js` — and the re-cut **replaces** them rather than
181
+ > becoming their parent, because a step of zero error is a step the cut rule can never take.
182
+ > Only leaves: above them the same re-cut would orphan a subtree. So the 120-at-the-leaves half
183
+ > of that measurement is addressed and the 67-at-the-coarse-levels half still stands.
184
+
166
185
  ### 3.4 Spatial variants at every partitioning step
167
186
 
168
187
  `clusterize()` picks `meshopt_buildMeshletsSpatial()` or `meshopt_buildMeshletsFlex()` on