@woosh/meep-engine 3.28.0 → 3.29.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 (482) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/binary/BitSet.d.ts +16 -0
  4. package/src/core/binary/BitSet.d.ts.map +1 -1
  5. package/src/core/binary/BitSet.js +159 -49
  6. package/src/core/binary/allocator/OffsetAllocator.d.ts +48 -0
  7. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  8. package/src/core/binary/allocator/OffsetAllocator.js +208 -9
  9. package/src/core/binary/compression/compress_bytes.d.ts +20 -0
  10. package/src/core/binary/compression/compress_bytes.d.ts.map +1 -0
  11. package/src/core/binary/compression/compress_bytes.js +35 -0
  12. package/src/core/binary/compression/decompress_bytes_into.d.ts +30 -0
  13. package/src/core/binary/compression/decompress_bytes_into.d.ts.map +1 -0
  14. package/src/core/binary/compression/decompress_bytes_into.js +87 -0
  15. package/src/core/binary/compression/deflate_decompress_bound.d.ts +27 -0
  16. package/src/core/binary/compression/deflate_decompress_bound.d.ts.map +1 -0
  17. package/src/core/binary/compression/deflate_decompress_bound.js +32 -0
  18. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts +24 -0
  19. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts.map +1 -0
  20. package/src/core/geom/3d/quadric/quadric_attribute_add.js +31 -0
  21. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts +46 -0
  22. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts.map +1 -0
  23. package/src/core/geom/3d/quadric/quadric_attribute_reduce.js +104 -0
  24. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts +62 -0
  25. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts.map +1 -0
  26. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.js +158 -0
  27. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts +21 -0
  28. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts.map +1 -0
  29. package/src/core/geom/3d/quadric/quadric_attribute_size.js +24 -0
  30. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts +34 -0
  31. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts.map +1 -0
  32. package/src/core/geom/3d/quadric/quadric_attribute_solve.js +55 -0
  33. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts +28 -0
  34. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts.map +1 -0
  35. package/src/core/geom/3d/quadric/quadric_attribute_sub.js +35 -0
  36. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts +15 -0
  37. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts.map +1 -1
  38. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.js +35 -0
  39. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts +93 -0
  40. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts.map +1 -0
  41. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.js +128 -0
  42. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts +38 -0
  43. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts.map +1 -0
  44. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.js +113 -0
  45. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +47 -1
  46. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  47. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +677 -8
  48. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts +22 -0
  49. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts.map +1 -0
  50. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.js +46 -0
  51. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts +19 -0
  52. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts.map +1 -0
  53. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.js +49 -0
  54. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts +63 -0
  55. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts.map +1 -0
  56. package/src/core/graph/layout/layered/BoxLayoutOptions.js +80 -0
  57. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.d.ts +35 -0
  58. package/src/core/graph/layout/layered/LayoutBoxGraph.d.ts.map +1 -0
  59. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.js +53 -1
  60. package/src/core/graph/layout/layered/layout_assign_coordinates.d.ts.map +1 -0
  61. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.js +72 -68
  62. package/src/core/graph/layout/layered/layout_assign_layers.d.ts.map +1 -0
  63. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.js +4 -2
  64. package/src/core/graph/layout/layered/layout_break_cycles.d.ts.map +1 -0
  65. package/src/core/graph/layout/layered/layout_insert_lanes.d.ts.map +1 -0
  66. package/src/core/graph/layout/layered/layout_order_layers.d.ts.map +1 -0
  67. package/src/core/graph/layout/layered/layout_orient_links.d.ts.map +1 -0
  68. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +7 -51
  69. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -1
  70. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +8 -69
  71. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +1 -1
  72. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +6 -6
  73. package/src/core/model/node-graph/visual/layout/route_grid_build.js +1 -1
  74. package/src/engine/graphics3/DebugDrawSystem.d.ts +7 -5
  75. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  76. package/src/engine/graphics3/DebugDrawSystem.js +206 -203
  77. package/src/engine/graphics3/DecalSystem.d.ts +4 -5
  78. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  79. package/src/engine/graphics3/DecalSystem.js +5 -5
  80. package/src/engine/graphics3/FogOfWarSystem.d.ts +7 -12
  81. package/src/engine/graphics3/FogOfWarSystem.d.ts.map +1 -1
  82. package/src/engine/graphics3/FogOfWarSystem.js +8 -12
  83. package/src/engine/graphics3/prefab/prefab_instantiate.js +230 -230
  84. package/src/shade/RENDERER_CONTRACT.md +12 -7
  85. package/src/shade/playground/caldera/README.md +529 -0
  86. package/src/shade/playground/caldera/caldera_format.d.ts +151 -0
  87. package/src/shade/playground/caldera/caldera_format.d.ts.map +1 -0
  88. package/src/shade/playground/caldera/caldera_format.js +241 -0
  89. package/src/shade/playground/caldera/index.html +126 -0
  90. package/src/shade/playground/caldera/main.d.ts.map +1 -0
  91. package/src/shade/playground/caldera/main.js +1565 -0
  92. package/src/shade/playground/mesh_row_ceiling/README.md +85 -0
  93. package/src/shade/playground/mesh_row_ceiling/index.html +31 -0
  94. package/src/shade/playground/mesh_row_ceiling/main.d.ts +2 -0
  95. package/src/shade/playground/mesh_row_ceiling/main.d.ts.map +1 -0
  96. package/src/shade/playground/mesh_row_ceiling/main.js +389 -0
  97. package/src/shade/playground/vgeo_runtime/README.md +5 -3
  98. package/src/shade/playground/vgeo_runtime/main.js +3 -2
  99. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +3 -3
  100. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -1
  101. package/src/shade/playground/vgeo_runtime/sample_asset.js +3 -3
  102. package/src/shade/playground/vgeo_scene/README.md +103 -24
  103. package/src/shade/playground/vgeo_scene/main.js +525 -24
  104. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  105. package/src/shade/renderer/GraphicsContext.js +6 -0
  106. package/src/shade/renderer/Renderer.d.ts +2 -0
  107. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  108. package/src/shade/renderer/Renderer.js +23 -0
  109. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  110. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +100 -8
  111. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +69 -33
  112. package/src/shade/renderer/extension/RenderExtension.d.ts +15 -18
  113. package/src/shade/renderer/extension/RenderExtension.d.ts.map +1 -1
  114. package/src/shade/renderer/extension/RenderExtension.js +15 -19
  115. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts +5 -5
  116. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts.map +1 -1
  117. package/src/shade/renderer/extension/RenderExtensionRegistry.js +29 -166
  118. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  119. package/src/shade/renderer/geometry/GPUGeometryManager.js +73 -16
  120. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  121. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +18 -3
  122. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +663 -0
  123. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +100 -5
  124. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  125. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +688 -81
  126. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +540 -0
  127. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +155 -64
  128. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +30 -13
  129. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +3 -3
  130. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +358 -0
  131. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +57 -7
  132. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  133. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +63 -9
  134. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts +40 -0
  135. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts.map +1 -0
  136. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.js +24 -0
  137. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts +91 -0
  138. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts.map +1 -0
  139. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.js +91 -0
  140. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts +24 -0
  141. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts.map +1 -0
  142. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.js +25 -0
  143. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts +77 -0
  144. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts.map +1 -0
  145. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.js +84 -0
  146. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +19 -6
  147. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -1
  148. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.js +20 -7
  149. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts +19 -0
  150. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts.map +1 -0
  151. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.js +58 -0
  152. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts +18 -0
  153. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts.map +1 -0
  154. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.js +116 -0
  155. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts +18 -0
  156. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -0
  157. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +545 -0
  158. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts +16 -0
  159. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -0
  160. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +110 -0
  161. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +2 -27
  162. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -1
  163. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.js +120 -1564
  164. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts +26 -0
  165. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts.map +1 -0
  166. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.js +123 -0
  167. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts +26 -0
  168. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts.map +1 -0
  169. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.js +78 -0
  170. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts +24 -0
  171. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -0
  172. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +337 -0
  173. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts +29 -0
  174. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -0
  175. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +147 -0
  176. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts +37 -0
  177. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts.map +1 -0
  178. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.js +243 -0
  179. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts +29 -0
  180. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -0
  181. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +117 -0
  182. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts +46 -0
  183. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts.map +1 -0
  184. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.js +45 -0
  185. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +64 -0
  186. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -0
  187. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +68 -0
  188. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +12 -7
  189. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -1
  190. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.js +33 -11
  191. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts +58 -0
  192. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts.map +1 -0
  193. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.js +79 -0
  194. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts +39 -0
  195. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts.map +1 -0
  196. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.js +68 -0
  197. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts +53 -0
  198. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts.map +1 -0
  199. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.js +243 -0
  200. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +24 -8
  201. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
  202. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +250 -25
  203. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +12 -8
  204. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -1
  205. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.js +58 -50
  206. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +8 -1
  207. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
  208. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +18 -3
  209. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts +38 -0
  210. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts.map +1 -0
  211. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.js +81 -0
  212. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +23 -5
  213. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -1
  214. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.js +28 -21
  215. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.js +1 -1
  216. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -1
  217. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +224 -60
  218. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +15 -0
  219. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -1
  220. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.js +17 -0
  221. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +17 -0
  222. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -1
  223. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.js +19 -0
  224. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +10 -1
  225. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -1
  226. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.js +103 -9
  227. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +14 -2
  228. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -1
  229. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.js +38 -14
  230. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts +6 -2
  231. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  232. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +28 -5
  233. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts +32 -0
  234. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts.map +1 -0
  235. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.js +93 -0
  236. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +20 -6
  237. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  238. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +22 -8
  239. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts +16 -0
  240. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts.map +1 -0
  241. package/src/shade/renderer/geometry/virtual/format/attribute/{vgeo_layers_copy_vertex.js → vgeo_layers_copy_corner.js} +8 -8
  242. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts +18 -7
  243. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts.map +1 -1
  244. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.js +19 -8
  245. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.d.ts +3 -3
  246. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.js +3 -3
  247. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts +27 -0
  248. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.js +97 -0
  250. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts +7 -3
  251. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts.map +1 -1
  252. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.js +21 -11
  253. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts +21 -0
  254. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.js +41 -0
  256. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts +19 -1
  257. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts.map +1 -1
  258. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +20 -1
  259. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts +25 -0
  260. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.js +24 -0
  262. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts +5 -0
  263. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts.map +1 -1
  264. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.js +5 -0
  265. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.d.ts.map +1 -1
  266. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.js +5 -0
  267. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts +10 -3
  268. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts.map +1 -1
  269. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.js +37 -3
  270. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts +21 -13
  271. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts.map +1 -1
  272. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +37 -13
  273. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts +8 -2
  274. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts.map +1 -1
  275. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.js +14 -2
  276. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts +18 -0
  277. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts.map +1 -1
  278. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +19 -1
  279. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HEADER_OFFSET.js +6 -0
  280. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts +21 -0
  281. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts.map +1 -0
  282. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.js +20 -0
  283. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts +9 -0
  284. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts.map +1 -1
  285. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.js +10 -1
  286. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.d.ts +1 -2
  287. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +10 -3
  288. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts +20 -0
  289. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts.map +1 -0
  290. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.js +43 -0
  291. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +4 -2
  292. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  293. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +8 -4
  294. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  295. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +2 -2
  296. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts +6 -4
  297. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts.map +1 -1
  298. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.js +6 -4
  299. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +13 -2
  300. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  301. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +13 -2
  302. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts +5 -1
  303. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts.map +1 -1
  304. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.js +14 -1
  305. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts.map +1 -1
  306. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.js +1 -0
  307. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts +4 -1
  308. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts.map +1 -1
  309. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.js +53 -11
  310. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  311. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +7 -3
  312. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts +12 -2
  313. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  314. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +111 -33
  315. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts +6 -2
  316. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  317. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +112 -42
  318. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts +2 -2
  319. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  320. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +51 -21
  321. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +385 -0
  322. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
  323. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +9 -0
  324. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  325. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +34 -0
  326. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -1
  327. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +18 -0
  328. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  329. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +47 -19
  330. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +1 -0
  331. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +22 -4
  332. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +7 -2
  333. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -1
  334. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +8 -3
  335. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +1 -0
  336. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +10 -0
  337. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +1 -1
  338. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +13 -11
  339. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -1
  340. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +13 -11
  341. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -1
  342. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +4 -0
  343. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  344. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +3 -4
  345. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts +6 -2
  346. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts.map +1 -1
  347. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.js +90 -12
  348. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts +22 -0
  349. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts.map +1 -0
  350. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.js +21 -0
  351. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  352. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +13 -8
  353. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  354. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -6
  355. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  356. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +4 -12
  357. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +0 -2
  358. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  359. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +4 -5
  360. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  361. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +5 -3
  362. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.d.ts.map +1 -1
  363. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.js +11 -4
  364. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  365. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +4 -3
  366. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.d.ts.map +1 -1
  367. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.js +3 -0
  368. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.d.ts.map +1 -1
  369. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.js +94 -90
  370. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  371. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +13 -5
  372. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  373. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +96 -89
  374. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.d.ts.map +1 -1
  375. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.js +3 -1
  376. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.d.ts.map +1 -1
  377. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.js +4 -1
  378. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.d.ts.map +1 -1
  379. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.js +100 -89
  380. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.d.ts.map +1 -1
  381. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.js +31 -7
  382. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.d.ts.map +1 -1
  383. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.js +1 -7
  384. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.d.ts.map +1 -1
  385. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.js +161 -153
  386. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  387. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +2 -7
  388. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  389. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +4 -8
  390. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  391. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +5 -8
  392. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts +25 -0
  393. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts.map +1 -0
  394. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.js +24 -0
  395. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts +10 -0
  396. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  397. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +30 -21
  398. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.d.ts.map +0 -1
  399. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts.map +0 -1
  400. package/src/core/model/node-graph/visual/layout/layout_assign_layers.d.ts.map +0 -1
  401. package/src/core/model/node-graph/visual/layout/layout_break_cycles.d.ts.map +0 -1
  402. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +0 -1
  403. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +0 -1
  404. package/src/core/model/node-graph/visual/layout/layout_orient_links.d.ts.map +0 -1
  405. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts +0 -61
  406. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts.map +0 -1
  407. package/src/shade/playground/vgeo_viewer/main.d.ts.map +0 -1
  408. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts +0 -18
  409. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +0 -1
  410. package/src/shade/playground/vgeo_viewer/select_cut.d.ts +0 -173
  411. package/src/shade/playground/vgeo_viewer/select_cut.d.ts.map +0 -1
  412. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.d.ts +0 -52
  413. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.d.ts.map +0 -1
  414. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts +0 -24
  415. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts.map +0 -1
  416. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.d.ts +0 -32
  417. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.d.ts.map +0 -1
  418. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.d.ts +0 -59
  419. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.d.ts.map +0 -1
  420. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.d.ts +0 -44
  421. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.d.ts.map +0 -1
  422. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts +0 -20
  423. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts.map +0 -1
  424. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.d.ts +0 -73
  425. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.d.ts.map +0 -1
  426. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +0 -13
  427. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +0 -1
  428. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.d.ts +0 -12
  429. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.d.ts.map +0 -1
  430. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts +0 -36
  431. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts.map +0 -1
  432. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.d.ts +0 -13
  433. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.d.ts.map +0 -1
  434. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.d.ts +0 -19
  435. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.d.ts.map +0 -1
  436. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts +0 -29
  437. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts.map +0 -1
  438. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.d.ts +0 -31
  439. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.d.ts.map +0 -1
  440. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.d.ts +0 -18
  441. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.d.ts.map +0 -1
  442. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts +0 -14
  443. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts.map +0 -1
  444. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts +0 -12
  445. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts.map +0 -1
  446. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.d.ts +0 -14
  447. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.d.ts.map +0 -1
  448. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.d.ts +0 -11
  449. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.d.ts.map +0 -1
  450. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.d.ts +0 -46
  451. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.d.ts.map +0 -1
  452. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +0 -22
  453. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +0 -1
  454. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.d.ts +0 -29
  455. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.d.ts.map +0 -1
  456. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.d.ts +0 -9
  457. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.d.ts.map +0 -1
  458. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.d.ts +0 -10
  459. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.d.ts.map +0 -1
  460. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.d.ts +0 -15
  461. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.d.ts.map +0 -1
  462. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts +0 -16
  463. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts.map +0 -1
  464. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts +0 -10
  465. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts.map +0 -1
  466. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts +0 -17
  467. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts.map +0 -1
  468. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts +0 -3
  469. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts.map +0 -1
  470. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts +0 -8
  471. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts.map +0 -1
  472. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.d.ts +0 -0
  473. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.d.ts +0 -0
  474. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.d.ts +0 -0
  475. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.js +0 -0
  476. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.d.ts +0 -0
  477. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.js +0 -0
  478. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.d.ts +0 -0
  479. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.js +0 -0
  480. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.d.ts +0 -0
  481. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.js +0 -0
  482. /package/src/shade/playground/{vgeo_viewer → caldera}/main.d.ts +0 -0
