@woosh/meep-engine 3.28.0 → 3.30.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 (685) hide show
  1. package/build/bundle-worker-image-decoder.js +1 -1
  2. package/build/bundle-worker-terrain.js +1 -1
  3. package/package.json +1 -1
  4. package/src/core/binary/BinaryBuffer.d.ts.map +1 -1
  5. package/src/core/binary/BinaryBuffer.js +14 -2
  6. package/src/core/binary/BitSet.d.ts +16 -0
  7. package/src/core/binary/BitSet.d.ts.map +1 -1
  8. package/src/core/binary/BitSet.js +159 -49
  9. package/src/core/binary/allocator/OffsetAllocator.d.ts +48 -0
  10. package/src/core/binary/allocator/OffsetAllocator.d.ts.map +1 -1
  11. package/src/core/binary/allocator/OffsetAllocator.js +208 -9
  12. package/src/core/binary/compression/compress_bytes.d.ts +20 -0
  13. package/src/core/binary/compression/compress_bytes.d.ts.map +1 -0
  14. package/src/core/binary/compression/compress_bytes.js +35 -0
  15. package/src/core/binary/compression/decompress_bytes_into.d.ts +30 -0
  16. package/src/core/binary/compression/decompress_bytes_into.d.ts.map +1 -0
  17. package/src/core/binary/compression/decompress_bytes_into.js +87 -0
  18. package/src/core/binary/compression/deflate_decompress_bound.d.ts +27 -0
  19. package/src/core/binary/compression/deflate_decompress_bound.d.ts.map +1 -0
  20. package/src/core/binary/compression/deflate_decompress_bound.js +32 -0
  21. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  22. package/src/core/bvh2/bvh3/BVH.js +14 -1
  23. package/src/core/cache/FrequencySketch.d.ts +3 -1
  24. package/src/core/cache/FrequencySketch.d.ts.map +1 -1
  25. package/src/core/cache/FrequencySketch.js +10 -4
  26. package/src/core/cache/wtinylfu/CacheWTinylfu.d.ts.map +1 -1
  27. package/src/core/cache/wtinylfu/CacheWTinylfu.js +24 -2
  28. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts +24 -0
  29. package/src/core/geom/3d/quadric/quadric_attribute_add.d.ts.map +1 -0
  30. package/src/core/geom/3d/quadric/quadric_attribute_add.js +31 -0
  31. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts +46 -0
  32. package/src/core/geom/3d/quadric/quadric_attribute_reduce.d.ts.map +1 -0
  33. package/src/core/geom/3d/quadric/quadric_attribute_reduce.js +104 -0
  34. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts +62 -0
  35. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.d.ts.map +1 -0
  36. package/src/core/geom/3d/quadric/quadric_attribute_set_from_triangle.js +158 -0
  37. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts +21 -0
  38. package/src/core/geom/3d/quadric/quadric_attribute_size.d.ts.map +1 -0
  39. package/src/core/geom/3d/quadric/quadric_attribute_size.js +24 -0
  40. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts +34 -0
  41. package/src/core/geom/3d/quadric/quadric_attribute_solve.d.ts.map +1 -0
  42. package/src/core/geom/3d/quadric/quadric_attribute_solve.js +55 -0
  43. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts +28 -0
  44. package/src/core/geom/3d/quadric/quadric_attribute_sub.d.ts.map +1 -0
  45. package/src/core/geom/3d/quadric/quadric_attribute_sub.js +35 -0
  46. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts +15 -0
  47. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.d.ts.map +1 -1
  48. package/src/core/geom/3d/topology/struct/binary/BinaryDataBlock.js +35 -0
  49. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts +93 -0
  50. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.d.ts.map +1 -0
  51. package/src/core/geom/3d/topology/struct/binary/BtWedgeSet.js +128 -0
  52. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts +20 -0
  53. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.d.ts.map +1 -1
  54. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_vertex_quadrics.js +69 -4
  55. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts +38 -0
  56. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.d.ts.map +1 -0
  57. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_compute_wedge_attribute_quadrics.js +113 -0
  58. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts +93 -17
  59. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.d.ts.map +1 -1
  60. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_simplify.js +930 -69
  61. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts +22 -0
  62. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.d.ts.map +1 -0
  63. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_live_count.js +46 -0
  64. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts +19 -0
  65. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.d.ts.map +1 -0
  66. package/src/core/geom/3d/topology/struct/binary/io/bt_wedges_per_vertex.js +49 -0
  67. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts +25 -0
  68. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.d.ts.map +1 -0
  69. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_fold_limit.js +228 -0
  70. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts +13 -0
  71. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.d.ts.map +1 -1
  72. package/src/core/geom/3d/topology/struct/binary/query/bt_edge_collapse_would_fold.js +10 -128
  73. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.d.ts.map +1 -1
  74. package/src/core/geom/3d/topology/struct/binary/query/bt_mesh_build_face_bvh.js +11 -1
  75. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts +63 -0
  76. package/src/core/graph/layout/layered/BoxLayoutOptions.d.ts.map +1 -0
  77. package/src/core/graph/layout/layered/BoxLayoutOptions.js +80 -0
  78. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.d.ts +35 -0
  79. package/src/core/graph/layout/layered/LayoutBoxGraph.d.ts.map +1 -0
  80. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/LayoutBoxGraph.js +53 -1
  81. package/src/core/graph/layout/layered/layout_assign_coordinates.d.ts.map +1 -0
  82. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.js +72 -68
  83. package/src/core/graph/layout/layered/layout_assign_layers.d.ts.map +1 -0
  84. package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.js +4 -2
  85. package/src/core/graph/layout/layered/layout_break_cycles.d.ts.map +1 -0
  86. package/src/core/graph/layout/layered/layout_insert_lanes.d.ts.map +1 -0
  87. package/src/core/graph/layout/layered/layout_order_layers.d.ts.map +1 -0
  88. package/src/core/graph/layout/layered/layout_orient_links.d.ts.map +1 -0
  89. package/src/core/graph/mn_graph_collapse_weighted_edge.js +6 -6
  90. package/src/core/math/lookup/ParameterLookupTable.d.ts.map +1 -1
  91. package/src/core/math/lookup/ParameterLookupTable.js +10 -5
  92. package/src/core/math/noise/sdnoise.d.ts.map +1 -1
  93. package/src/core/math/noise/sdnoise.js +16 -10
  94. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts +7 -51
  95. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.d.ts.map +1 -1
  96. package/src/core/model/node-graph/visual/layout/NodeGraphLayoutOptions.js +8 -69
  97. package/src/core/model/node-graph/visual/layout/layout_build_problem.js +1 -1
  98. package/src/core/model/node-graph/visual/layout/layout_node_graph.js +6 -6
  99. package/src/core/model/node-graph/visual/layout/route_grid_build.js +1 -1
  100. package/src/core/model/object/ImmutableObjectPool.d.ts.map +1 -1
  101. package/src/core/model/object/ImmutableObjectPool.js +17 -7
  102. package/src/core/process/executor/ConcurrentExecutor.d.ts +1 -5
  103. package/src/core/process/executor/ConcurrentExecutor.d.ts.map +1 -1
  104. package/src/core/process/executor/ConcurrentExecutor.js +95 -1
  105. package/src/core/process/task/task_find_dependency_cycle.d.ts +17 -0
  106. package/src/core/process/task/task_find_dependency_cycle.d.ts.map +1 -0
  107. package/src/core/process/task/task_find_dependency_cycle.js +81 -0
  108. package/src/core/wfc/WFCCellChoice.d.ts +17 -0
  109. package/src/core/wfc/WFCCellChoice.d.ts.map +1 -0
  110. package/src/core/wfc/WFCCellChoice.js +36 -0
  111. package/src/core/wfc/WFCLattice.d.ts +105 -0
  112. package/src/core/wfc/WFCLattice.d.ts.map +1 -0
  113. package/src/core/wfc/WFCLattice.js +235 -0
  114. package/src/core/wfc/WFCRuleTable.d.ts +172 -0
  115. package/src/core/wfc/WFCRuleTable.d.ts.map +1 -0
  116. package/src/core/wfc/WFCRuleTable.js +334 -0
  117. package/src/core/wfc/WFCState.d.ts +273 -0
  118. package/src/core/wfc/WFCState.d.ts.map +1 -0
  119. package/src/core/wfc/WFCState.js +552 -0
  120. package/src/core/wfc/WFCStatus.d.ts +11 -0
  121. package/src/core/wfc/WFCStatus.d.ts.map +1 -0
  122. package/src/core/wfc/WFCStatus.js +28 -0
  123. package/src/core/wfc/WFCTileWeights.d.ts +48 -0
  124. package/src/core/wfc/WFCTileWeights.d.ts.map +1 -0
  125. package/src/core/wfc/WFCTileWeights.js +86 -0
  126. package/src/core/wfc/WFC_UNRESOLVED.d.ts +12 -0
  127. package/src/core/wfc/WFC_UNRESOLVED.d.ts.map +1 -0
  128. package/src/core/wfc/WFC_UNRESOLVED.js +11 -0
  129. package/src/core/wfc/wfc_estimate_bytes.d.ts +24 -0
  130. package/src/core/wfc/wfc_estimate_bytes.d.ts.map +1 -0
  131. package/src/core/wfc/wfc_estimate_bytes.js +47 -0
  132. package/src/core/wfc/wfc_rule_table_tile_components.d.ts +27 -0
  133. package/src/core/wfc/wfc_rule_table_tile_components.d.ts.map +1 -0
  134. package/src/core/wfc/wfc_rule_table_tile_components.js +80 -0
  135. package/src/core/wfc/wfc_solve.d.ts +21 -0
  136. package/src/core/wfc/wfc_solve.d.ts.map +1 -0
  137. package/src/core/wfc/wfc_solve.js +154 -0
  138. package/src/core/wfc/wfc_state_apply_boundary.d.ts +18 -0
  139. package/src/core/wfc/wfc_state_apply_boundary.d.ts.map +1 -0
  140. package/src/core/wfc/wfc_state_apply_boundary.js +61 -0
  141. package/src/core/wfc/wfc_state_assign.d.ts +11 -0
  142. package/src/core/wfc/wfc_state_assign.d.ts.map +1 -0
  143. package/src/core/wfc/wfc_state_assign.js +36 -0
  144. package/src/core/wfc/wfc_state_ban_bits.d.ts +17 -0
  145. package/src/core/wfc/wfc_state_ban_bits.d.ts.map +1 -0
  146. package/src/core/wfc/wfc_state_ban_bits.js +81 -0
  147. package/src/core/wfc/wfc_state_collapse.d.ts +12 -0
  148. package/src/core/wfc/wfc_state_collapse.d.ts.map +1 -0
  149. package/src/core/wfc/wfc_state_collapse.js +73 -0
  150. package/src/core/wfc/wfc_state_flush_dirty.d.ts +12 -0
  151. package/src/core/wfc/wfc_state_flush_dirty.d.ts.map +1 -0
  152. package/src/core/wfc/wfc_state_flush_dirty.js +39 -0
  153. package/src/core/wfc/wfc_state_propagate.d.ts +14 -0
  154. package/src/core/wfc/wfc_state_propagate.d.ts.map +1 -0
  155. package/src/core/wfc/wfc_state_propagate.js +155 -0
  156. package/src/core/wfc/wfc_state_read.d.ts +11 -0
  157. package/src/core/wfc/wfc_state_read.d.ts.map +1 -0
  158. package/src/core/wfc/wfc_state_read.js +38 -0
  159. package/src/core/wfc/wfc_state_restrict.d.ts +18 -0
  160. package/src/core/wfc/wfc_state_restrict.d.ts.map +1 -0
  161. package/src/core/wfc/wfc_state_restrict.js +39 -0
  162. package/src/core/wfc/wfc_state_set_eligible.d.ts +24 -0
  163. package/src/core/wfc/wfc_state_set_eligible.d.ts.map +1 -0
  164. package/src/core/wfc/wfc_state_set_eligible.js +49 -0
  165. package/src/core/wfc/wfc_state_undo_to.d.ts +13 -0
  166. package/src/core/wfc/wfc_state_undo_to.d.ts.map +1 -0
  167. package/src/core/wfc/wfc_state_undo_to.js +84 -0
  168. package/src/core/wfc/wfc_words_per_cell.d.ts +11 -0
  169. package/src/core/wfc/wfc_words_per_cell.d.ts.map +1 -0
  170. package/src/core/wfc/wfc_words_per_cell.js +16 -0
  171. package/src/engine/ecs/EntityComponentDataset.d.ts +8 -0
  172. package/src/engine/ecs/EntityComponentDataset.d.ts.map +1 -1
  173. package/src/engine/ecs/EntityComponentDataset.js +37 -14
  174. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts +8 -1
  175. package/src/engine/ecs/fow/FogOfWarRevealerSystem.d.ts.map +1 -1
  176. package/src/engine/ecs/fow/FogOfWarRevealerSystem.js +37 -2
  177. package/src/engine/ecs/guid/UUID.d.ts.map +1 -1
  178. package/src/engine/ecs/guid/UUID.js +45 -17
  179. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.d.ts.map +1 -1
  180. package/src/engine/ecs/ik/OneBoneSurfaceAlignmentSolver.js +3 -1
  181. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.d.ts.map +1 -1
  182. package/src/engine/ecs/terrain/ecs/cling/ClingToTerrainSystem.js +366 -360
  183. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.d.ts.map +1 -1
  184. package/src/engine/graphics/texture/sampler/resize/sampler2d_scale_down_linear.js +24 -10
  185. package/src/engine/graphics3/DebugDrawSystem.d.ts +7 -5
  186. package/src/engine/graphics3/DebugDrawSystem.d.ts.map +1 -1
  187. package/src/engine/graphics3/DebugDrawSystem.js +206 -203
  188. package/src/engine/graphics3/DecalSystem.d.ts +4 -5
  189. package/src/engine/graphics3/DecalSystem.d.ts.map +1 -1
  190. package/src/engine/graphics3/DecalSystem.js +5 -5
  191. package/src/engine/graphics3/FogOfWarSystem.d.ts +7 -12
  192. package/src/engine/graphics3/FogOfWarSystem.d.ts.map +1 -1
  193. package/src/engine/graphics3/FogOfWarSystem.js +8 -12
  194. package/src/engine/graphics3/prefab/prefab_instantiate.js +230 -230
  195. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.d.ts.map +1 -1
  196. package/src/engine/grid/grid2transform/GridPosition2TransformSystem.js +24 -5
  197. package/src/engine/interpolation/Interpoland.d.ts +7 -1
  198. package/src/engine/interpolation/Interpoland.d.ts.map +1 -1
  199. package/src/engine/interpolation/Interpoland.js +8 -1
  200. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -1
  201. package/src/engine/interpolation/InterpolationSystem.js +3 -0
  202. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +5 -3
  203. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -1
  204. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +14 -3
  205. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -1
  206. package/src/engine/interpolation/pose_interpoland.js +2 -0
  207. package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
  208. package/src/engine/network/orchestrator/NetworkPeer.js +15 -10
  209. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  210. package/src/engine/physics/cloth/ecs/ClothWorld.js +17 -0
  211. package/src/engine/physics/mls-mpm/MLS_MPM.d.ts.map +1 -1
  212. package/src/engine/physics/mls-mpm/MLS_MPM.js +14 -0
  213. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts +2 -1
  214. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.d.ts.map +1 -1
  215. package/src/engine/sound/sopra/definition/clip/AbstractAudioClip.js +1 -1
  216. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts +1 -0
  217. package/src/engine/sound/sopra/definition/clip/BlendContainerAudioClip.d.ts.map +1 -1
  218. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts +1 -0
  219. package/src/engine/sound/sopra/definition/clip/RandomContainerAudioClip.d.ts.map +1 -1
  220. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts +2 -1
  221. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.d.ts.map +1 -1
  222. package/src/engine/sound/sopra/definition/clip/SampleAudioClip.js +12 -3
  223. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts +1 -0
  224. package/src/engine/sound/sopra/definition/clip/SequenceContainerAudioClip.d.ts.map +1 -1
  225. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts +1 -0
  226. package/src/engine/sound/sopra/definition/clip/SwitchContainerAudioClip.d.ts.map +1 -1
  227. package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
  228. package/src/engine/sound/sopra/runtime/EventInstance.js +4 -1
  229. package/src/format/image/avif/av1/decode/decode_coefficients.d.ts.map +1 -1
  230. package/src/format/image/avif/av1/decode/decode_coefficients.js +248 -259
  231. package/src/format/image/avif/av1/decode/read_golomb.d.ts +21 -0
  232. package/src/format/image/avif/av1/decode/read_golomb.d.ts.map +1 -0
  233. package/src/format/image/avif/av1/decode/read_golomb.js +49 -0
  234. package/src/format/image/avif/av1/filter/loop_restoration.js +461 -451
  235. package/src/format/image/avif/av1/util/round2.d.ts +19 -2
  236. package/src/format/image/avif/av1/util/round2.d.ts.map +1 -1
  237. package/src/format/image/avif/av1/util/round2.js +51 -27
  238. package/src/format/image/avif/heif/parse_avif_file.js +24 -0
  239. package/src/shade/RENDERER_CONTRACT.md +12 -7
  240. package/src/shade/playground/caldera/README.md +529 -0
  241. package/src/shade/playground/caldera/caldera_format.d.ts +151 -0
  242. package/src/shade/playground/caldera/caldera_format.d.ts.map +1 -0
  243. package/src/shade/playground/caldera/caldera_format.js +241 -0
  244. package/src/shade/playground/caldera/index.html +126 -0
  245. package/src/shade/playground/caldera/main.d.ts.map +1 -0
  246. package/src/shade/playground/caldera/main.js +1565 -0
  247. package/src/shade/playground/mesh_row_ceiling/README.md +85 -0
  248. package/src/shade/playground/mesh_row_ceiling/index.html +31 -0
  249. package/src/shade/playground/mesh_row_ceiling/main.d.ts +2 -0
  250. package/src/shade/playground/mesh_row_ceiling/main.d.ts.map +1 -0
  251. package/src/shade/playground/mesh_row_ceiling/main.js +389 -0
  252. package/src/shade/playground/vgeo_runtime/README.md +5 -3
  253. package/src/shade/playground/vgeo_runtime/main.js +3 -2
  254. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +3 -3
  255. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -1
  256. package/src/shade/playground/vgeo_runtime/sample_asset.js +3 -3
  257. package/src/shade/playground/vgeo_scene/README.md +103 -24
  258. package/src/shade/playground/vgeo_scene/main.js +525 -24
  259. package/src/shade/renderer/GraphicsContext.d.ts.map +1 -1
  260. package/src/shade/renderer/GraphicsContext.js +6 -0
  261. package/src/shade/renderer/Renderer.d.ts +2 -0
  262. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  263. package/src/shade/renderer/Renderer.js +23 -0
  264. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.d.ts.map +1 -1
  265. package/src/shade/renderer/animation/skinning/GPUMeshSkinningContext.js +100 -8
  266. package/src/shade/renderer/buffer/table/GPUTypedTable.d.ts.map +1 -1
  267. package/src/shade/renderer/buffer/table/GPUTypedTable.js +14 -0
  268. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +69 -33
  269. package/src/shade/renderer/extension/RenderExtension.d.ts +15 -18
  270. package/src/shade/renderer/extension/RenderExtension.d.ts.map +1 -1
  271. package/src/shade/renderer/extension/RenderExtension.js +15 -19
  272. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts +5 -5
  273. package/src/shade/renderer/extension/RenderExtensionRegistry.d.ts.map +1 -1
  274. package/src/shade/renderer/extension/RenderExtensionRegistry.js +29 -166
  275. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  276. package/src/shade/renderer/geometry/GPUGeometryManager.js +73 -16
  277. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  278. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +18 -3
  279. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +663 -0
  280. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts +100 -5
  281. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  282. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +688 -81
  283. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +540 -0
  284. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +155 -64
  285. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +76 -16
  286. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +3 -3
  287. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +448 -0
  288. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +44 -7
  289. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  290. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +48 -9
  291. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts +40 -0
  292. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.d.ts.map +1 -0
  293. package/src/shade/renderer/geometry/virtual/build/level/VGeoContribution.js +24 -0
  294. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts +91 -0
  295. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.d.ts.map +1 -0
  296. package/src/shade/renderer/geometry/virtual/build/level/VGeoFrontier.js +91 -0
  297. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts +24 -0
  298. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.d.ts.map +1 -0
  299. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupRepack.js +25 -0
  300. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts +77 -0
  301. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.d.ts.map +1 -0
  302. package/src/shade/renderer/geometry/virtual/build/level/VGeoGroupStep.js +84 -0
  303. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +19 -6
  304. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -1
  305. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.js +20 -7
  306. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts +19 -0
  307. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.d.ts.map +1 -0
  308. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster.js +58 -0
  309. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts +18 -0
  310. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.d.ts.map +1 -0
  311. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_cluster_graph.js +116 -0
  312. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts +18 -0
  313. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.d.ts.map +1 -0
  314. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_group_step.js +545 -0
  315. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts +16 -0
  316. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.d.ts.map +1 -0
  317. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_leaf_frontier.js +110 -0
  318. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +2 -27
  319. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -1
  320. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.js +120 -1564
  321. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts +26 -0
  322. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.d.ts.map +1 -0
  323. package/src/shade/renderer/geometry/virtual/build/level/vgeo_compute_group_boundary.js +123 -0
  324. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts +26 -0
  325. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.d.ts.map +1 -0
  326. package/src/shade/renderer/geometry/virtual/build/level/vgeo_error_bands.js +78 -0
  327. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts +24 -0
  328. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.d.ts.map +1 -0
  329. package/src/shade/renderer/geometry/virtual/build/level/vgeo_partition_into_groups.js +337 -0
  330. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts +29 -0
  331. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.d.ts.map +1 -0
  332. package/src/shade/renderer/geometry/virtual/build/level/vgeo_run_round.js +147 -0
  333. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts +37 -0
  334. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.d.ts.map +1 -0
  335. package/src/shade/renderer/geometry/virtual/build/level/vgeo_stitch_frontier.js +243 -0
  336. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts +29 -0
  337. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.d.ts.map +1 -0
  338. package/src/shade/renderer/geometry/virtual/build/level/vgeo_try_repack.js +117 -0
  339. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts +46 -0
  340. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_CREASE_ANGLE.d.ts.map +1 -0
  341. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts +74 -0
  342. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.d.ts.map +1 -0
  343. package/src/shade/renderer/geometry/virtual/build/mesh/VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS.js +78 -0
  344. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +12 -7
  345. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -1
  346. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.js +33 -11
  347. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts +58 -0
  348. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.d.ts.map +1 -0
  349. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_layout.js +79 -0
  350. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts +39 -0
  351. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.d.ts.map +1 -0
  352. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_read.js +68 -0
  353. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts +53 -0
  354. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.d.ts.map +1 -0
  355. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_attribute_vector_write.js +243 -0
  356. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +32 -7
  357. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -1
  358. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.js +243 -24
  359. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +12 -8
  360. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -1
  361. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.js +58 -50
  362. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +5 -0
  363. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -1
  364. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.js +13 -1
  365. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts +38 -0
  366. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.d.ts.map +1 -0
  367. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_resolve_attribute_weights.js +81 -0
  368. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +23 -5
  369. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -1
  370. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.js +28 -21
  371. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.js +1 -1
  372. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -1
  373. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.js +224 -60
  374. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +15 -0
  375. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -1
  376. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.js +17 -0
  377. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +17 -0
  378. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -1
  379. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.js +19 -0
  380. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +10 -1
  381. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -1
  382. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.js +103 -9
  383. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +14 -2
  384. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -1
  385. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.js +38 -14
  386. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts +6 -2
  387. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  388. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +27 -4
  389. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts +32 -0
  390. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.d.ts.map +1 -0
  391. package/src/shade/renderer/geometry/virtual/build/vgeo_subtree_bounds.js +93 -0
  392. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +20 -6
  393. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  394. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +22 -8
  395. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts +16 -0
  396. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_corner.d.ts.map +1 -0
  397. package/src/shade/renderer/geometry/virtual/format/attribute/{vgeo_layers_copy_vertex.js → vgeo_layers_copy_corner.js} +8 -8
  398. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts +18 -7
  399. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.d.ts.map +1 -1
  400. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_declare.js +19 -8
  401. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.d.ts +3 -3
  402. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_like.js +3 -3
  403. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts +27 -0
  404. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.d.ts.map +1 -0
  405. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_mesh_wedges.js +97 -0
  406. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts +7 -3
  407. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.d.ts.map +1 -1
  408. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET.js +21 -11
  409. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts +21 -0
  410. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.d.ts.map +1 -0
  411. package/src/shade/renderer/geometry/virtual/format/cluster/VGEO_CLUSTER_OFFSET_V4.js +41 -0
  412. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts +19 -1
  413. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts.map +1 -1
  414. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +20 -1
  415. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts +25 -0
  416. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.d.ts.map +1 -0
  417. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_DEFLATE.js +24 -0
  418. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts +5 -0
  419. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.d.ts.map +1 -1
  420. package/src/shade/renderer/geometry/virtual/format/frame/VGEO_CODEC_LZ4.js +5 -0
  421. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.d.ts.map +1 -1
  422. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_codec_name.js +5 -0
  423. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts +10 -3
  424. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.d.ts.map +1 -1
  425. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_decode_frame_blob.js +37 -3
  426. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts +21 -13
  427. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts.map +1 -1
  428. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +37 -13
  429. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts +8 -2
  430. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.d.ts.map +1 -1
  431. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_frame_decode_bound.js +14 -2
  432. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts +18 -0
  433. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts.map +1 -1
  434. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +19 -1
  435. package/src/shade/renderer/geometry/virtual/format/header/VGEO_HEADER_OFFSET.js +6 -0
  436. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts +21 -0
  437. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.d.ts.map +1 -0
  438. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MAX_LEVEL_COUNT.js +20 -0
  439. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts +9 -0
  440. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.d.ts.map +1 -1
  441. package/src/shade/renderer/geometry/virtual/format/header/VGEO_MIN_READER_VERSION.js +10 -1
  442. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.d.ts +1 -2
  443. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +10 -3
  444. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts +20 -0
  445. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.d.ts.map +1 -0
  446. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET_V5.js +43 -0
  447. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +4 -2
  448. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  449. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +8 -4
  450. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  451. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +2 -2
  452. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts +6 -4
  453. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.d.ts.map +1 -1
  454. package/src/shade/renderer/geometry/virtual/format/read/VGeoDirectory.js +6 -4
  455. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +13 -2
  456. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  457. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +13 -2
  458. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts +5 -1
  459. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts.map +1 -1
  460. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.js +14 -1
  461. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts.map +1 -1
  462. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.js +1 -0
  463. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts +4 -1
  464. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.d.ts.map +1 -1
  465. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_directory.js +53 -11
  466. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  467. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +7 -3
  468. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts +12 -2
  469. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  470. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +111 -33
  471. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts +6 -2
  472. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  473. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +112 -42
  474. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts +2 -2
  475. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  476. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +51 -21
  477. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +385 -0
  478. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -1
  479. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +9 -0
  480. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  481. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +14 -1
  482. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  483. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +34 -0
  484. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -1
  485. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +18 -0
  486. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  487. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +47 -19
  488. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +1 -0
  489. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +22 -4
  490. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +7 -2
  491. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -1
  492. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +8 -3
  493. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +1 -0
  494. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +10 -0
  495. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +1 -1
  496. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +13 -11
  497. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -1
  498. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +13 -11
  499. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -1
  500. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +4 -0
  501. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.d.ts.map +1 -1
  502. package/src/shade/renderer/global_illumination/probe/octahedral/GPUProbeAtlas.js +12 -3
  503. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  504. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +3 -4
  505. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts +6 -2
  506. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.d.ts.map +1 -1
  507. package/src/shade/renderer/gpu_primitive/indirect/IndirectComputeCommandBuildPass.js +90 -12
  508. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts +22 -0
  509. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.d.ts.map +1 -0
  510. package/src/shade/renderer/gpu_primitive/indirect/WEBGPU_GUARANTEED_MAX_COMPUTE_WORKGROUPS_PER_DIMENSION.js +21 -0
  511. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.d.ts.map +1 -1
  512. package/src/shade/renderer/light/environment/rgbe/RGBE_ReadPixels_RLE.js +7 -1
  513. package/src/shade/renderer/lightmap/GPULightMap.d.ts.map +1 -1
  514. package/src/shade/renderer/lightmap/GPULightMap.js +349 -336
  515. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  516. package/src/shade/renderer/loader/gltf/load_gltf.js +1137 -1128
  517. package/src/shade/renderer/particles/DESIGN.md +14 -5
  518. package/src/shade/renderer/particles/GPUParticleSystem.d.ts +25 -10
  519. package/src/shade/renderer/particles/GPUParticleSystem.d.ts.map +1 -1
  520. package/src/shade/renderer/particles/GPUParticleSystem.js +182 -38
  521. package/src/shade/renderer/particles/graph/groups/simplex_reference.d.ts.map +1 -1
  522. package/src/shade/renderer/particles/graph/groups/simplex_reference.js +16 -10
  523. package/src/shade/renderer/particles/graph_particles.d.ts +4 -2
  524. package/src/shade/renderer/particles/graph_particles.d.ts.map +1 -1
  525. package/src/shade/renderer/particles/graph_particles.js +8 -2
  526. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.d.ts.map +1 -1
  527. package/src/shade/renderer/particles/optimizer/particle_vm_semantics.js +8 -2
  528. package/src/shade/renderer/particles/optimizer/pass_fold_constants.d.ts.map +1 -1
  529. package/src/shade/renderer/particles/optimizer/pass_fold_constants.js +10 -4
  530. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  531. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +13 -8
  532. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts +31 -0
  533. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.d.ts.map +1 -0
  534. package/src/shade/renderer/particles/shaders/shader_particle_init_free_list.js +62 -0
  535. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  536. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -6
  537. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts +13 -0
  538. package/src/shade/renderer/particles/vm/ParticleVMReference.d.ts.map +1 -1
  539. package/src/shade/renderer/particles/vm/ParticleVMReference.js +95 -17
  540. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  541. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +4 -12
  542. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts +21 -0
  543. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.d.ts.map +1 -0
  544. package/src/shade/renderer/rasterize/bucket/rasterization_has_opaque_bucket.js +28 -0
  545. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +0 -2
  546. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  547. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +4 -5
  548. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  549. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +5 -3
  550. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.d.ts.map +1 -1
  551. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way.js +11 -4
  552. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  553. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +4 -3
  554. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.d.ts.map +1 -1
  555. package/src/shade/renderer/rasterize/expand/bucket/meshlet/graph_sort_meshlets_by_bucket_id.js +3 -0
  556. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.d.ts.map +1 -1
  557. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_count_by_rasterization_bucket.js +94 -90
  558. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  559. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +13 -5
  560. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.d.ts.map +1 -1
  561. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_rasterization_bucket_sort.js +96 -89
  562. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.d.ts.map +1 -1
  563. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_one_way.js +3 -1
  564. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.d.ts.map +1 -1
  565. package/src/shade/renderer/rasterize/expand/graph_meshlets_filter_two_way.js +4 -1
  566. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.d.ts.map +1 -1
  567. package/src/shade/renderer/rasterize/expand/mesh/shader_mesh_expand_to_meshlets_prefix.js +100 -89
  568. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.d.ts.map +1 -1
  569. package/src/shade/renderer/rasterize/expand/mesh/shader_prefix_sum_to_command.js +31 -7
  570. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.d.ts.map +1 -1
  571. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.js +1 -7
  572. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.d.ts.map +1 -1
  573. package/src/shade/renderer/rasterize/expand/pass1/filter_meshlets.2-way.compute.js +161 -153
  574. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  575. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +2 -7
  576. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  577. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +19 -2
  578. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  579. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +4 -8
  580. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  581. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +5 -8
  582. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.d.ts.map +1 -1
  583. package/src/shade/renderer/rasterize/standard/graph_rasterize_scene.js +19 -2
  584. package/src/shade/renderer/shader/type/WebGPUStruct.d.ts.map +1 -1
  585. package/src/shade/renderer/shader/type/WebGPUStruct.js +12 -0
  586. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts +25 -0
  587. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.d.ts.map +1 -0
  588. package/src/shade/renderer/shadow/map/DEFAULT_SHADOWMAP_UPDATE_BUDGET.js +24 -0
  589. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts +10 -0
  590. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  591. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +30 -21
  592. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts +8 -2
  593. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.d.ts.map +1 -1
  594. package/src/shade/renderer/texture/mipmaps/WebGPUMipmapGenerator.js +41 -6
  595. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +2383 -2356
  596. package/src/view/tooltip/DomTooltipManager.d.ts +7 -0
  597. package/src/view/tooltip/DomTooltipManager.d.ts.map +1 -1
  598. package/src/view/tooltip/DomTooltipManager.js +22 -0
  599. package/src/view/tooltip/DomTooltipObserver.d.ts.map +1 -1
  600. package/src/view/tooltip/DomTooltipObserver.js +13 -3
  601. package/src/core/model/node-graph/visual/layout/LayoutBoxGraph.d.ts.map +0 -1
  602. package/src/core/model/node-graph/visual/layout/layout_assign_coordinates.d.ts.map +0 -1
  603. package/src/core/model/node-graph/visual/layout/layout_assign_layers.d.ts.map +0 -1
  604. package/src/core/model/node-graph/visual/layout/layout_break_cycles.d.ts.map +0 -1
  605. package/src/core/model/node-graph/visual/layout/layout_insert_lanes.d.ts.map +0 -1
  606. package/src/core/model/node-graph/visual/layout/layout_order_layers.d.ts.map +0 -1
  607. package/src/core/model/node-graph/visual/layout/layout_orient_links.d.ts.map +0 -1
  608. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts +0 -61
  609. package/src/shade/playground/vgeo_viewer/cut_geometry.d.ts.map +0 -1
  610. package/src/shade/playground/vgeo_viewer/main.d.ts.map +0 -1
  611. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts +0 -18
  612. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +0 -1
  613. package/src/shade/playground/vgeo_viewer/select_cut.d.ts +0 -173
  614. package/src/shade/playground/vgeo_viewer/select_cut.d.ts.map +0 -1
  615. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.d.ts +0 -52
  616. package/src/shade/renderer/geometry/virtual/build/VGeoFaceGraph.d.ts.map +0 -1
  617. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts +0 -24
  618. package/src/shade/renderer/geometry/virtual/build/VGeoGatheredFaces.d.ts.map +0 -1
  619. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.d.ts +0 -32
  620. package/src/shade/renderer/geometry/virtual/build/VGeoLevel.d.ts.map +0 -1
  621. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.d.ts +0 -59
  622. package/src/shade/renderer/geometry/virtual/build/VGeoLevelOptions.d.ts.map +0 -1
  623. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.d.ts +0 -44
  624. package/src/shade/renderer/geometry/virtual/build/VGeoLevelSummary.d.ts.map +0 -1
  625. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts +0 -20
  626. package/src/shade/renderer/geometry/virtual/build/VGeoSubmesh.d.ts.map +0 -1
  627. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.d.ts +0 -73
  628. package/src/shade/renderer/geometry/virtual/build/VirtualGeometryMesh.d.ts.map +0 -1
  629. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +0 -13
  630. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +0 -1
  631. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.d.ts +0 -12
  632. package/src/shade/renderer/geometry/virtual/build/vgeo_bounding_sphere.d.ts.map +0 -1
  633. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts +0 -36
  634. package/src/shade/renderer/geometry/virtual/build/vgeo_build_levels.d.ts.map +0 -1
  635. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.d.ts +0 -13
  636. package/src/shade/renderer/geometry/virtual/build/vgeo_build_page_nodes.d.ts.map +0 -1
  637. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.d.ts +0 -19
  638. package/src/shade/renderer/geometry/virtual/build/vgeo_build_root_group.d.ts.map +0 -1
  639. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts +0 -29
  640. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_normals.d.ts.map +0 -1
  641. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.d.ts +0 -31
  642. package/src/shade/renderer/geometry/virtual/build/vgeo_compute_vertex_tangents.d.ts.map +0 -1
  643. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.d.ts +0 -18
  644. package/src/shade/renderer/geometry/virtual/build/vgeo_containing_sphere.d.ts.map +0 -1
  645. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts +0 -14
  646. package/src/shade/renderer/geometry/virtual/build/vgeo_extract_submesh.d.ts.map +0 -1
  647. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts +0 -12
  648. package/src/shade/renderer/geometry/virtual/build/vgeo_gather_faces.d.ts.map +0 -1
  649. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.d.ts +0 -14
  650. package/src/shade/renderer/geometry/virtual/build/vgeo_graph_components.d.ts.map +0 -1
  651. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.d.ts +0 -11
  652. package/src/shade/renderer/geometry/virtual/build/vgeo_narrow_indices.d.ts.map +0 -1
  653. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.d.ts +0 -46
  654. package/src/shade/renderer/geometry/virtual/build/vgeo_normalize_source.d.ts.map +0 -1
  655. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +0 -22
  656. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +0 -1
  657. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.d.ts +0 -29
  658. package/src/shade/renderer/geometry/virtual/build/vgeo_partition_graph.d.ts.map +0 -1
  659. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.d.ts +0 -9
  660. package/src/shade/renderer/geometry/virtual/build/vgeo_sphere_inflate_to_contain.d.ts.map +0 -1
  661. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.d.ts +0 -10
  662. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set.d.ts.map +0 -1
  663. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.d.ts +0 -15
  664. package/src/shade/renderer/geometry/virtual/build/vgeo_split_face_set_packed.d.ts.map +0 -1
  665. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts +0 -16
  666. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_layers_copy_vertex.d.ts.map +0 -1
  667. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts +0 -10
  668. package/src/shade/renderer/loader/gltf/fix_up_material_sides.d.ts.map +0 -1
  669. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts +0 -17
  670. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.d.ts.map +0 -1
  671. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts +0 -3
  672. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.d.ts.map +0 -1
  673. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts +0 -8
  674. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.d.ts.map +0 -1
  675. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_coordinates.d.ts +0 -0
  676. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_assign_layers.d.ts +0 -0
  677. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.d.ts +0 -0
  678. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_break_cycles.js +0 -0
  679. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.d.ts +0 -0
  680. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_insert_lanes.js +0 -0
  681. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.d.ts +0 -0
  682. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_order_layers.js +0 -0
  683. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.d.ts +0 -0
  684. /package/src/core/{model/node-graph/visual/layout → graph/layout/layered}/layout_orient_links.js +0 -0
  685. /package/src/shade/playground/{vgeo_viewer → caldera}/main.d.ts +0 -0