@@ -4,12 +4,8 @@ import {
4
4
  allocator_run_size_for,
5
5
  OffsetAllocator
6
6
  } from "../../../../core/binary/allocator/OffsetAllocator.js";
7
- import { BinaryBuffer } from "../../../../core/binary/BinaryBuffer.js";
8
- import { platform_compute_endianness } from "../../../../core/binary/platform_compute_endianness.js";
9
7
  import Signal from "../../../../core/events/signal/Signal.js";
10
8
  import { MEGABYTE } from "../../../../core/science/units/memory/MEGABYTE.js";
11
- import { read_wgsl_type_value } from "../../shader/type/serde/read_gpu_typed_buffer.js";
12
- import { write_wgsl_type_value } from "../../shader/type/serde/write_gpu_typed_buffer.js";
13
9
  import { inspect_gpu_buffer_typed } from "../../buffer/inspect_gpu_buffer_typed.js";
14
10
  import {
15
11
  BUFFER_INTRA_COPY_REGION_STRUCT,
@@ -50,6 +46,121 @@ const DEFAULT_DATA_CAPACITY = 32 * MEGABYTE;
50
46
  */
51
47
  const ALLOCATION_LIMIT = 128 * 1024;
52
48
 
49
+ /**
50
+ * Where a row's `address` sits, in words. The patch that places a batch adds the allocation offset
51
+ * to this word of every row and copies the rest as bytes — which is only right while the row is
52
+ * packed and the field is where the struct says (`GPUMeshletManager.spec.js` pins both).
53
+ * @type {number}
54
+ */
55
+ const METADATA_ROW_WORDS = MESHLET_METADATA_STRUCT.aligned_size >>> 2;
56
+
57
+ /**
58
+ * The most records one {@link GPUMeshletManager#sweep} moves. A move is an O(1) slide in the
59
+ * allocator, four copy encodes, `meshlet_count` pairs in a patch table and one `changed`
60
+ * dispatch, so this bounds the main-thread cost of a sweep per frame by a count that does not
61
+ * grow with the resident set (COMPACTION_PLAN_2026_09_16 §3, §4.3).
62
+ * @type {number}
63
+ */
64
+ export const MESHLET_SWEEP_MOVE_BUDGET = 64;
65
+
66
+ /**
67
+ * The most words one sweep copies, data and rows together — 4 MB of GPU copy through the staging
68
+ * buffer. Whichever of the two budgets is met first ends the call, but never before one move
69
+ * when one is possible: a single record larger than this still moves, once.
70
+ * @type {number}
71
+ */
72
+ export const MESHLET_SWEEP_WORD_BUDGET = 1024 * 1024;
73
+
74
+ /**
75
+ * The most cursor steps one sweep takes, moves or not. A step that moves nothing — a hole the
76
+ * cursor was stepped back for that an allocation has since filled — is a link walk.
77
+ * @type {number}
78
+ */
79
+ export const MESHLET_SWEEP_STEP_BUDGET = 4096;
80
+
81
+ /**
82
+ * A removal that leaves at least this much of an arena free *in pieces* — free space less the
83
+ * largest free run — and at least an eighth of the arena that way, arms the sweep by itself.
84
+ *
85
+ * A sweep armed only by a refusal reclaims the space after the growth it could have prevented,
86
+ * and the next fill carves that tail before the next large arrival. Armed on scatter, the tail
87
+ * is clear by the time something needs it, at a cost bounded per frame as ever and proportional
88
+ * to the churn in total. Under a churn of one class the holes are recycled as fast as they are
89
+ * made and the scatter stays below the eighth; this is for a pool that thins.
90
+ * In words for the data arena; the row arena's floor is {@link MESHLET_SWEEP_SCATTER_MINIMUM_ROWS}.
91
+ * @type {number}
92
+ */
93
+ export const MESHLET_SWEEP_SCATTER_MINIMUM_WORDS = 16 * 1024;
94
+
95
+ /**
96
+ * @type {number}
97
+ */
98
+ export const MESHLET_SWEEP_SCATTER_MINIMUM_ROWS = 512;
99
+ const METADATA_ADDRESS_WORD = MESHLET_METADATA_STRUCT.fields.find(f => f.name === 'address').offset >>> 2;
100
+
101
+ /**
102
+ * What the manager has done to its arenas, for a bench, a tour or an overlay to read without
103
+ * reaching inside. A rebuild is the O(all batches) event the compaction plan is about; a growth
104
+ * is the bounded alternative; the sweep counters are COMPACTION_PLAN_2026_09_16 §4.7's.
105
+ */
106
+ class GPUMeshletManagerStats {
107
+ growths = 0;
108
+ rebuilds = 0;
109
+ moves = 0;
110
+ sweeps_armed = 0;
111
+ sweeps_completed = 0;
112
+ moves_last_sweep = 0;
113
+ }
114
+
115
+ /**
116
+ * One arena's compaction state. An episode is armed by a fragmentation refusal and ended by the
117
+ * sweep reaching the arena's end or by a rebuild; `grew` says whether the episode has spent its
118
+ * one growth (plan §4.4).
119
+ */
120
+ class ArenaSweep {
121
+ armed = false;
122
+ grew = false;
123
+
124
+ /**
125
+ * The used node the current pass considers next. {@link ALLOCATOR_NO_SPACE} when no pass is
126
+ * running.
127
+ * @type {number}
128
+ */
129
+ cursor = ALLOCATOR_NO_SPACE;
130
+
131
+ /**
132
+ * Where the next pass starts: the used node after the lowest hole opened since the current
133
+ * pass went by it — or, with no pass running, since the arena was last packed. Kept by every
134
+ * removal, so a pass needs no walk to find its first hole. {@link ALLOCATOR_NO_SPACE} when
135
+ * nothing is known to be behind a hole.
136
+ *
137
+ * A pass does not step back for a hole opened below it: under a pool that thins fast, every
138
+ * removal would send the cursor back below the records it had just packed, and the pass
139
+ * would never reach the end. It finishes, then a second pass from here closes every hole the
140
+ * first one left behind in one walk — the holes gather into one bubble whichever pass slides
141
+ * over them.
142
+ * @type {number}
143
+ */
144
+ restart = ALLOCATOR_NO_SPACE;
145
+
146
+ /**
147
+ * Records moved in the current episode.
148
+ * @type {number}
149
+ */
150
+ moves = 0;
151
+ }
152
+
153
+ /**
154
+ * One record's move in one arena, as the sweep recorded it: where the range was, where it is,
155
+ * both in that arena's elements.
156
+ */
157
+ class ArenaMove {
158
+ /** @type {MeshletBatchAllocationRecord} */
159
+ record;
160
+ from = 0;
161
+ to = 0;
162
+ }
163
+
53
164
  class MeshletBatchAllocationRecord {
54
165
  /**
55
166
  * @type {Allocation}
@@ -123,10 +234,76 @@ export class GPUMeshletManager {
123
234
  #device;
124
235
 
125
236
  /**
126
- *
237
+ * Node budget both allocators are built with; a rebuild re-picks the larger of this and twice
238
+ * the live batch count.
127
239
  * @type {number}
128
240
  */
129
- #deallocations_since_last_rebuild = 0;
241
+ #allocation_limit = ALLOCATION_LIMIT;
242
+
243
+ /**
244
+ * The state of the compaction of each arena (COMPACTION_PLAN_2026_09_16 §4.3–4.4).
245
+ * @type {ArenaSweep}
246
+ */
247
+ #sweep_data = new ArenaSweep();
248
+
249
+ /**
250
+ * @type {ArenaSweep}
251
+ */
252
+ #sweep_metadata = new ArenaSweep();
253
+
254
+ /**
255
+ * What the sweep needs of an arena, gathered so the two are one code path: its sweep state,
256
+ * its live records by the allocator node that describes their range (a used node's index is
257
+ * stable until it is freed, and a freed one is immediately reused — so the map is kept by one
258
+ * release path and the sweep asserts on it), and how to read a record's range in it.
259
+ */
260
+ #arena_data = {
261
+ sweep: this.#sweep_data,
262
+ /** @type {Map<number, MeshletBatchAllocationRecord>} */
263
+ records: new Map(),
264
+ allocator: () => this.#allocator_data,
265
+ /** @param {MeshletBatchAllocationRecord} record @returns {Allocation} */
266
+ allocation_of: record => record.data,
267
+ /** @param {MeshletBatchAllocationRecord} record @returns {number} true elements */
268
+ elements_of: record => record.data_size,
269
+ element_words: 1,
270
+ scatter_minimum: MESHLET_SWEEP_SCATTER_MINIMUM_WORDS,
271
+ };
272
+
273
+ #arena_metadata = {
274
+ sweep: this.#sweep_metadata,
275
+ /** @type {Map<number, MeshletBatchAllocationRecord>} */
276
+ records: new Map(),
277
+ allocator: () => this.#allocator_metadata,
278
+ /** @param {MeshletBatchAllocationRecord} record @returns {Allocation} */
279
+ allocation_of: record => record.metadata,
280
+ /** @param {MeshletBatchAllocationRecord} record @returns {number} true elements */
281
+ elements_of: record => record.meshlet_count,
282
+ element_words: METADATA_ROW_WORDS,
283
+ scatter_minimum: MESHLET_SWEEP_SCATTER_MINIMUM_ROWS,
284
+ };
285
+
286
+ /**
287
+ * The command context the last {@link sweep} recorded moves into, while it has not finished.
288
+ * A recorded copy names the range a record left; an arrival placed there in the meantime is
289
+ * uploaded ahead of the copy, and a growth or rebuild destroys the buffer the copy names — so
290
+ * nothing may allocate, free, grow or rebuild until that context has been submitted
291
+ * (COMPACTION_PLAN_2026_09_16 §4.3.1, §5).
292
+ * @type {ShadeGPUCommandContext|null}
293
+ */
294
+ #moving_ctx = null;
295
+
296
+ /**
297
+ * @type {GPUMeshletManagerStats}
298
+ */
299
+ #stats = new GPUMeshletManagerStats();
300
+
301
+ /**
302
+ * @returns {Readonly<GPUMeshletManagerStats>}
303
+ */
304
+ get stats() {
305
+ return this.#stats;
306
+ }
130
307
 