@@ -0,0 +1,663 @@
1
+ # GPUMeshletManager — compaction under churn
2
+
3
+ *2026-09-16. Revision 3: built. Revision 2 was the reviewed plan (§9); §8 has what each row
4
+ measured and the commit that landed it, §10 what the build changed against the plan.*
5
+
6
+ `GPUMeshletManager` is the shared meshlet pool: static meshes, skinned clones
7
+ (`clone_batch_allocation`), dynamic meshes, the scene editor's asset swaps, and the virtual-geometry
8
+ tier's pages all live in its two arenas. Virtual geometry is the motivator here — it is the first
9
+ client to churn the pool at tens of allocations a second — but nothing below is shaped to it. A
10
+ page is a batch like any other; the plan treats "many near-uniform batches coming and going around
11
+ a few large ones that stay, with the occasional large one swapped" as the load, and that load is a
12
+ skinned crowd's respawns or an editor's asset swaps as much as it is a page tier.
13
+
14
+ The defect: `compact()` is a two-buffer rebuild that re-allocates and re-uploads *every* record and
15
+ fires `changed` for every one that moved, O(all batches) in one event, and the allocation path
16
+ calls it whenever a request is refused with enough free space behind it. On the Zorah container
17
+ (4,096 page slots, ~60 KB pages, ~34 clusters each) a two-round tour of eight rosettes turned over
18
+ 3,009 pages, and the only main-thread tasks over 50 ms — four of them, 79 to 115 ms — were each a
19
+ `compact()` (RUNTIME_PLAN_2026_09_14 §7). Everything else the residency does per reading is
20
+ microseconds.
21
+
22
+ ## 0. Decision
23
+
24
+ Four changes, landed and measured in two steps (§8):
25
+
26
+ 1. **Quantize allocations to the allocator's bins.** Every request is rounded up to
27
+ `allocator_run_size_for(size)` before it reaches the `OffsetAllocator`. A hole left by a freed
28
+ batch is then exactly a bin, and any request of the same class is accepted from it. This removes
29
+ the "fitting hole refused" class of refusal (63–80 % of refusals measured, §2.1) and most of the
30
+ splintering behind the rest, at 3.4 % of data words and 3.9 % of metadata rows on page-shaped
31
+ sizes (worst case 12.5 %, the allocator's own bin ratio).
32
+ 2. **A byte-level metadata patch** in place of the per-row struct serde, for `add_batch` and the
33
+ rebuild alike: the rows are packed at 40 bytes with `address` at word 6, so patching a batch is a
34
+ copy and one add per row. This is most of the 28 µs a record the rebuild costs today (§2.2), and
35
+ `add_batch` pays the same serde on every promotion.
36
+ 3. **Refusal policy: grow, never re-pack to zero headroom.** A refused request grows the arena
37
+ (which leaves 25 % headroom and moves nothing) and, when the refusal was fragmentation, arms the
38
+ sweep. At most one growth per sweep episode; a second fragmentation refusal before the sweep has
39
+ finished falls back to the rebuild, which is now bounded by step 2. The full rebuild stays
40
+ public as `compact()` and is the last resort when the device's buffer limit stops growth.
41
+ 4. **A sliding, budgeted compactor** for the fragmentation that quantization does not prevent —
42
+ mixed size classes, a large asset swapped under small churn: an O(1) `OffsetAllocator#slide_down`
43
+ primitive swaps a used range with the free range before it; the manager walks a cursor up each
44
+ arena and slides records down over the free space that gathers behind the cursor, at most
45
+ `MOVE_BUDGET` records and `WORD_BUDGET` words per `sweep(cmd_ctx)`, which the renderer calls once
46
+ per frame. A moved record's bytes — data *and* metadata rows — go through a transient staging
47
+ buffer (two `copyBufferToBuffer`, so an overlapping move is well-defined and GPU-written rows
48
+ survive); a data move also patches the rows' `address` with the address-patch shader the clone
49
+ flush already uses; `changed` fires. Per-frame cost is bounded by the budget, not by the
50
+ resident count.
51
+
52
+ Steps 1–3 are landed first and measured (§8, C2/C4): under the motivating load they leave the sweep
53
+ with nothing to do, and that is recorded rather than assumed. Step 4 is then built against a load
54
+ that needs it (§2.4) and proved on that load.
55
+
56
+ Everything a consumer has to do is in §5: `changed` keeps its meaning and gains a per-frame bound;
57
+ `GPUGeometryManager` patches the one row a move touches instead of logging a warning;
58
+ `GPUMeshSkinningContext` starts listening, which it never did.
59
+
60
+ ## 1. The manager today
61
+
62
+ Two `OffsetAllocator`s, data (words) and metadata (rows), over two `GPUBuffer`s. A record holds an
63
+ `Allocation` in each. `#allocate_data_and_metadata` tries both, and on refusal of either:
64
+
65
+ - if `#deallocations_since_last_rebuild > 0` and the refused allocator's `freeStorage` is at least
66
+ `allocator_run_size_for(size)` — i.e. a rebuild *would* hand the request one run — it calls
67
+ `compact()`;
68
+ - otherwise it calls `grow()`, which appends and moves nothing.
69
+
70
+ `compact()` = `#rebuild`: new buffer pair, fresh allocators, every record re-allocated in insertion
71
+ order, one `copyBufferToBuffer` per record for data, one CPU re-encode and `writeBuffer` per record
72
+ for metadata (`#write_patched_metadata`, generic struct serde per row), `changed.send0()` for every
73
+ record whose data offset changed. Node budget re-picked. The new arenas are the old *sizes*: a
74
+ rebuild adds no space, it only packs — to whatever headroom the last growth left.
75
+
76
+ Consumers of a record (§5): `GPUGeometryManager` writes `metadata.offset` into the geometry table
77
+ and answers `changed` with a `console.warn` and a TODO; `GPUVirtualGeometryPool` writes it into a
78
+ page row and follows `changed` correctly; `GPUMeshSkinningContext` caches both offsets in a binding
79
+ and keys a per-meshlet table by absolute clone row, and does not listen. BLAS leaves carry
80
+ batch-local meshlet indices and need nothing.
81
+
82
+ ## 2. Measurements (before touching anything)
83
+
84
+ Node, `SoftwareGPUDevice`, synthetic churn: a resident set of `N` "page" batches drawn uniformly
85
+ from 14,000–16,000 words and 30–38 rows (the Zorah shape: ~60 KB, ~34 clusters), plus six static
86
+ batches of 33 K to 2.4 M words that stay for the whole run; then `2N` churn events, each evicting a
87
+ uniformly random resident page and adding a fresh one. The driver is `churn_bench.mjs` in the
88
+ session scratchpad; §6 checks the load-bearing numbers in as a bench spec. The real tier evicts in
89
+ CLOCK order, which fragments *less* (62 raw refusals against 255 uniform at 12 % headroom, 4,096
90
+ resident); uniform eviction is the conservative choice.
91
+
92
+ The bare-allocator runs below are at a quarter of the real sizes and the manager runs had to be
93
+ (the software device caps `maxBufferSize` at 256 MB and 4,096 real pages are 245 MB before
94
+ headroom). The allocator's bins are a 3-bit float, so a quarter-scale page draws the same three
95
+ bins one exponent lower (3,584 / 3,840 / 4,096 at 17 / 51 / 32 % against 14,336 / 15,360 / 16,384
96
+ at 17 / 51 / 32 %); the real-scale refusal counts are within 20 % of the quarter-scale ones in
97
+ every column and split the same way.
98
+
99
+ ### 2.1 Why near-uniform churn fragments an `OffsetAllocator` at all
100
+
101
+ Bare allocator, arena = live × 1.12, refusal classified by the true hole structure computed from
102
+ the live ranges:
103
+
104
+ | resident | churn | requests | refused | of which a *fitting* hole was refused | of which true fragmentation | holes at end |
105
+ |---:|---:|---|---:|---:|---:|---:|
106
+ | 512 | 1,024 | raw | 15 | 12 | 3 | 253 |
107
+ | 512 | 1,024 | quantized | 0 | 0 | 0 | 56 |
108
+ | 1,024 | 2,048 | raw | 54 | 40 | 14 | 478 |
109
+ | 1,024 | 2,048 | quantized | 0 | 0 | 0 | 109 |
110
+ | 2,048 | 4,096 | raw | 139 | 96 | 43 | 959 |
111
+ | 2,048 | 4,096 | quantized | 0 | 0 | 0 | 115 |
112
+ | 4,096 | 8,192 | raw | 269 | 186 | 83 | 1,895 |
113
+ | 4,096 | 8,192 | quantized | 0 | 0 | 0 | 194 |
114
+
115
+ Two mechanisms, and both are the bins:
116
+
117
+ - **A hole serves only the bin below its own.** A free run is filed under the largest bin that fits
118
+ *inside* it (`uintToFloatRoundDown`); a request is looked up from the smallest bin that fits the
119
+ *request* (`uintToFloatRoundUp`). Bins at 8,192–16,384 words are 1,024 apart, so a hole of
120
+ 15,300 is filed under 14,336 and serves nothing above 14,336. With sizes spread ±7 % around
121
+ 15,000 a hole of the same class as the request fails it more often than not. This is
122
+ `allocator_run_size_for`'s note ("a run of 68 is filed under 64, a request for 68 looks from 72")
123
+ met at scale: 63–80 % of refusals had a hole that fit.
124
+ - **Raw-size requests splinter holes.** A 14,200-word request accepted from a 15,300 hole leaves
125
+ 1,100 free words no page will ever use; the count of holes grows to half the resident count
126
+ (1,895 at 4,096) and free space that is plentiful in total is useless in any one place: the
127
+ remaining 20–37 % of refusals.
128
+
129
+ Quantizing the request to `allocator_run_size_for(size)` closes the first entirely — for every
130
+ size, `roundDown(run_size_for(s)) === roundUp(s)` (checked exhaustively to 2²² and at the u32
131
+ edges), so a freed run of a class is looked up from exactly that class's bin — and most of the
132
+ second: a bin-sized request consumes an *uncoalesced* bin-sized hole whole. Coalesced holes still
133
+ splinter (two freed 16,384 runs make 32,768, a 15,360 request leaves 17,408, a 16,384 request from
134
+ that leaves a 1,024 splinter), which is what the 56–194 residual holes are. Zero refusals over
135
+ 8,192 events at every size; holes down 4–10×. Overhead measured over 100,000 page-shaped draws:
136
+ **3.41 % of data words, 3.93 % of rows**; 4.3 % mean over log-uniform sizes; 12.06 % worst over
137
+ 1 K–4 M. Bin-sized and power-of-two clients pay nothing, and bins are fixed points of
138
+ `allocator_run_size_for`, so `grow_target_for` and the growth ladder are unaffected.
139
+
140
+ ### 2.2 How often the manager compacts, and what it costs
141
+
142
+ `GPUMeshletManager` as it stands, `compact()` wrapped and timed (wrapping is fine in a scratch
143
+ driver; the bench spec reads the same facts off `stats`, §4.7):
144
+
145
+ | resident | churn events | arena headroom at churn start | compactions | events per compaction | compact p50 | compact max | per record | growths |
146
+ |---:|---:|---:|---:|---:|---:|---:|---:|---:|
147
+ | 512 | 1,024 | 190 % | 0 | — | — | — | — | 0 |
148
+ | 1,024 | 2,048 | 78 % | 0 | — | — | — | — | 0 |
149
+ | 2,048 | 4,096 | 21 % | 0 | — | — | — | — | 1 |
150
+ | 3,300 | 6,600 | 22.6 % | 5 | 1,320 | | | | |
151
+ | 3,700 | 7,400 | 10.3 % | 6 | 1,233 | | | | |
152
+ | 4,096 | 8,192 | **0.2 %** | **356** | **23** | **115 ms** | 209 ms | **28 µs** | 3 |
153
+
154
+ (The 3,300 and 3,700 rows are the policy reviewer's re-run of the same driver with the fill varied
155
+ against the same 62.5 MB arena.)
156
+
157
+ The per-record figure matches the Zorah trace (79–115 ms at ~4,100 records is 19–28 µs a record)
158
+ — the cost is the CPU re-encode and upload of every record's rows, not the copy. Compaction was
159
+ 99 % of the churn's wall time at 4,096; every `add_batch` that did not compact took 30–50 µs (the
160
+ same serde, once) and every `remove_batch` 1.5–3.5 µs.
161
+
162
+ Headroom is the variable. Growth is `max(grow_target_for, +16 K words, ×1.25)`, a ladder of
163
+ 32 → 40 → 50 → 62.5 MB; where the fill stops on that ladder is where the headroom lands, and a
164
+ rebuild re-packs to the same size, so it *restores* that headroom rather than adding to it. The
165
+ Zorah tour's four compactions over ~3,009 turnovers (≈ 750 events each) sit between the 10 % and
166
+ 23 % rows; the 0.2 % row is what a fill that lands just under a rung looks like — 23 events per
167
+ rebuild — and it is one ladder step away from any scene.
168
+
169
+ ### 2.3 Headroom against refusals
170
+
171
+ Bare allocator at 4,096 resident, headroom over *reserved* words (the quantized arena sized from
172
+ the quantized live set; the first draft sized it from raw words and read 3.4 % low):
173
+
174
+ | headroom | raw: refused (fitting / fragmented) | quantized: refused |
175
+ |---:|---|---:|
176
+ | 0.3 % | 748 (477 / 271) | 50 |
177
+ | 1 % | 703 (442 / 261) | 15 |
178
+ | 3 % | 639 (406 / 233) | **0** |
179
+ | 6 % | 516 (322 / 194) | **0** |
180
+ | 12 % | 269 (186 / 83) | **0** |
181
+
182
+ Quantized requests need ~3 % headroom over reserved (≈ 6.5 % over true bytes) to never refuse
183
+ under this load; raw requests refuse at every headroom. So the policy has to leave headroom
184
+ (growth's 25 % does) and must not pack it away.
185
+
186
+ ### 2.4 A load the sweep is for
187
+
188
+ Under §2.1–2.3, quantization plus grow-on-refusal leaves the motivating load with zero refusals,
189
+ so the sweep would be exercised by nothing above. The load it is for is **mixed classes with a
190
+ large record swapped under small churn**: the page churn of §2 plus, every `K` events, one of the
191
+ six statics removed and re-added one class larger (an editor swapping an asset; a crowd's new
192
+ character type). Each swap's request finds its old hole one class too small and the small churn
193
+ has carved the tail. Numbers for that load — refusals, growths, rebuilds with steps 1–3 alone, and
194
+ then with the sweep — are §8's C3 row's to fill; the bench spec runs it (§6).
195
+
196
+ ## 3. Alternatives, with numbers
197
+
198
+ | option | what it buys | what it costs | verdict |
199
+ |---|---|---|---|
200
+ | **Quantize requests to bins** (manager-side, `allocator_run_size_for`) | refusals under near-uniform churn: 269 → 0 at 4,096; holes 1,895 → 194; no allocator change; the rebuild's sequential packing becomes exact (a bin request ≤ a remainder is ≤ the remainder's bin), closing a latent refusal `#rebuild` can assert on today | 3.4 % data / 3.9 % rows on page-shaped sizes; 4.3 % mean; ≤ 12.5 % worst | **take**. Generic: every recurring size class benefits, every client pays the same bounded ratio |
201
+ | **Byte-level row patch** | rebuild and `add_batch` per-record cost 28 µs → the copy and one add per row | none | **take** |
202
+ | **Quantize + grow-on-refusal + fast rebuild, no sweep** | zero refusals under the motivating load; a rebuild, when one fires, at ~1/6 the cost | fragmentation that is not near-uniform still ends in an O(n) event | **taken as the first step**; measured before the sweep is built, so the sweep is proved against what remains |
203
+ | Fix the lookup in `OffsetAllocator` (also walk the bin below, first node ≥ size, bounded to K) | zero overhead | probabilistic (holes in one bin are ~uniform in [bin, next), a request in the upper part misses most of them; K = 8 finds one ~97 % of the time under this load and less under others), changes a shared O(1) allocator's contract, does nothing about splintering | reject |
204
+ | Size classes / regions per client | perfect recycling for one class | the manager would have to know the classes — exactly bending it to one client's shape; mixed clients still fragment across regions | reject; quantization is the class-free form of the same idea |
205
+ | Grow instead of compact when fragmentation is not what refused | no-op with quantization: the "fitting hole refused" class stops existing, so every remaining refusal *is* fragmentation | — | subsumed by §4.4 |
206
+ | Incremental rebuild (the two-buffer rebuild spread over frames) | keeps the packing order | two buffers live for the duration and every shader binds one; not viable | reject |
207
+ | Re-allocate-based hole filling ("moving tail": `allocate`, copy, `free` the old range; no new primitive) | no allocator change | the allocator's bins are LIFO with no address order, so a re-allocation is not lower by construction, and it can never merge two splinters into one run — which is exactly the refusal quantization leaves | reject |
208
+ | **Sliding compaction in place** (§4.2–4.3) | O(1) allocator step, bounded moves per frame, no second buffer, the free space consolidates as one growing run behind the cursor | a new allocator primitive; a staged copy per moved record; `changed` per moved record within the budget | **take**, as the second step |
209
+ | A synchronous budgeted sweep before growing | avoids a growth when a few moves would do | the data move's address patch is a compute dispatch and needs a `ShadeGPUCommandContext`, which the manager cannot make from the `GPUDevice` it holds; a CPU-patched row re-upload instead would overwrite the bounds the skinning pass writes on the GPU | **not in this plan** (§7); the natural refinement once a `GraphicsContext` is threaded through `GPUGeometryManager` |
210
+ | Full rebuild only (status quo) | simplest | 115 ms every 23 events at 4,096 resident with 0.2 % headroom, 79–115 ms four times a tour on Zorah | the defect |
211
+
212
+ Budget, provisional until C3 measures a move: `MOVE_BUDGET = 64` records and
213
+ `WORD_BUDGET = 1,048,576` words (4 MB of data) per `sweep`, whichever is hit first, and never
214
+ less than one move when one is possible; `STEP_BUDGET = 4,096` cursor steps a call. A move is an
215
+ O(1) slide, four copy encodes, `meshlet_count` pairs in a patch table and one `changed`; at a
216
+ projected ~3–5 µs that is ≤ 0.35 ms of main thread a frame while a sweep runs, and a sweep of
217
+ 4,096 moved records completes in 64 frames. The constants are counts because that is what the
218
+ code bounds; §8's C3 row records the measured ms and MB they come to, and they are re-picked from
219
+ that. A forced move of a 2.4 M-word static (one move over `WORD_BUDGET`) is 19 MB of GPU copy and
220
+ a 5,600-pair patch — not a hitch.
221
+
222
+ ## 4. Design
223
+
224
+ ### 4.1 Quantization and the row patch
225
+
226
+ `#allocate_data_and_metadata(data_size_words, meshlet_count)` allocates
227
+ `allocator_run_size_for(data_size_words)` and `allocator_run_size_for(meshlet_count)`. The record
228
+ keeps `data_size` and `meshlet_count` as the true sizes — every copy, patch and readback uses
229
+ those — and the allocator's node keeps the rounded size, which is what `free` returns to the bins.
230
+ `#rebuild` rounds the same way. `add_batch` asserts both sizes non-zero (a zero-size node has no
231
+ place in a partition; `clone_batch_allocation` already asserts it). `gpu_memory_usage_occupied`
232
+ keeps reporting true bytes; `gpu_memory_usage_reserved` reports what the arenas have handed out,
233
+ so the overhead is a number rather than a claim.
234
+
235
+ `#write_patched_metadata` becomes a byte copy of `metadata_buffer_original` with one `u32` add at
236
+ word 6 of each 40-byte row (`MESHLET_METADATA_STRUCT` is packed, `size === aligned_size === 40`,
237
+ pinned by a spec that fails if the layout ever changes), then one `queue.writeBuffer`.
238
+
239
+ ### 4.2 `OffsetAllocator` primitives
240
+
241
+ Node indices are already API (`Allocation.metadata`, `free_node`). Four additions, all O(1), none
242
+ consuming a node:
243
+
244
+ - `slide_down(node_index) → delta`. `node_index` must be a used node. If its address-predecessor is
245
+ a free node of size `F`, the two swap places: the used node's offset drops by `F`, the free node's
246
+ offset rises by the used node's size; the free node's size — and therefore its bin — is
247
+ unchanged, so no bin-list surgery. If the free node now adjoins a free successor the two are
248
+ merged: both removed from their bins *before* either changes size (`#removeNodeFromBin` finds the
249
+ bin from the node's current size), then one combined node inserted — one node back on the
250
+ freelist. Head and tail tracking are updated. Returns `F`, or 0 when the predecessor is used or
251
+ absent. The merge is the common case, not the exception: every record whose allocation left a
252
+ remainder has a free successor until something fills it.
253
+ - `first_used_node() → node_index | ALLOCATOR_NO_SPACE`, `next_used_node(node_index) → …`. Address
254
+ order. Free nodes coalesce, so at most one free node sits between two used ones and each is a
255
+ walk of at most two links. `first_used_node` needs the node that starts the chain; `#head_node`
256
+ is the node whose `NEIGHBOR_PREV` is unused, maintained where the chain's first link changes
257
+ (`reset`, `free_node`'s combined node, `slide_down`) — from the link, not from the offset, so a
258
+ zero-size node at offset 0 cannot mislabel it.
259
+ - `can_allocate(size) → boolean`. Exactly `allocate(size)`'s own refusal test, without allocating:
260
+ the freelist is not empty *and* a bin at or above `roundUp(size)` is non-empty. Node exhaustion
261
+ is the one shortfall no arena size fixes, and a probe that ignored it would send the manager to
262
+ grow for nothing.
263
+
264
+ Invariants `slide_down` preserves, and the spec pins over a randomized sequence of allocate / free /
265
+ slide / grow against a reference model: the nodes partition `[0, size)`; free nodes never adjoin;
266
+ every free node is filed under `roundDown(size)` and every `usedBins` / `usedBinsTop` bit is set
267
+ iff its bin is non-empty; `freeStorage` is the sum of free node sizes; `freeOffset` is
268
+ `maxAllocs − 1 − nodes in the chain`; `#head_node` / `#tail_node` name the nodes starting / ending
269
+ the chain; no node is consumed by a slide and one is returned by a merge. `#tail_node`'s comment
270
+ that `#insertNodeIntoBin` is the only writer of an end offset is amended: `slide_down` is the
271
+ second, and a missed tail update would orphan the free node on the next `grow` silently — so the
272
+ tail spec asserts that after sliding a tail record of size `s` over a free `F` and growing by
273
+ `d`, `freeOffset` is unchanged (the free-tail path spends no node) and `allocate(F + d)` lands at
274
+ the free node's post-slide offset.
275
+
276
+ ### 4.3 The sweep
277
+
278
+ Per arena the manager keeps two pointers, each a used node's index or `NONE`: the **cursor** of
279
+ the running pass, and the **restart** — the used node after the lowest hole opened below the
280
+ running pass (or, with no pass running, since the arena was last packed). `remove_batch` keeps
281
+ both — before freeing: a pointer on the removed node moves to the node after it; a hole below the
282
+ cursor and below the restart's record moves the restart there. A pass that reaches the arena end
283
+ with a restart pending starts again from it, in the same episode; one with none ends the episode.
284
+ So a sweep needs no prefix walk to find its first hole, and holes opened under a running pass are
285
+ closed by the next pass in one walk. (Revision 2 stepped the cursor *back* on every removal below
286
+ it; under a pool that thins fast that sent it back under the records it had just packed on every
287
+ removal, and the pass never reached the end — §10.) `#rebuild` clears both. `grow` re-files the
288
+ tail free node and writes one used node's successor link; it renames nothing, so both survive it.
289
+
290
+ A sweep is **armed** by a fragmentation refusal (§4.4) and runs until the cursor reaches the arena
291
+ end. A step:
292
+
293
+ ```
294
+ node = cursor
295
+ delta = allocator.slide_down(node)
296
+ cursor = allocator.next_used_node(node) // NONE at the arena end: the sweep is done
297
+ if delta > 0: the record owning `node` moved down by `delta` → §4.3.1
298
+ ```
299
+
300
+ Because sliding puts the free space *after* the moved record, every hole the cursor has passed is
301
+ one free node immediately behind the cursor — the **bubble** — and every record from the first hole
302
+ onward slides over it; a sweep ends with all free space in one run at the tail. Allocations during
303
+ a sweep land wherever the bins say — below the cursor (already packed, since `allocate` hands out
304
+ the *start* of a run), in the bubble (it shrinks; the sweep continues), or above (the sweep
305
+ reaches it). A step that moves nothing (a hole the cursor was stepped back for that an allocation
306
+ has since filled) costs a link walk and counts against `STEP_BUDGET`.
307
+
308
+ Records are found from nodes by a per-arena `Map<node_index, record>`, maintained through one
309
+ `#release` path for every free the manager does — `remove_batch`, the partial-success release in
310
+ `#allocate_data_and_metadata`, `#rebuild` — because a freed node's index is immediately the merged
311
+ free node's and then the next allocation's, so a stale cursor would name a *different live record*
312
+ and never fail loudly. The step asserts `map.get(cursor).allocation.metadata === cursor`, writes
313
+ the cursor before `changed.send0()`, and re-reads it after — a handler may `remove_batch`.
314
+
315
+ #### 4.3.1 What a move does
316
+
317
+ The steps run CPU-side first over the whole batch — slides, record offsets, the list of
318
+ `(old, new, words)` per arena and the `(row, delta)` patch pairs — and the GPU work is recorded
319
+ after, from that list:
320
+
321
+ 1. One transient staging buffer (`COPY_SRC | COPY_DST`) sized to the largest moved range of this
322
+ batch; every move uses it from offset 0. Copies in one command buffer are ordered, so reusing
323
+ one region is correct, and a fixed descriptor is what `buffer_allocator_main` pools well.
324
+ 2. Per data move, in cursor order: `copyBufferToBuffer(data, old, stage, 0, n)`,
325
+ `copyBufferToBuffer(stage, 0, data, new, n)`. WebGPU refuses a same-buffer copy and the
326
+ intra-copy compute shader the clone flush uses is not overlap-safe; a slide by less than the
327
+ record's size overlaps. Two plain copies are well-defined, execute on the software device (so
328
+ a spec reads the bytes back), and cursor order means a record only ever slides into space an
329
+ earlier record vacated, whose read was recorded earlier.
330
+ 3. Per metadata move: the same two copies over the rows. The rows are copied, not re-derived
331
+ from `metadata_buffer_original`, because the skinning bounds chain writes `bounds_box` into
332
+ clone rows on the GPU every frame and a re-derivation would put rest-pose boxes over deformed
333
+ vertices — permanently for an unbound clone, which draws frozen at its last pose.
334
+ 4. Per data move: `meshlet_count` `(row, delta)` pairs, with `delta = (−F) >>> 0`; one
335
+ `shader_meshlet_metadata_address_patch` dispatch per batch, after the copies. Same shader,
336
+ same wrapping add, as the clone flush.
337
+ 5. `record.changed.send0()` for every moved record, after the whole batch is recorded.
338
+
339
+ Everything is in the one `cmd_ctx`, in order, so a pass recorded after `sweep` in the same context
340
+ — or in any later submit — reads rows and data that agree. `sweep` remembers the context it
341
+ recorded into and asserts it is `finished` before the manager allocates, frees, grows or rebuilds
342
+ again: a recorded copy names `old`, and an `add_batch` placed at `old` in the meantime uploads
343
+ with `queue.writeBuffer` *now*, ahead of the copy; a `grow` in the meantime would destroy the
344
+ buffer the copy names. No consumer does either today (§5 says where the calls sit), and the assert
345
+ is what keeps it that way.
346
+
347
+ ### 4.4 Refusal policy
348
+
349
+ `#allocate_data_and_metadata`, per attempt (three attempts, as today):
350
+
351
+ 1. Allocate both (quantized). Both land → done. A partial success is released through `#release`.
352
+ 2. For each refused arena, classify: **fragmented** if `freeStorage ≥ allocator_run_size_for(size)`
353
+ (a sweep can help: with that much free and a refusal there are at least two free nodes), else
354
+ **short**.
355
+ 3. Fragmented and no growth yet in the current sweep episode: arm the sweep if it is not armed,
356
+ `grow` (25 % headroom, moves nothing; `grow_target_for` is minimal and the manager's ladder
357
+ rounds it up), note the growth against the episode, retry.
358
+ 4. Fragmented and the episode already grew — a second fragmentation refusal before the sweep
359
+ finished, the case the reviewers named (an editor re-adding a large asset each time small churn
360
+ carves the tail) — `compact()`: bounded now by §4.1, ~3–5 µs a record, and it ends the episode.
361
+ Retry.
362
+ 5. Short: `grow`, retry. (A refusal with a full freelist is short by this test only if the arena
363
+ is full too; a node-exhausted allocator with room is fragmented-but-unsweepable — `grow` then
364
+ changes nothing, and attempt three's `compact()` re-picks the node budget. Stated so it is not
365
+ a surprise; §6 pins it.)
366
+ 6. Still refused (growth stopped at `maxBufferSize`): `compact()`, then the existing throw. On an
367
+ adapter that hands out 256 MB this is the status quo, at a sixth of the cost.
368
+
369
+ An episode is: armed by a fragmentation refusal **or by scatter** — a removal that leaves at least
370
+ an eighth of the arena free in pieces (free space less the largest free run, both from the bins,
371
+ O(1); and at least 16 K words or 512 rows that way) arms the sweep by itself, so the tail is clear
372
+ by the time a large arrival needs it rather than after the growth it forced (§10) — and ended by
373
+ the sweep reaching the arena end with no restart pending, or by a rebuild. Memory: the high-water mark is reserved × 1.25 plus the momentary old+new pair a `grow`
374
+ already costs; +25 % once per episode against the status quo's rebuild-in-place, then the sweep
375
+ puts that headroom at the tail where the next growth would otherwise be, so an episode that
376
+ recurs finds room before it refuses. On the Zorah ladder the arena is unchanged unless
377
+ quantization's +3.4 % crosses a rung. `#deallocations_since_last_rebuild` goes: growth guarantees
378
+ progress, and the counter's job — "do not rebuild for nothing" — is done by `freeStorage`.
379
+
380
+ `compact()` stays public and unchanged in meaning (full synchronous rebuild), for a caller at a
381
+ level boundary who wants the arenas packed now. The manager itself reaches it only in steps 4–6.
382
+
383
+ ### 4.5 `sweep(cmd_ctx)` and `update(cmd_ctx)`
384
+
385
+ Two methods, because `update` is called three times a frame in the renderer (`GraphicsContext.
386
+ update` at the head of `#begin_frame`, `GPUSceneContext.build`, and `GPUMeshSkinningContext.update`
387
+ directly) and a per-frame bound has to have one call site:
388
+
389
+ - `sweep(cmd_ctx)`: one budgeted step per arena (budgets shared across the two), nothing when no
390
+ sweep is armed. Called by `GraphicsContext.update` once per frame, before `geometries.update`.
391
+ - `update(cmd_ctx)`: the clone flush, as today. Reads whatever offsets the records hold, so a clone
392
+ queued before a sweep in the same frame copies from post-sweep offsets.
393
+
394
+ ### 4.6 What stays the same
395
+
396
+ Records, the `changed` signal, `add_batch`/`remove_batch`/`clone_batch_allocation`/`update`/
397
+ `grow`/`compact`/`destroy`, the buffers being replaced by `grow` and `compact` only, the O(1)
398
+ `remove_batch`, the pending-clone rules (a queued clone names records, and cancellation by either
399
+ record), `ALLOCATION_LIMIT`. A move consumes no allocator node.
400
+
401
+ ### 4.7 `stats`
402
+
403
+ A read-only `stats` object: `growths`, `rebuilds`, `moves`, `sweeps_armed`, `sweeps_completed`,
404
+ `moves_last_sweep`. Public because the bench spec, the tour and a host's overlay all need "did the
405
+ rebuild path fire" without a spy, and because a sweep that arms and never completes is a defect
406
+ nothing else would show.
407
+
408
+ ## 5. The consumers' contract
409
+
410
+ What the manager guarantees:
411
+
412
+ - **`changed` fires when, and only when, `record.data.offset` or `record.metadata.offset` has
413
+ changed**, synchronously, with the new offsets already on the record. A consumer that holds only
414
+ `metadata.offset` — every consumer today — compares before acting; a data-only move changes
415
+ nothing it holds. It fires from inside `sweep(cmd_ctx)` and from inside `compact()` (which an
416
+ allocation may reach, §4.4). (Today it fires only for a data move, from `compact()` alone; a
417
+ rebuild that moved a record's rows but not its data sent nothing, which the VG pool would have
418
+ missed.)
419
+ - **Per `sweep` call, at most `MOVE_BUDGET` records move**, and the renderer calls it once per
420
+ frame. A `compact()` moves every record; the manager reaches it only as §4.4 says, and `stats`
421
+ counts it.
422
+ - **The bytes are where the offsets say by the time anything recorded after the move runs.** A
423
+ move recorded into `sweep`'s `cmd_ctx` executes with that command buffer, rows and data and
424
+ address patch in order; a consumer that publishes an address with `queue.writeBuffer` any time
425
+ before that command buffer is submitted is consistent, and one that records an address into a
426
+ command buffer must record after `sweep` — which is the order the renderer has: `sweep` at the
427
+ head of `Renderer.#begin_frame`, the VG runtime's `begin_frame` flush (a `FrameStart` graph
428
+ extension) and every pass after it. VG installs, the one allocating path that is not a scene
429
+ build, run from a readback promise between frames.
430
+ - **Nothing allocates, frees, grows or rebuilds between a `sweep` that recorded moves and that
431
+ context's `finish`** — asserted by the manager (§4.3.1). The renderer's three contexts each
432
+ finish with no allocation in between.
433
+
434
+ What each consumer does:
435
+
436
+ | consumer | holds | today | change |
437
+ |---|---|---|---|
438
+ | `GPUGeometryManager` | `metadata.offset` in the geometry table row per source and per clone | `console.warn` + TODO on sources; no listener on clones | the `changed` handler compares the offset and `queue.writeBuffer`s that one `GEOMETRY_METADATA_STRUCT` row into `buffer_metadata` — inside the handler, not deferred to its own `update`, because the skinning context's direct `meshlets.update` is not followed by one and a deferred patch would leave the row stale for the frame; skipped when `__needs_update` is set (the rebuild writes every row) or the row is past the buffer (same). Clones get the listener too; `remove`/`remove_clone` remove it. |
439
+ | `GPUVirtualGeometryPool` | `MESHLET_BASE` per page row | listens; writes the word, marks the arena; flushed at the runtime's `begin_frame` | none |
440
+ | `GPUMeshSkinningContext` | `source_meshlet_metadata_offset`, `clone_meshlet_metadata_offset` per binding; `prev_position_offsets` keyed by absolute clone row | does not listen — today a `compact()` with a skinned mesh resident silently deforms the wrong rows | listen on both records per binding (unsubscribe closures kept on the binding and run by `#drop_binding_at`, as the VG pool does): on a metadata offset change, `#bindings_dirty = true`. `update` then re-derives every binding's offsets from its records and **rebuilds the whole prev-position offsets table from the bindings** (capacity ensured to the highest new row) before the wholesale upload it already does. Rebuilding rather than re-keying per listener, because a rebuild's `changed` order is insertion order and one binding's new rows can be another's old rows. The prev-position *history* lives in a side buffer keyed by word offset and is untouched, so no frame of motion vectors is lost. |
441
+ | BLAS (`GPUGeometryMeshletsBVH`, `bvh_to_gpu_buffer`, `GPUBLASRefitBatch`) | batch-local meshlet indices; the refit reads `geometries[i].meshlets_address` on the GPU | — | none beyond the geometry row being right |
442
+ | anything holding `buffer_data`/`buffer_metadata` | the buffer objects | replaced by `grow`/`compact` already; re-read per frame | none — a sweep never replaces a buffer |
443
+
444
+ ## 6. Proof
445
+
446
+ Every spec is written red first against the code before its ledger row, in
447
+ `GPUMeshletManager.spec.js` / `OffsetAllocator.spec.js` / the consumers' specs, no spies or mocks,
448
+ asserting the public API, `stats`, or what reaches `SoftwareGPUDevice` (copies and `writeBuffer`
449
+ execute there; compute dispatches are recorded and their bound buffers are readable before
450
+ `finish`).
451
+
452
+ **`OffsetAllocator`** (all red today — the methods do not exist):
453
+ - `slide_down` over a randomized allocate / free / slide / grow sequence against a reference model
454
+ checking every §4.2 invariant, bin bitmasks and freelist count included.
455
+ - `slide_down` merges the free node with a free successor and returns a node to the freelist.
456
+ - the tail case as §4.2 states it; sliding over the head leaves the used node as head.
457
+ - `first_used_node`/`next_used_node` walk exactly the used nodes in address order, across frees.
458
+ - `can_allocate` agrees with `allocate` on both sides of a bin boundary **and** on a node-exhausted
459
+ allocator with room.
460
+
461
+ **`GPUMeshletManager`**:
462
+ - *quantization*: a batch of size `S` (not itself a bin) freed, and one of `S + 1` in the same bin
463
+ arriving with the tail shorter than the bin gap, lands in the hole: `buffer_data` is the same
464
+ object (a rebuild replaces it at the same size) and the record's offset is the hole's. Red: the
465
+ raw request is refused and rebuilt for.
466
+ - *the row patch*: `add_batch` on the device holds the same rows the serde produced (`gpu_addresses`
467
+ agrees with the old code's output pinned as bytes); a layout spec fails if `size`,
468
+ `aligned_size` or `address`'s offset in `MESHLET_METADATA_STRUCT` change.
469
+ - *bounded sweep*: with `N > MOVE_BUDGET` records above one hole, after the refusal that arms it,
470
+ one `sweep` fires `changed` for exactly `min(N, MOVE_BUDGET)` records and copies at most
471
+ `WORD_BUDGET` words; the next continues; after `ceil(N / MOVE_BUDGET)` calls every record is
472
+ packed, the free space is one tail run (an `add_batch` the size of the whole freed space lands
473
+ with `buffer_data` the same object), and `stats.rebuilds` is 0. Red: the rebuild moves all of
474
+ them at once and replaces the buffer.
475
+ - *a move carries the bytes and re-addresses the rows*: after a sweep, `gpu_data_words` agrees with
476
+ the record's new offset, the metadata rows at the new metadata offset are the old rows (copied,
477
+ not re-derived: a row whose `bounds_box` was overwritten on the device by a `writeBuffer` in the
478
+ spec keeps that box), and the patch dispatch's table names exactly the moved rows with the right
479
+ deltas (read off the bound buffer before `finish`; the software device does not run the shader).
480
+ - *overlapping move*: a record sliding down by less than its own size arrives intact.
481
+ - *records not moved keep their offsets and get no `changed`* (inside the bounded-sweep spec).
482
+ - *refusal policy*: the existing "a request the arena has room for in pieces is compacted for, not
483
+ grown for" is **rewritten** to the new contract — it is grown for, once, and after the sweep the
484
+ next such request lands without a second growth and without a rebuild (`stats`); a second
485
+ fragmentation refusal mid-sweep is answered by exactly one rebuild; node exhaustion with room
486
+ ends in a rebuild rather than a throw.
487
+ - *cursor under removal*: removing the record the cursor names, and removing records above and
488
+ below it mid-sweep, ends with every record packed and one free run (the §4.3 postcondition).
489
+ - *the record-and-allocate rule*: an `add_batch` between a `sweep` that recorded moves and the
490
+ context's `finish` throws.
491
+ - *clones*: the existing "a queued clone survives the compaction" case is kept (explicit
492
+ `compact()`); a new case queues a clone, sweeps the source down in the same context via `sweep`
493
+ then `update`, and reads the flush's regions table off the dispatch's bound buffer (captured
494
+ before `finish`) to check it names the post-sweep offsets.
495
+ - *the three named cases stay green*: clones surviving compaction, growth leaving records in
496
+ place, O(1) `remove_batch`.
497
+
498
+ **Bench spec** `GPUMeshletManager.bench.spec.js` (the repository's `*.bench.spec.js` form):
499
+ - per-`sweep` cost at 1,000 and at 32,000 resident records with identical work per call
500
+ (`MOVE_BUDGET` moves of 4-word records, so the software device's copies do not dominate),
501
+ paired, alternated, minimum of rounds; the difference — the O(N) signature a walk of 32,000
502
+ records would leave, ~30–60 µs — asserted under 10 µs.
503
+ - the §2 churn at 4,096 (quarter scale) asserting `stats.rebuilds === 0` and `changed` per `sweep`
504
+ ≤ `MOVE_BUDGET`; the §2.4 mixed load asserting the same, and recording growths.
505
+
506
+ **Consumers**: `GPUGeometryManager.spec.js` — after a `compact()` moves a registered geometry
507
+ and a clone, the geometry table rows on the device carry the new `meshlets_address` (red: the
508
+ rows are stale). `GPUMeshSkinningContext.spec.js` — after a `compact()` moves a clone (and a
509
+ source), the uploaded bindings carry the new offsets and the prev-position table is keyed by the
510
+ new rows, driven through the spec's own `frame()` (red today); and a dropped binding's records
511
+ moving afterwards write nothing.
512
+
513
+ **The Zorah tour** (`tour.mjs`, RUNTIME_PLAN_2026_09_14 §7's recipe): eight rosettes, five seconds
514
+ each, twice round, at 4,096 slots, headless Chrome. Recorded per sample: `stats`,
515
+ `gpu_memory_usage` / `_reserved` / `_occupied`, long tasks, frame p50/p95. Pass: zero main-thread
516
+ tasks over 50 ms attributable to the manager (the vite worker-module stalls in the first seconds
517
+ are the runtime plan's known other source; a long task in a later sample is the manager's until
518
+ shown otherwise), `stats.rebuilds === 0`, page turnover of the same order (~3,000), `refused` not
519
+ worse, p50/p95 not worse than 8.3/12.6 ms. And, because under this load the sweep is expected to
520
+ have nothing to do, a second browser scenario in the same page: the §2.4 swap load driven through
521
+ `scene_context.geometries.meshlets` from the driver, asserting `stats.moves > 0`,
522
+ `stats.rebuilds === 0` and the same long-task bar.
523
+
524
+ ## 7. What this does not do
525
+
526
+ - **No synchronous sweep before growing.** §3 says why; the cost is one 25 % growth per episode.
527
+ - **No shrinking.** An arena grown under fragmentation keeps its size after the sweep reclaims the
528
+ space; the reclaimed run at the tail is what later growth would have asked for. Shrinking is a
529
+ buffer swap, i.e. a `compact()`, and a caller who wants it has that.
530
+ - **No sweep below an eighth of scatter.** A pool whose holes are recycled as fast as they are
531
+ made — near-uniform churn under quantization — stays under the ratio and is left alone; the
532
+ sweep is for a pool that thins. The ratio is a constant, not a knob.
533
+ - **`compact()` still re-derives rows from CPU bytes**, so it resets any `bounds_box` the skinning
534
+ chain wrote — as it always has. A sweep does not; that is why rows are copied (§4.3.1).
535
+ - **No packing-order guarantee.** The rebuild packed in insertion order; the sweep packs in
536
+ address order.
537
+ - **No change to `ALLOCATION_LIMIT`** or to the node budget's meaning. A slide consumes no node.
538
+ - **No change to what a `MeshletBatch` is** or to the metadata row layout.
539
+
540
+ ## 8. Ledger
541
+
542
+ All on the software device at a quarter of the Zorah sizes unless said otherwise; every spec was
543
+ red against the code before its row.
544
+
545
+ | # | item | spec (red first) | measured / commit |
546
+ |---|---|---|---|
547
+ | C2 | manager: quantized allocation, byte-level row patch, empty batches reserve one element, `gpu_memory_usage_reserved`, `stats`, `allocation_limit` option | `GPUMeshletManager.spec.js` | the §2 churn at 4,096: 356 rebuilds → 0; `add_batch` 39 → 19 µs; a rebuild of 4,102 records 28 → 6.5–7.0 µs a record — `3de80c278`, `89ae26912` (the empty-batch fix rode with C4: the assert refused eleven scene-row and cut specs) |
548
+ | C4 | manager: refusal policy (grow once per episode → rebuild → throw), counter removed, the "compacted for" spec rewritten | `GPUMeshletManager.spec.js` | three cases: grown for once and nothing moves; a second refusal mid-episode is one rebuild; out of nodes with room is a rebuild, not a throw — `89ae26912` |
549
+ | C7a | bench spec: the §2 churn against C2 + C4 and the §2.4 phase load | `GPUMeshletManager.bench.spec.js` | churn: 0 rebuilds, ≤ 6 growths; phase load without a sweep: 2 episodes, 4 growths, 0 rebuilds, 68.8 MB over 42.1 MB reserved — `10d3eaa85` |
550
+ | C5 | `GPUGeometryManager`: row patch in the handler, listener on clones, removal | `GPUGeometryManager.spec.js` | two cases — `36116320c` |
551
+ | C6 | `GPUMeshSkinningContext`: listeners, table rebuilt from bindings, unsubscribe | `GPUMeshSkinningContext.spec.js` | one case (the binding named the clone's old row) — `f6119fb4f` |
552
+ | C1 | `OffsetAllocator`: `slide_down`, `first_used_node`, `next_used_node`, `can_allocate`, `#head_node` | `OffsetAllocator.spec.js` | six cases and a 4,000-step randomized sequence — `42b2af34a` |
553
+ | C3 | manager: cursors, one release path, `sweep(cmd_ctx)`, the record-then-allocate assert, `GraphicsContext.update` calls it | `GPUMeshletManager.spec.js` | eight cases; the §6 list as written — `892e27197` |
554
+ | C7b | bench spec: the phase load with the sweep, the paired per-`sweep` cost | `GPUMeshletManager.bench.spec.js` | one sweep of 64 moves: 481 µs at 1,000 resident, 402 at 32,000 (noise-dominated, then re-measured under C3b: 135 vs 138 µs); the phase load with a refusal-armed sweep grew 4 times, as without — the finding that made C3b — `044b8f779` |
555
+ | C3b | manager: the sweep arms itself on an eighth of scatter; the restart pointer replaces the step-back | `GPUMeshletManager.spec.js` | one case (three holes arm, two do not); phase load with the sweep: 24 episodes armed and finished, 126,990 moves at ≤ 64 a call, 3 growths and 67.5 MB where the pool that could only grow had 4 and 68.8 — `fe97c55fe` |
556
+ | C8 | Zorah tour and the phase scenario on the live pool, numbers into §10 | `H:/tmp/tour_out_c8/rows.json` | see §10 |
557
+
558
+ ## 9. Review
559
+
560
+ Three disjoint reviewers on revision 1, 2026-09-16, before anything was built. What they found
561
+ and what it changed:
562
+
563
+ **Allocator primitive** (8 findings). `can_allocate` as a bare bin probe ignored node exhaustion
564
+ (`allocate` refuses on an empty freelist first) — fixed in §4.2 and §4.4 step 5. A removal *below*
565
+ the cursor left a second free run the sweep never revisited, breaking the bubble claim — fixed by
566
+ the cursor step-back rule in §4.3. `#head_node` tagged by offset mislabels after `allocate(0)` —
567
+ derived from the chain link instead, and the manager asserts non-zero sizes. The merge inside
568
+ `slide_down` has to remove both nodes before either changes size, and insert last — stated. The
569
+ tail spec as first written could pass with the tail stale — strengthened. The quantization identity
570
+ holds per uncoalesced hole only — reworded, with the coalesced counterexample. A stale cursor
571
+ names another live record and never fails loudly — one `#release` path, an assert in the step, and
572
+ the cursor re-read after `changed`. `grow` keeps the tail node's index (a nit; the claim was that
573
+ the cursor survives it, which it does).
574
+
575
+ **GPU ordering and consumers** (12 findings). Two blockers: the skinning context calls
576
+ `meshlets.update` directly and nothing runs `geometries.update` after it, so a deferred row patch
577
+ left the geometry table stale for that frame — the row is now patched inside the handler; and
578
+ `update` runs three times a frame, so "at most `MOVE_BUDGET` per frame" was false by 3× — the
579
+ sweep is its own method with one call site. Re-deriving rows from CPU bytes on a move would
580
+ overwrite GPU-written skinning bounds — rows are copied. The skinning listener as first described
581
+ corrupted another binding's rows under a rebuild's insertion-order `changed`, and listeners were
582
+ never removed — the table is rebuilt from bindings, and bindings unsubscribe. The plan had not
583
+ stated that nothing may allocate between a recorded move and the context's `finish` — stated and
584
+ asserted. The staging buffer is sized to the largest move and reused; the sync sweep could not
585
+ have had a transient buffer at all, which with the address-patch dispatch is why it is gone. The
586
+ consumer specs could not arm a sweep through `GPUGeometryManager`'s default-sized manager — they
587
+ drive `compact()`, which exercises the same `changed` contract. The regions-table check must
588
+ capture the bound buffer before `finish`. A file name was wrong.
589
+
590
+ **Policy, measurement, proof** (11 findings). The 4,096 row was a fill landing 0.2 % under a
591
+ ladder rung, and the Zorah tour's ≈ 750 events per compaction sits with the 10–23 % rows — the
592
+ headroom column and the reconciliation are in §2.2. Under the plan's own numbers the motivating
593
+ load never reaches the sweep — §0 now lands quantization and the policy first, measures, and
594
+ builds the sweep against the §2.4 load it is for; §3 has the no-sweep row. §2.3's quantized
595
+ column was sized from raw words — re-run over reserved; "~6 %" became "~3 % over reserved". The
596
+ policy grew on every unsatisfied refusal with nothing bounding a second growth mid-sweep — one
597
+ growth per episode, then the (now cheap) rebuild. Budgets were projections presented as
598
+ measurements — marked provisional and re-picked from C3's numbers. `STEP_BUDGET` capped a walk
599
+ the manager can avoid — the cursor is maintained on every removal. "Zero rebuilds" was
600
+ unobservable without a spy — `stats`. Several specs were green against today's code as written —
601
+ each now states the red condition. The tour's long-task bar is confounded by the vite worker
602
+ stalls — attributed. The moving-tail alternative was missing from §3 — added and rejected.
603
+ Prev-position history is not lost on a move once rows are copied rather than re-keyed (resolved
604
+ by the ordering reviewer's fix). Numbers in §2.1 that did not match the bench are corrected.
605
+
606
+ ## 10. As built
607
+
608
+ Nine commits on master, `e002fd5d2` (this document) to `fe97c55fe`, 2026-09-16; the specs in §6
609
+ as written, with the changes below; 5,350 specs green across `src/shade/renderer`, the playgrounds
610
+ and `src/core/binary`; the declaration gate at its budget of 19 throughout.
611
+
612
+ ### 10.1 The Zorah tour
613
+
614
+ `tour2.mjs` (the runtime plan's driver with the `compact()` wrapper replaced by `stats`), headless
615
+ Chrome, the vite server of this checkout, the range server on the Zorah directory, the same
616
+ container at 4,096 slots, eight rosettes held five seconds each, twice round, then the rest pose;
617
+ then the §2.4 phase load driven through the pool's API from the page while the tier kept
618
+ churning underneath (1,024 synthetic pages, six phases of grow-by-half / thin / import, a moment
619
+ between the thinning and the import). `H:/tmp/tour_out_c8/rows.json`.
620
+
621
+ | | before (RUNTIME_PLAN_2026_09_14 §7) | after |
622
+ |---|---:|---:|
623
+ | main-thread tasks over 50 ms, whole tour | 4 in 150 s, 79–115 ms, every one a rebuild | **0** |
624
+ | rebuilds | 4 | **0** |
625
+ | page turnover (evictions) | 3,009 | 3,050 |
626
+ | frame time zoomed in, p50 / p95 | 8.3 / 12.6 ms | 4.2 / 4.3–8.4 ms |
627
+ | sweeps | — | 1 armed (by scatter, round 1 stop 3), 1 finished: 3,411 moves inside one five-second sample, no long task |
628
+ | growths over the tour | — | 10, to 244.7 MB over 236.5 MB reserved (224.1 MB occupied) |
629
+ | `refused` (the residency's counter) | 999 | 1,725 — see below |
630
+ | phase load on the live pool | — | 6 episodes armed, 6 finished, 16,058 moves, 3 growths (capacity: 90 MB of synthetic pages), 0 rebuilds, 0 long tasks, p95 8.3 ms |
631
+ | page errors | 0 | 0 |
632
+
633
+ The residency's `refused` — installs the VRAM tier turned down — is higher than the runtime plan's
634
+ tour recorded. The pool's own counters rule out the pool: no allocation failed (growth answered
635
+ every shortfall), no rebuild ran, no move happened during the samples where the count rose. The
636
+ frames ran at half the period of the earlier tour, so the tier took about twice the readings over
637
+ the same five seconds per stop, and a full tier refuses per reading; that is the likely reading,
638
+ and it is not established here. It is open, and it is the residency's number, not this manager's.
639
+
640
+ ### 10.2 What the build changed against revision 2
641
+
642
+ - **The cursor does not step back; a restart pointer does** (§4.3). The step-back rule the
643
+ allocator review asked for is correct for one removal and wrong for a stream of them: the
644
+ phase load's thinning sent the cursor back under the records it had just packed on every
645
+ removal, and no pass ever reached the end. A pass now finishes, and the used node after the
646
+ lowest hole opened below it starts the next, which closes every hole the first left behind in
647
+ one walk. The randomized allocator spec and the "cursor under removals" spec pin the same
648
+ postcondition either way.
649
+ - **The sweep arms itself on scatter** (§4.4, C3b). A sweep armed only by a refusal grew exactly
650
+ as often as no sweep at all, because it reclaimed the space after the growth it could have
651
+ prevented and the next fill carved the tail before the next arrival. An eighth of an arena free
652
+ in pieces arms it; under the Zorah tour that happened once.
653
+ - **An empty batch reserves one element rather than being refused.** The non-zero assert the
654
+ allocator review asked for refused geometries a scene registers with no triangles yet; eleven
655
+ scene-row and cut specs said so.
656
+ - **The budgets stayed at their provisional values.** A move measured at ~2 µs on the software
657
+ device (135 µs a call of 64, flat in the resident count); 64 moves and 4 MB a call is a small
658
+ fraction of a frame on a real device and finished a 3,411-record sweep unnoticed on Zorah.
659
+ - **The bench's phase load gained a settle period** between the thinning and the import, because
660
+ an import in the very frame after a thousand deletions is not the case the sweep is for and no
661
+ bounded sweep can have consolidated by then; a large arrival mid-sweep still costs the episode's
662
+ one growth, as §4.4 says.
663
+ - **Nothing in §5 changed**, and the VG pool needed nothing.