131
308
  /**
132
309
  * Every live allocation, in the order it was made — which is the order a rebuild packs them in.
@@ -167,26 +344,32 @@ export class GPUMeshletManager {
167
344
  * @param {GPUDevice} options.device
168
345
  * @param {number} [options.initial_data_capacity] in bytes
169
346
  * @param {number} [options.initial_metadata_capacity] in meshlets
347
+ * @param {number} [options.allocation_limit] node budget of each arena — the most ranges, live
348
+ * and free together, either can describe. Growth never raises it; a rebuild re-picks it.
170
349
  */
171
350
  constructor({
172
351
  device,
173
352
  initial_data_capacity = DEFAULT_DATA_CAPACITY,
174
353
  initial_metadata_capacity = DEFAULT_METADATA_CAPACITY,
354
+ allocation_limit = ALLOCATION_LIMIT,
175
355
  }) {
176
356
  assert.defined(device, 'device');
177
357
 
178
358
  assert.isNonNegativeInteger(initial_data_capacity, 'initial_data_capacity');
179
359
  assert.isNonNegativeInteger(initial_metadata_capacity, 'initial_metadata_capacity');
360
+ assert.isNonNegativeInteger(allocation_limit, 'allocation_limit');
180
361
 
181
362
  assert.greaterThan(initial_data_capacity, 0, 'initial_data_capacity');
182
363
  assert.greaterThan(initial_metadata_capacity, 0, 'initial_data_capacity');
364
+ assert.greaterThan(allocation_limit, 0, 'allocation_limit');
183
365
 
184
366
  this.#device = device;
367
+ this.#allocation_limit = allocation_limit;
185
368
 
186
369
  const data_buffer_size_words = Math.ceil(initial_data_capacity / 4);
187
370
 
188
- this.#allocator_data = new OffsetAllocator(data_buffer_size_words, ALLOCATION_LIMIT);
189
- this.#allocator_metadata = new OffsetAllocator(initial_metadata_capacity, ALLOCATION_LIMIT);
371
+ this.#allocator_data = new OffsetAllocator(data_buffer_size_words, allocation_limit);
372
+ this.#allocator_metadata = new OffsetAllocator(initial_metadata_capacity, allocation_limit);
190
373
 
191
374
  this.#meshlet_data_buffer = device.createBuffer({
192
375
  label: 'GPUMeshletManager/data',
@@ -203,6 +386,8 @@ export class GPUMeshletManager {
203
386
  }
204
387
 
205
388
  compact() {
389
+ this.#assert_not_moving();
390
+
206
391
  this.#rebuild(
207
392
  this.#meshlet_data_buffer.size >>> 2,
208
393
  Math.ceil(this.#meshlet_metadata_buffer.size / MESHLET_METADATA_STRUCT.aligned_size)
@@ -218,8 +403,8 @@ export class GPUMeshletManager {
218
403
  * re-patching, and no {@link MeshletBatchAllocationRecord#changed} fires. Compare {@link compact},
219
404
  * which makes room by re-packing instead, and pays all three of those for every record it moves.
220
405
  *
221
- * Growing closes no holes, which is why it leaves {@link #deallocations_since_last_rebuild}
222
- * alone: the holes it did not close are still there for a later {@link compact} to close.
406
+ * Growing closes no holes: the holes it did not close are still there for the sweep, or a
407
+ * later {@link compact}, to close.
223
408
  *
224
409
  * The arena an allocator is asked for is {@link OffsetAllocator#grow_target_for}, not the
225
410
  * shortfall: room for exactly `extra_data_size` can still be refused the allocation that asked
@@ -240,6 +425,8 @@ export class GPUMeshletManager {
240
425
  return;
241
426
  }
242
427
 
428
+ this.#assert_not_moving();
429
+
243
430
  const device = this.#device;
244
431
 
245
432
  const old_buffer_data = this.#meshlet_data_buffer;
@@ -337,6 +524,8 @@ export class GPUMeshletManager {
337
524
 
338
525
  device.queue.submit([encoder.finish()]);
339
526
 
527
+ this.#stats.growths++;
528
+
340
529
  if (new_buffer_data !== null) {
341
530
  old_buffer_data.destroy();
342
531
 
@@ -351,8 +540,16 @@ export class GPUMeshletManager {
351
540
  }
352
541
 
353
542
  /**
354
- * Helper to patch pointers in metadata and upload to GPU
355
- * @param {ArrayBuffer} source_array_buffer
543
+ * Place a batch's rows in the metadata buffer with every `address` made absolute.
544
+ *
545
+ * The rows a batch arrives with address its data relative to the batch; on the device they
546
+ * address the arena. The row is packed (`MESHLET_METADATA_STRUCT.size === aligned_size`) and
547
+ * the field is one word, so the patch is a byte copy of the batch's rows and one add per row.
548
+ * This used to be a struct read and a struct write per row through the generic serde, at
549
+ * ~20 µs a batch of 34 rows — which was most of what a rebuild cost per record, and what every
550
+ * page promotion paid (COMPACTION_PLAN_2026_09_16 §2.2).
551
+ *
552
+ * @param {ArrayBuffer} source_array_buffer the batch's rows, addresses relative to the batch
356
553
  * @param {number} meshlet_count
357
554
  * @param {number} data_offset_words
358
555
  * @param {number} metadata_offset_index
@@ -365,29 +562,22 @@ export class GPUMeshletManager {
365
562
  metadata_offset_index,
366
563
  target_buffer
367
564
  ) {
368
- const temp_writer = BinaryBuffer.fromEndianness(platform_compute_endianness());
369
- temp_writer.setCapacity(meshlet_count * MESHLET_METADATA_STRUCT.size); // pre-allocate to avoid resizing
370
-
371
- const source_reader = BinaryBuffer.fromArrayBuffer(source_array_buffer);
372
- source_reader.endianness = platform_compute_endianness();
373
-
374
- // Iterate over meshlets in the batch, patch the address, and write to temp buffer
375
- for (let i = 0; i < meshlet_count; i++) {
376
- const header = read_wgsl_type_value(source_reader, MESHLET_METADATA_STRUCT);
565
+ const word_count = meshlet_count * METADATA_ROW_WORDS;
377
566
 
378
- // PATCH: Add the allocation offset to the relative address
379
- header.address += data_offset_words;
567
+ // `metadata_buffer_original` is an ArrayBuffer from a BinaryBuffer, offset 0; a slice
568
+ // rather than a view because the batch keeps its rows relative for the next placement
569
+ const words = new Uint32Array(source_array_buffer.slice(0, word_count * 4));
380
570
 
381
- write_wgsl_type_value(header, temp_writer, MESHLET_METADATA_STRUCT);
571
+ for (let w = METADATA_ADDRESS_WORD; w < word_count; w += METADATA_ROW_WORDS) {
572
+ words[w] += data_offset_words;
382
573
  }
383
574
 
384
- // Upload
385
575
  this.#device.queue.writeBuffer(
386
576
  target_buffer,
387
577
  metadata_offset_index * MESHLET_METADATA_STRUCT.aligned_size,
388
- temp_writer.data,
578
+ words.buffer,
389
579
  0,
390
- meshlet_count * MESHLET_METADATA_STRUCT.aligned_size
580
+ word_count * 4
391
581
  );
392
582
  }
393
583
 
@@ -417,7 +607,7 @@ export class GPUMeshletManager {
417
607
  const record_count = records.size;
418
608
  const new_allocator_data = new OffsetAllocator(
419
609
  new_data_size,
420
- Math.max(record_count * 2, ALLOCATION_LIMIT)
610
+ Math.max(record_count * 2, this.#allocation_limit)
421
611
  );
422
612
 
423
613
  const old_buffer_data = this.#meshlet_data_buffer;
@@ -429,7 +619,7 @@ export class GPUMeshletManager {
429
619
 
430
620
  const new_allocator_metadata = new OffsetAllocator(
431
621
  new_metadata_size,
432
- Math.max(record_count * 2, ALLOCATION_LIMIT)
622
+ Math.max(record_count * 2, this.#allocation_limit)
433
623
  );
434
624
 
435
625
  const old_buffer_metadata = this.#meshlet_metadata_buffer;
@@ -445,11 +635,20 @@ export class GPUMeshletManager {
445
635
 
446
636
  const moved_records = [];
447
637
 
448
- // move data, in allocation order, which is the order the set keeps
638
+ const nodes_data = this.#arena_data.records;
639
+ const nodes_metadata = this.#arena_metadata.records;
640
+
641
+ nodes_data.clear();
642
+ nodes_metadata.clear();
643
+
644
+ // move data, in allocation order, which is the order the set keeps. Each range is the
645
+ // bin the record was placed in originally (see #allocate_data_and_metadata), which is
646
+ // also what makes sequential packing exact: a bin-sized request never falls in the gap
647
+ // between a remainder's size and the bin it is filed under.
449
648
  for (const record of records) {
450
649
 
451
- const new_allocation_data = new_allocator_data.allocate(record.data_size);
452
- const new_allocation_metadata = new_allocator_metadata.allocate(record.meshlet_count);
650
+ const new_allocation_data = new_allocator_data.allocate(allocator_run_size_for(record.data_size));
651
+ const new_allocation_metadata = new_allocator_metadata.allocate(allocator_run_size_for(record.meshlet_count));
453
652
 
454
653
  assert.notEqual(new_allocation_data.offset, ALLOCATOR_NO_SPACE);
455
654
  assert.notEqual(new_allocation_metadata.offset, ALLOCATOR_NO_SPACE);
@@ -478,6 +677,9 @@ export class GPUMeshletManager {
478
677
  record.data = new_allocation_data;
479
678
  record.metadata = new_allocation_metadata;
480
679
 
680
+ nodes_data.set(new_allocation_data.metadata, record);
681
+ nodes_metadata.set(new_allocation_metadata.metadata, record);
682
+
481
683
  if (metadata_record_moved) {
482
684
  moved_records.push(record);
483
685
  }
@@ -501,96 +703,170 @@ export class GPUMeshletManager {
501
703
  this.#allocator_data = new_allocator_data;
502
704
  this.#allocator_metadata = new_allocator_metadata;
503
705
 
504
- // reset count
505
- this.#deallocations_since_last_rebuild = 0;
706
+ // packed: whatever episode was running is over, and the nodes it was walking are gone
707
+ for (const sweep of [this.#sweep_data, this.#sweep_metadata]) {
708
+ sweep.armed = false;
709
+ sweep.grew = false;
710
+ sweep.cursor = ALLOCATOR_NO_SPACE;
711
+ sweep.restart = ALLOCATOR_NO_SPACE;
712
+ sweep.moves = 0;
713
+ }
714
+
715
+ this.#stats.rebuilds++;
506
716
  }
507
717
 
508
718
  /**
509
719
  * Allocate `data_size_words` in the data buffer and `meshlet_count`
510
- * in the metadata buffer, growing or compacting on failure and trying
511
- * again. Throws if the third attempt still can't satisfy both. The two
720
+ * in the metadata buffer, making room on refusal and trying again.
721
+ * Throws if the last attempt still can't satisfy both. The two
512
722
  * allocations are kept in lockstep — partial successes are released
513
723
  * before retrying so we never leak.
514
724
  *
515
- * Which of the two a failure gets is decided by what the failure is.
516
- * {@link grow} appends a contiguous run and moves nothing, so it is
517
- * the cheap and complete answer to an arena that is merely full.
518
- * {@link compact} adds no space at all — it closes holes — and pays a
519
- * migration of every allocation, a CPU metadata rewrite each and a
520
- * `changed` for every moved address to do it, so it is reserved for
521
- * the failure it is the only answer to: the room is already there and
522
- * merely scattered. Queued clone work is carried through either —
523
- * {@link #pending_clones} names records, not addresses — so an
524
- * allocation made while a clone is pending is safe.
725
+ * Every request is rounded up to the bin it would be looked up from
726
+ * (`allocator_run_size_for`) before it reaches the allocator. The
727
+ * allocator files a freed run under the largest bin that fits inside
728
+ * it and looks a request up from the smallest bin that fits the
729
+ * request, so a hole left by a batch serves only requests a whole bin
730
+ * smaller than it — under a churn of near-uniform batches that was
731
+ * most refusals, and each refusal a rebuild. A range that is exactly
732
+ * a bin is filed under exactly the bin any request of its class is
733
+ * looked up from, and is consumed whole by one, leaving no splinter.
734
+ * The record keeps the true sizes; the arena reserves the bin
735
+ * (COMPACTION_PLAN_2026_09_16 §2.1, §4.1).
736
+ *
737
+ * What a refusal gets (plan §4.4). {@link grow} appends and moves
738
+ * nothing, so it is the answer to an arena that is short — and, once
739
+ * per sweep episode, to one that is merely fragmented: the growth
740
+ * leaves headroom, the holes stay, and the sweep closes them over the
741
+ * frames that follow. A second fragmentation refusal before the sweep
742
+ * has finished is the case growth would compound, so it gets the one
743
+ * {@link compact}, which is O(all batches) and now bounded by the row
744
+ * patch. The rebuild is also the last resort before the throw, for an
745
+ * arena the device will not let grow — and for one that is out of
746
+ * nodes, which no arena size answers and which a rebuild re-picks.
747
+ * Queued clone work is carried through any of these — {@link
748
+ * #pending_clones} names records, not addresses.
525
749
  *
526
750
  * @param {number} data_size_words
527
751
  * @param {number} meshlet_count
528
752
  * @returns {{data: Allocation, metadata: Allocation}}
529
753
  */
530
754
  #allocate_data_and_metadata(data_size_words, meshlet_count) {
531
- let data_allocation;
532
- let metadata_allocation;
755
+ assert.isNonNegativeInteger(data_size_words, 'data_size_words');
756
+ assert.isNonNegativeInteger(meshlet_count, 'meshlet_count');
757
+
758
+ this.#assert_not_moving();
759
+
760
+ // An empty batch — a mesh with no triangles yet, a placeholder geometry — reserves one
761
+ // element rather than none: a zero-sized node has no place in a partition of the arena,
762
+ // and the sweep's head and cursor rules (plan §4.2) assume every node covers something.
763
+ const data_run = allocator_run_size_for(Math.max(1, data_size_words));
764
+ const metadata_run = allocator_run_size_for(Math.max(1, meshlet_count));
533
765
 
534
- for (let try_index = 0; try_index < 3; try_index++) {
766
+ // attempt 0 grows; attempt 1 grows or rebuilds; attempt 2 rebuilds; attempt 3 is the last
767
+ const LAST_ATTEMPT = 3;
535
768
 
536
- data_allocation = this.#allocator_data.allocate(data_size_words);
537
- metadata_allocation = this.#allocator_metadata.allocate(meshlet_count);
769
+ for (let attempt = 0; ; attempt++) {
770
+
771
+ const data_allocation = this.#allocator_data.allocate(data_run);
772
+ const metadata_allocation = this.#allocator_metadata.allocate(metadata_run);
538
773
 
539
774
  const data_ok = data_allocation.offset !== ALLOCATOR_NO_SPACE;
540
775
  const metadata_ok = metadata_allocation.offset !== ALLOCATOR_NO_SPACE;
541
776
 
542
777
  if (data_ok && metadata_ok) {
543
- break;
778
+ return { data: data_allocation, metadata: metadata_allocation };
544
779
  }
545
780
 
546
781
  // release partial allocation
547
782
  if (data_ok) {
548
783
  this.#allocator_data.free(data_allocation);
549
-
550
- data_allocation.offset = ALLOCATOR_NO_SPACE;
551
- data_allocation.metadata = ALLOCATOR_NO_SPACE;
552
784
  }
553
785
  if (metadata_ok) {
554
786
  this.#allocator_metadata.free(metadata_allocation);
787
+ }
555
788
 
556
- metadata_allocation.offset = ALLOCATOR_NO_SPACE;
557
- metadata_allocation.metadata = ALLOCATOR_NO_SPACE;
789
+ if (attempt === LAST_ATTEMPT) {
790
+ throw new Error('Failed to allocate GPU memory for meshlet batch');
558
791
  }
559
792
 
560
793
  // An allocator holding enough free space for the request it just refused is one whose
561
- // space is scattered, and closing the holes is the only thing that helps. Anything else
562
- // is a shortfall, which growth covers by appending. "Enough" is what a compaction would
563
- // hand back — every hole as one run — measured against the run the request is accepted
564
- // from, or a compaction could return the whole arena as one run and still be refused.
794
+ // space is scattered: with that much free and no run accepted there are at least two
795
+ // free nodes, so closing holes is what would help. Anything else is a shortfall.
796
+ // "Enough" is measured against the run the request is accepted from, or a packing
797
+ // could hand back the whole free space as one run and still be refused.
565
798
  const data_is_fragmented = !data_ok
566
- && this.#allocator_data.freeStorage >= allocator_run_size_for(data_size_words);
799
+ && this.#allocator_data.freeStorage >= data_run;
567
800
 
568
801
  const metadata_is_fragmented = !metadata_ok
569
- && this.#allocator_metadata.freeStorage >= allocator_run_size_for(meshlet_count);
570
-
571
- // #rebuild zeroes the counter, so a compaction that does not free the request through
572
- // leaves the next pass to grow — the loop always makes progress
573
- if (
574
- this.#deallocations_since_last_rebuild > 0
575
- && (data_is_fragmented || metadata_is_fragmented)
576
- ) {
802
+ && this.#allocator_metadata.freeStorage >= metadata_run;
803
+
804
+ let rebuild = attempt === LAST_ATTEMPT - 1;
805
+
806
+ if (data_is_fragmented) {
807
+ rebuild ||= this.#sweep_data.grew;
808
+
809
+ this.#arm_sweep(this.#sweep_data, this.#allocator_data);
810
+ }
811
+
812
+ if (metadata_is_fragmented) {
813
+ rebuild ||= this.#sweep_metadata.grew;
814
+
815
+ this.#arm_sweep(this.#sweep_metadata, this.#allocator_metadata);
816
+ }
817
+
818
+ if (rebuild) {
577
819
  this.compact();
578
820
 
579
821
  continue;
580
822
  }
581
823
 
582
- const grow_data_size = data_ok ? 0 : data_size_words;
583
- const grow_metadata_size = metadata_ok ? 0 : meshlet_count;
824
+ if (data_is_fragmented) {
825
+ this.#sweep_data.grew = true;
826
+ }
827
+
828
+ if (metadata_is_fragmented) {
829
+ this.#sweep_metadata.grew = true;
830
+ }
584
831
 
585
- this.grow(grow_data_size, grow_metadata_size);
832
+ this.grow(data_ok ? 0 : data_run, metadata_ok ? 0 : metadata_run);
833
+ }
834
+ }
586
835
 
836
+ /**
837
+ * Start a sweep of one arena, if one is not running. The cursor is kept up to date by every
838
+ * removal whether or not a sweep is running (plan §4.3), so arming is a flag.
839
+ *
840
+ * @param {ArenaSweep} sweep
841
+ * @param {OffsetAllocator} allocator
842
+ */
843
+ #arm_sweep(sweep, allocator) {
844
+ if (sweep.armed) {
845
+ return;
587
846
  }
588
847
 
589
- if (data_allocation.offset === ALLOCATOR_NO_SPACE || metadata_allocation.offset === ALLOCATOR_NO_SPACE) {
590
- throw new Error('Failed to allocate GPU memory for meshlet batch');
848
+ sweep.armed = true;
849
+ sweep.grew = false;
850
+ sweep.moves = 0;
851
+
852
+ if (sweep.cursor === ALLOCATOR_NO_SPACE) {
853
+ // every hole comes from a removal, which records where the pass should start; the
854
+ // case of none known walks from the head, bounded by the step budget
855
+ sweep.cursor = sweep.restart !== ALLOCATOR_NO_SPACE ? sweep.restart : allocator.first_used_node();
856
+ sweep.restart = ALLOCATOR_NO_SPACE;
591
857
  }
592
858
 
593
- return { data: data_allocation, metadata: metadata_allocation };
859
+ this.#stats.sweeps_armed++;
860
+ }
861
+
862
+ /**
863
+ * @throws when a sweep's moves are recorded into a command context that has not finished
864
+ */
865
+ #assert_not_moving() {
866
+ assert.ok(
867
+ this.#moving_ctx === null || this.#moving_ctx.finished,
868
+ 'GPUMeshletManager: a sweep recorded moves into a command context that has not finished; an allocation, removal, growth or rebuild now would race the copies it recorded'
869
+ );
594
870
  }
595
871
 
596
872
  /**
@@ -621,13 +897,15 @@ export class GPUMeshletManager {
621
897
 
622
898
  this.#records.add(record);
623
899
 
900
+ this.#arena_data.records.set(data.metadata, record);
901
+ this.#arena_metadata.records.set(metadata.metadata, record);
902
+
624
903
  return record;
625
904
  }
626
905
 
627
906
  /**
628
907
  * Release a batch allocation back to the data and metadata allocators. The GPU bytes are
629
- * not cleared — the freed slices are reclaimed lazily by the next {@link compact} (a
630
- * deallocation bumps the compaction counter the allocator consults under pressure).
908
+ * not cleared — the freed slices are handed to whoever asks next, or closed by a sweep.
631
909
  * Other batches' allocations are unaffected (the OffsetAllocator frees disjoint slices),
632
910
  * so callers only need to ensure nothing still references this batch's metadata rows
633
911
  * (the geometry table rebuild in {@link GPUGeometryManager} handles that).
@@ -643,17 +921,328 @@ export class GPUMeshletManager {
643
921
  return false;
644
922
  }
645
923
 
924
+ this.#assert_not_moving();
925
+
646
926
  this.#cancel_pending_clones_naming(record);
647
927
 
648
- this.#allocator_data.free(record.data);
649
- this.#allocator_metadata.free(record.metadata);
928
+ this.#retire(this.#arena_data, record);
929
+ this.#retire(this.#arena_metadata, record);
650
930
 
651
931
  this.#records.delete(record);
652
- this.#deallocations_since_last_rebuild++;
653
932
 
654
933
  return true;
655
934
  }
656
935
 
936
+ /**
937
+ * Give a record's range in one arena back, keeping the sweep's pointers and the node map right.
938
+ *
939
+ * A freed node's index is immediately the merged free node's, and then the next allocation's;
940
+ * a pointer left on it would name another live record and never fail loudly. So a pointer on
941
+ * this node is moved to the used node after it before the free. And the hole this leaves is
942
+ * recorded for the sweep: if it is above the running pass's cursor the pass reaches it; if
943
+ * it is below — or no pass is running — the *restart* point is moved down to the used node
944
+ * after it when it is the lowest hole known, so the next pass starts there and closes it
945
+ * (COMPACTION_PLAN_2026_09_16 §4.3).
946
+ *
947
+ * @param {object} arena
948
+ * @param {MeshletBatchAllocationRecord} record
949
+ */
950
+ #retire(arena, record) {
951
+ const sweep = arena.sweep;
952
+ const allocator = arena.allocator();
953
+ const allocation = arena.allocation_of(record);
954
+ const node = allocation.metadata;
955
+
956
+ const successor = allocator.next_used_node(node);
957
+
958
+ if (node === sweep.cursor) {
959
+ sweep.cursor = successor;
960
+ }
961
+
962
+ if (node === sweep.restart) {
963
+ sweep.restart = successor;
964
+ } else if (this.#is_below(arena, allocation, sweep.cursor) && this.#is_below(arena, allocation, sweep.restart)) {
965
+ sweep.restart = successor;
966
+ }
967
+
968
+ arena.records.delete(node);
969
+
970
+ allocator.free(allocation);
971
+
972
+ // Free space less the largest free run, both from the bins: O(1), and the largest run
973
+ // is bin-rounded down, which errs towards arming.
974
+ const report = allocator.storageReport();
975
+ const scattered = report.totalFreeSpace - report.largestFreeRegion;
976
+
977
+ if (scattered >= arena.scatter_minimum && scattered * 8 >= allocator.size) {
978
+ this.#arm_sweep(sweep, allocator);
979
+ }
980
+ }
981
+
982
+ /**
983
+ * Whether `allocation` sits below the record `pointer` names — or `pointer` names nothing,
984
+ * which counts as below: a hole with no pass or restart ahead of it is the lowest known.
985
+ *
986
+ * @param {object} arena
987
+ * @param {Allocation} allocation
988
+ * @param {number} pointer node index, or ALLOCATOR_NO_SPACE
989
+ * @returns {boolean}
990
+ */
991
+ #is_below(arena, allocation, pointer) {
992
+ if (pointer === ALLOCATOR_NO_SPACE) {
993
+ return true;
994
+ }
995
+
996
+ const record = arena.records.get(pointer);
997
+
998
+ assert.defined(record, 'record, a sweep pointer names a node no record holds');
999
+
1000
+ return allocation.offset < arena.allocation_of(record).offset;
1001
+ }
1002
+
1003
+ /**
1004
+ * One budgeted step of the compaction of both arenas, recorded into `cmd_ctx`.
1005
+ *
1006
+ * Nothing happens unless a fragmentation refusal has armed a sweep (see
1007
+ * {@link #allocate_data_and_metadata}). Then, in each arena, the cursor walks up the used
1008
+ * ranges sliding each down over the free space before it — which gathers every hole passed
1009
+ * into one run just behind the cursor — until {@link MESHLET_SWEEP_MOVE_BUDGET} records or
1010
+ * {@link MESHLET_SWEEP_WORD_BUDGET} words have moved (never fewer than one when one can) or
1011
+ * {@link MESHLET_SWEEP_STEP_BUDGET} steps have been taken, across both arenas. The sweep is
1012
+ * done when the cursor reaches the arena's end: all free space is one run at the tail, and
1013
+ * the episode is over.
1014
+ *
1015
+ * A moved record's bytes — data and rows alike — go through a transient staging buffer in
1016
+ * two `copyBufferToBuffer`s: WebGPU refuses a same-buffer copy, the intra-copy shader the
1017
+ * clone flush uses is not overlap-safe, and a slide by less than a record's size overlaps.
1018
+ * Rows are copied rather than re-derived from the batch's bytes because the skinning bounds
1019
+ * chain writes into clone rows on the device every frame. A data move then patches the
1020
+ * moved rows' `address` with the clone flush's shader. Every move is recorded in cursor
1021
+ * order, so a record sliding into space another vacated earlier in the same call reads what
1022
+ * that record's copy already wrote out. `changed` fires once per moved record after the
1023
+ * whole batch is recorded, with the new offsets on the record.
1024
+ *
1025
+ * The renderer calls this once per frame, at the head of the frame, before
1026
+ * {@link update}, so the clone flush reads post-sweep offsets and every pass reads rows and
1027
+ * data that agree. Until `cmd_ctx` finishes the manager refuses to allocate, free, grow or
1028
+ * rebuild — a recorded copy names ranges that must stay what they are until it runs.
1029
+ *
1030
+ * @param {ShadeGPUCommandContext} cmd_ctx
1031
+ */
1032
+ sweep(cmd_ctx) {
1033
+ assert.defined(cmd_ctx, 'cmd_ctx');
1034
+
1035
+ this.#assert_not_moving();
1036
+
1037
+ const budget = {
1038
+ moves: MESHLET_SWEEP_MOVE_BUDGET,
1039
+ words: MESHLET_SWEEP_WORD_BUDGET,
1040
+ steps: MESHLET_SWEEP_STEP_BUDGET,
1041
+ };
1042
+
1043
+ const data_moves = this.#sweep_arena(this.#arena_data, budget);
1044
+ const metadata_moves = this.#sweep_arena(this.#arena_metadata, budget);
1045
+
1046
+ if (data_moves.length === 0 && metadata_moves.length === 0) {
1047
+ return;
1048
+ }
1049
+
1050
+ this.#record_moves(cmd_ctx, data_moves, metadata_moves);
1051
+
1052
+ this.#moving_ctx = cmd_ctx;
1053
+
1054
+ const moved = new Set();
1055
+
1056
+ for (const move of data_moves) {
1057
+ moved.add(move.record);
1058
+ }
1059
+
1060
+ for (const move of metadata_moves) {
1061
+ moved.add(move.record);
1062
+ }
1063
+
1064
+ for (const record of moved) {
1065
+ record.changed.send0();
1066
+ }
1067
+ }
1068
+
1069
+ /**
1070
+ * The sweep steps of one arena, within `budget` (which is spent across both arenas).
1071
+ *
1072
+ * @param {object} arena
1073
+ * @param {{moves: number, words: number, steps: number}} budget
1074
+ * @returns {ArenaMove[]}
1075
+ */
1076
+ #sweep_arena(arena, budget) {
1077
+ const sweep = arena.sweep;
1078
+
1079
+ /** @type {ArenaMove[]} */
1080
+ const moves = [];
1081
+
1082
+ if (!sweep.armed) {
1083
+ return moves;
1084
+ }
1085
+
1086
+ const allocator = arena.allocator();
1087
+
1088
+ while (budget.moves > 0 && budget.words > 0 && budget.steps > 0) {
1089
+ const node = sweep.cursor;
1090
+
1091
+ if (node === ALLOCATOR_NO_SPACE) {
1092
+ if (sweep.restart !== ALLOCATOR_NO_SPACE) {
1093
+ // holes opened below this pass while it ran: another pass, from the lowest
1094
+ sweep.cursor = sweep.restart;
1095
+ sweep.restart = ALLOCATOR_NO_SPACE;
1096
+
1097
+ continue;
1098
+ }
1099
+
1100
+ // the arena's end: all free space is one run at the tail
1101
+ sweep.armed = false;
1102
+ sweep.grew = false;
1103
+
1104
+ this.#stats.sweeps_completed++;
1105
+ this.#stats.moves_last_sweep = sweep.moves;
1106
+
1107
+ sweep.moves = 0;
1108
+
1109
+ break;
1110
+ }
1111
+
1112
+ const record = arena.records.get(node);
1113
+
1114
+ assert.defined(record, 'record, the cursor names a node no record holds');
1115
+
1116
+ const allocation = arena.allocation_of(record);
1117
+
1118
+ assert.equal(allocation.metadata, node, 'allocation.metadata, the cursor names a node another record holds');
1119
+
1120
+ budget.steps--;
1121
+
1122
+ const delta = allocator.slide_down(node);
1123
+
1124
+ sweep.cursor = allocator.next_used_node(node);
1125
+
1126
+ if (delta === 0) {
1127
+ continue;
1128
+ }
1129
+
1130
+ const move = new ArenaMove();
1131
+
1132
+ move.record = record;
1133
+ move.from = allocation.offset;
1134
+ move.to = allocation.offset - delta;
1135
+
1136
+ allocation.offset = move.to;
1137
+
1138
+ moves.push(move);
1139
+
1140
+ budget.moves--;
1141
+ budget.words -= arena.elements_of(record) * arena.element_words;
1142
+
1143
+ sweep.moves++;
1144
+
1145
+ this.#stats.moves++;
1146
+ }
1147
+
1148
+ return moves;
1149
+ }
1150
+
1151
+ /**
1152
+ * The GPU side of a sweep step: the staged copies of every moved range, then the address
1153
+ * patch of every row whose data moved.
1154
+ *
1155
+ * One staging buffer sized to the largest move, reused from offset 0 by every copy: copies in
1156
+ * one command buffer are ordered. The patch table is built after the row copies from the
1157
+ * records' post-move row offsets, which is where the rows are by the time it runs.
1158
+ *
1159
+ * @param {ShadeGPUCommandContext} cmd_ctx
1160
+ * @param {ArenaMove[]} data_moves
1161
+ * @param {ArenaMove[]} metadata_moves
1162
+ */
1163
+ #record_moves(cmd_ctx, data_moves, metadata_moves) {
1164
+ const data_buffer = this.#meshlet_data_buffer;
1165
+ const metadata_buffer = this.#meshlet_metadata_buffer;
1166
+ const row_bytes = MESHLET_METADATA_STRUCT.aligned_size;
1167
+
1168
+ let largest_bytes = 0;
1169
+
1170
+ for (const move of data_moves) {
1171
+ largest_bytes = Math.max(largest_bytes, move.record.data_size * 4);
1172
+ }
1173
+
1174
+ for (const move of metadata_moves) {
1175
+ largest_bytes = Math.max(largest_bytes, move.record.meshlet_count * row_bytes);
1176
+ }
1177
+
1178
+ if (largest_bytes > 0) {
1179
+ const stage = cmd_ctx.allocateTransientBuffer(
1180
+ GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
1181
+ largest_bytes
1182
+ );
1183
+
1184
+ for (const move of data_moves) {
1185
+ const bytes = move.record.data_size * 4;
1186
+
1187
+ if (bytes === 0) {
1188
+ continue;
1189
+ }
1190
+
1191
+ cmd_ctx.copyBufferToBuffer(data_buffer, move.from * 4, stage, 0, bytes);
1192
+ cmd_ctx.copyBufferToBuffer(stage, 0, data_buffer, move.to * 4, bytes);
1193
+ }
1194
+
1195
+ for (const move of metadata_moves) {
1196
+ const bytes = move.record.meshlet_count * row_bytes;
1197
+
1198
+ if (bytes === 0) {
1199
+ continue;
1200
+ }
1201
+
1202
+ cmd_ctx.copyBufferToBuffer(metadata_buffer, move.from * row_bytes, stage, 0, bytes);
1203
+ cmd_ctx.copyBufferToBuffer(stage, 0, metadata_buffer, move.to * row_bytes, bytes);
1204
+ }
1205
+ }
1206
+
1207
+ let patch_count = 0;
1208
+
1209
+ for (const move of data_moves) {
1210
+ patch_count += move.record.meshlet_count;
1211
+ }
1212
+
1213
+ if (patch_count === 0) {
1214
+ return;
1215
+ }
1216
+
1217
+ const patches = new Uint32Array(patch_count * 2);
1218
+
1219
+ let p = 0;
1220
+
1221
+ for (const move of data_moves) {
1222
+ const record = move.record;
1223
+
1224
+ // the same wrapping add as the clone flush: a move down is a negative delta
1225
+ const delta = (move.to - move.from) >>> 0;
1226
+
1227
+ for (let i = 0; i < record.meshlet_count; i++) {
1228
+ patches[p++] = record.metadata.offset + i;
1229
+ patches[p++] = delta;
1230
+ }
1231
+ }
1232
+
1233
+ const patches_buffer = cmd_ctx.allocateTransientBufferAndLoad(patches.buffer, GPUBufferUsage.STORAGE);
1234
+
1235
+ shader_meshlet_metadata_address_patch.dispatchThreads({
1236
+ encoder: cmd_ctx,
1237
+ thread_count_x: patch_count,
1238
+ bindings: {
1239
+ meshlet_metadata: metadata_buffer,
1240
+ patches: patches_buffer,
1241
+ patch_count,
1242
+ },
1243
+ });
1244
+ }
1245
+
657
1246
  /**
658
1247
  * Drop the queued clone copies that name `record`, because it is about to stop owning the
659
1248
  * ranges they were going to read from or write to.
@@ -729,6 +1318,9 @@ export class GPUMeshletManager {
729
1318
 
730
1319
  this.#records.add(record);
731
1320
 
1321
+ this.#arena_data.records.set(data.metadata, record);
1322
+ this.#arena_metadata.records.set(metadata.metadata, record);
1323
+
732
1324
  // The queue names the two records rather than the addresses they
733
1325
  // hold right now. Both are in `#records`, so a rebuild between
734
1326
  // this call and the flush moves them and the flush still reads
@@ -922,6 +1514,21 @@ export class GPUMeshletManager {
922
1514
  return result;
923
1515
  }
924
1516
 
1517
+ /**
1518
+ * Bytes the arenas have handed out: every batch's range rounded to its bin, data and metadata
1519
+ * both. Above {@link gpu_memory_usage_occupied} by the quantization overhead — 3.4 % of data
1520
+ * and 3.9 % of rows on page-shaped batches, at most 12.5 % on any.
1521
+ *
1522
+ * @returns {number}
1523
+ */
1524
+ get gpu_memory_usage_reserved() {
1525
+ const data = this.#allocator_data;
1526
+ const metadata = this.#allocator_metadata;
1527
+
1528
+ return (data.size - data.freeStorage) * 4
1529
+ + (metadata.size - metadata.freeStorage) * MESHLET_METADATA_STRUCT.aligned_size;
1530
+ }
1531
+
925
1532
  get gpu_memory_usage() {
926
1533
  let result = 0;
927
1534