@woosh/meep-engine 3.24.0 → 3.25.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 (322) hide show
  1. package/package.json +1 -1
  2. package/src/engine/graphics3/PickingSystem.d.ts +3 -3
  3. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  4. package/src/engine/graphics3/PickingSystem.js +52 -55
  5. package/src/engine/graphics3/shader_picking_gather.d.ts +40 -0
  6. package/src/engine/graphics3/shader_picking_gather.d.ts.map +1 -0
  7. package/src/engine/graphics3/shader_picking_gather.js +120 -0
  8. package/src/engine/physics/cloth/MEASUREMENTS.md +1050 -0
  9. package/src/engine/physics/cloth/PLAN.md +411 -49
  10. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts +52 -0
  11. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts.map +1 -0
  12. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.js +70 -0
  13. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts +12 -3
  14. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  15. package/src/engine/physics/cloth/ecs/ClothDynamics.js +340 -331
  16. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +36 -22
  17. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  18. package/src/engine/physics/cloth/ecs/ClothInstance.js +38 -23
  19. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts +84 -0
  20. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts.map +1 -0
  21. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.js +141 -0
  22. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +51 -82
  23. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  24. package/src/engine/physics/cloth/ecs/ClothSystem.js +81 -869
  25. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts +48 -0
  26. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts.map +1 -0
  27. package/src/engine/physics/cloth/ecs/ClothWorkerCore.js +107 -0
  28. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +286 -0
  29. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -0
  30. package/src/engine/physics/cloth/ecs/ClothWorld.js +1104 -0
  31. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +182 -0
  32. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -0
  33. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +562 -0
  34. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts +2 -0
  35. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts.map +1 -0
  36. package/src/engine/physics/cloth/ecs/cloth.worker.js +12 -0
  37. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  38. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +2 -1
  39. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +58 -0
  40. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  41. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +341 -271
  42. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +20 -2
  43. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  44. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +39 -15
  45. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts +29 -0
  46. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts.map +1 -0
  47. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.js +78 -0
  48. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts +17 -0
  49. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts.map +1 -0
  50. package/src/engine/physics/cloth/ecs/makeClothWorker.js +18 -0
  51. package/src/engine/physics/cloth/playground/README.md +73 -2
  52. package/src/engine/physics/cloth/playground/wind.html +166 -0
  53. package/src/engine/physics/cloth/playground/wind_build.d.ts +81 -0
  54. package/src/engine/physics/cloth/playground/wind_build.d.ts.map +1 -0
  55. package/src/engine/physics/cloth/playground/wind_build.js +141 -0
  56. package/src/engine/physics/cloth/playground/wind_main.d.ts +2 -0
  57. package/src/engine/physics/cloth/playground/wind_main.d.ts.map +1 -0
  58. package/src/engine/physics/cloth/playground/wind_main.js +681 -0
  59. package/src/engine/physics/cloth/solver/ClothState.d.ts +134 -79
  60. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  61. package/src/engine/physics/cloth/solver/ClothState.js +273 -12
  62. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +33 -0
  63. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  64. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +34 -0
  65. package/src/engine/physics/cloth/solver/cloth_step.d.ts +21 -2
  66. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  67. package/src/engine/physics/cloth/solver/cloth_step.js +173 -4
  68. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +40 -0
  69. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -0
  70. package/src/engine/physics/cloth/solver/cloth_step_instance.js +284 -0
  71. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts +107 -0
  72. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts.map +1 -0
  73. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.js +263 -0
  74. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +88 -0
  75. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -0
  76. package/src/engine/physics/cloth/wind/AbstractClothWind.js +100 -0
  77. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +85 -0
  78. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -0
  79. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +182 -0
  80. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +79 -0
  81. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -0
  82. package/src/engine/physics/cloth/wind/ClothFluidWind.js +210 -0
  83. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +101 -0
  84. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -0
  85. package/src/engine/physics/cloth/wind/cloth_sample_air.js +290 -0
  86. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  87. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  88. package/src/shade/device/mock/texture_copy.js +28 -0
  89. package/src/shade/playground/vgeo_runtime/README.md +94 -0
  90. package/src/shade/playground/vgeo_runtime/index.html +62 -0
  91. package/src/shade/playground/vgeo_runtime/main.d.ts +2 -0
  92. package/src/shade/playground/vgeo_runtime/main.d.ts.map +1 -0
  93. package/src/shade/playground/vgeo_runtime/main.js +399 -0
  94. package/src/shade/playground/vgeo_viewer/sample_asset.js +1 -1
  95. package/src/shade/renderer/camera/Camera.d.ts +14 -0
  96. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  97. package/src/shade/renderer/camera/Camera.js +7 -1
  98. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.d.ts.map +1 -1
  99. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.js +16 -1
  100. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  101. package/src/shade/renderer/geometry/GPUGeometryManager.js +29 -9
  102. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +10 -0
  103. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  104. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +13 -0
  105. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +46 -18
  106. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +70 -22
  107. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +97 -19
  108. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +12 -2
  109. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  110. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +14 -3
  111. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +2 -2
  112. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  113. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +124 -6
  114. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +22 -0
  115. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +1 -0
  116. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.js +53 -0
  117. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +8 -1
  118. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  119. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +8 -1
  120. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts +16 -0
  121. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts.map +1 -0
  122. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.js +15 -0
  123. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts +54 -0
  124. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts.map +1 -0
  125. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.js +53 -0
  126. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +13 -2
  127. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  128. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +14 -2
  129. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +100 -0
  130. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +113 -0
  132. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  133. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +5 -0
  134. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  135. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +108 -0
  136. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  137. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +23 -0
  138. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  139. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +40 -0
  140. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +228 -0
  141. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -0
  142. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +932 -0
  143. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +176 -0
  144. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -0
  145. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +425 -0
  146. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +241 -0
  147. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +574 -0
  148. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +120 -0
  149. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -0
  150. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +674 -0
  151. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +287 -0
  152. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -0
  153. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +976 -0
  154. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +42 -0
  155. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -0
  156. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +96 -0
  157. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts +16 -0
  158. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts.map +1 -0
  159. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.js +15 -0
  160. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts +19 -0
  161. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -0
  162. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +83 -0
  163. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts +9 -0
  164. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts.map +1 -0
  165. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.js +8 -0
  166. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +19 -0
  167. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -0
  168. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +160 -0
  169. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts +3 -0
  170. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -0
  171. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +255 -0
  172. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts +3 -0
  173. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -0
  174. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +80 -0
  175. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts +3 -0
  176. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -0
  177. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +144 -0
  178. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts +3 -0
  179. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -0
  180. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +530 -0
  181. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +39 -0
  182. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -0
  183. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +54 -0
  184. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +41 -0
  185. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -0
  186. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +56 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts +49 -0
  188. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -0
  189. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +328 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +10 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts.map +1 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +51 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +13 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -0
  195. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +12 -0
  196. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +17 -0
  197. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts.map +1 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +99 -0
  199. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts +9 -0
  200. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts.map +1 -0
  201. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +8 -0
  202. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +7 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts.map +1 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +27 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts +14 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts.map +1 -0
  207. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.js +13 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts +7 -0
  209. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts.map +1 -0
  210. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.js +35 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts +9 -0
  212. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts.map +1 -0
  213. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.js +8 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts +15 -0
  215. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts.map +1 -0
  216. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.js +14 -0
  217. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +7 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts.map +1 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +36 -0
  220. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +10 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -0
  222. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +9 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +11 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts.map +1 -0
  225. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +54 -0
  226. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts +9 -0
  227. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts.map +1 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.js +8 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts +11 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.js +10 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts +7 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts.map +1 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.js +30 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts +9 -0
  236. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts.map +1 -0
  237. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.js +8 -0
  238. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts +13 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts.map +1 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.js +12 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts +14 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts.map +1 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.js +13 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +11 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +61 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts +9 -0
  248. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +8 -0
  250. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts +13 -0
  251. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts.map +1 -0
  252. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.js +12 -0
  253. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts +9 -0
  254. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.js +8 -0
  256. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts +20 -0
  257. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts.map +1 -0
  258. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.js +19 -0
  259. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts +23 -0
  260. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +70 -0
  262. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +18 -0
  263. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -0
  264. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +25 -0
  265. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +35 -0
  266. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -0
  267. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +41 -0
  268. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +32 -0
  269. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -0
  270. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +138 -0
  271. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  272. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  273. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  274. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +9 -0
  275. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  276. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +12 -1
  277. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  278. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +12 -1
  279. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  280. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +10 -0
  281. package/src/shade/renderer/scene/GPUSceneContext.d.ts +18 -0
  282. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  283. package/src/shade/renderer/scene/GPUSceneContext.js +647 -594
  284. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  285. package/src/shade/renderer/scene/rows/GPUSceneRows.js +22 -0
  286. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts +35 -0
  287. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts.map +1 -0
  288. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.js +91 -0
  289. package/src/engine/graphics3/instantiate_scene_bundle.d.ts +0 -54
  290. package/src/engine/graphics3/instantiate_scene_bundle.d.ts.map +0 -1
  291. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +0 -39
  292. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +0 -1
  293. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts +0 -35
  294. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts.map +0 -1
  295. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts +0 -96
  296. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts.map +0 -1
  297. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts +0 -20
  298. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts.map +0 -1
  299. package/src/shade/renderer/animation/skin_bind_transform.d.ts +0 -25
  300. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +0 -1
  301. package/src/shade/renderer/animation/skin_frame_defect.d.ts +0 -34
  302. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +0 -1
  303. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +0 -24
  304. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +0 -1
  305. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +0 -70
  306. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +0 -1
  307. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts +0 -96
  308. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts.map +0 -1
  309. package/src/shade/renderer/scene/InstanceBatch.d.ts +0 -92
  310. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +0 -1
  311. package/src/shade/renderer/scene/Mesh.d.ts +0 -55
  312. package/src/shade/renderer/scene/Mesh.d.ts.map +0 -1
  313. package/src/shade/renderer/scene/Node3D.d.ts +0 -159
  314. package/src/shade/renderer/scene/Node3D.d.ts.map +0 -1
  315. package/src/shade/renderer/scene/Node3DDirtyList.d.ts +0 -59
  316. package/src/shade/renderer/scene/Node3DDirtyList.d.ts.map +0 -1
  317. package/src/shade/renderer/scene/SceneNodeBinding.d.ts +0 -52
  318. package/src/shade/renderer/scene/SceneNodeBinding.d.ts.map +0 -1
  319. package/src/shade/renderer/scene/SkinnedMesh.d.ts +0 -27
  320. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +0 -1
  321. package/src/shade/renderer/scene/TransformAuthority.d.ts +0 -19
  322. package/src/shade/renderer/scene/TransformAuthority.d.ts.map +0 -1
@@ -0,0 +1,976 @@
1
+ import { assert } from "../../../../../core/assert.js";
2
+ import { ALLOCATOR_NO_SPACE, OffsetAllocator } from "../../../../../core/binary/allocator/OffsetAllocator.js";
3
+ import { VGEO_DEFAULT_PAGE_SIZE } from "../format/page/VGEO_DEFAULT_PAGE_SIZE.js";
4
+ import { VGEO_CLUSTER_ROW_OFFSET } from "./layout/VGEO_CLUSTER_ROW_OFFSET.js";
5
+ import { VGEO_CLUSTER_ROW_WORDS } from "./layout/VGEO_CLUSTER_ROW_WORDS.js";
6
+ import { VGEO_GEOMETRY_ROW_OFFSET } from "./layout/VGEO_GEOMETRY_ROW_OFFSET.js";
7
+ import { VGEO_GEOMETRY_ROW_WORDS } from "./layout/VGEO_GEOMETRY_ROW_WORDS.js";
8
+ import { VGEO_GROUP_FLAG_SELECTABLE } from "./layout/VGEO_GROUP_FLAG_SELECTABLE.js";
9
+ import { VGEO_GROUP_ROW_OFFSET } from "./layout/VGEO_GROUP_ROW_OFFSET.js";
10
+ import { VGEO_GROUP_ROW_WORDS } from "./layout/VGEO_GROUP_ROW_WORDS.js";
11
+ import { VGEO_NODE_ROW_WORDS } from "./layout/VGEO_NODE_ROW_WORDS.js";
12
+ import { VGEO_NO_CHILD_PAGE } from "./layout/VGEO_NO_CHILD_PAGE.js";
13
+ import { VGEO_PAGE_ROW_NONE } from "./layout/VGEO_PAGE_ROW_NONE.js";
14
+ import { VGEO_PAGE_ROW_OFFSET } from "./layout/VGEO_PAGE_ROW_OFFSET.js";
15
+ import { VGEO_PAGE_ROW_WORDS } from "./layout/VGEO_PAGE_ROW_WORDS.js";
16
+ import { vgeo_runtime_page_block_words } from "./vgeo_runtime_page_block_words.js";
17
+ import { vgeo_runtime_page_slot_words } from "./vgeo_runtime_page_slot_words.js";
18
+ import { vgeo_runtime_write_page_block } from "./vgeo_runtime_write_page_block.js";
19
+
20
+ /**
21
+ * What a VRAM-resident page **is**, with no device anywhere near it.
22
+ *
23
+ * Two word arrays: the arena of fixed-stride page slots, and the per-geometry page lookup.
24
+ * {@link GPUVirtualGeometryPool} owns the buffers, the budget and the meshlet-pool allocation; this
25
+ * owns the layout, and the split is the same one `format/read/` and `residency/` already make —
26
+ * mechanics below, policy and devices above.
27
+ *
28
+ * It is worth having as its own object for a reason beyond tidiness: the cut kernel's agreement
29
+ * against `vgeo_select_cut` is the single most important test in this campaign, and it can run in
30
+ * node against these arrays. A mirror written for the spec would prove the mirror.
31
+ *
32
+ * ## Slots, not allocations
33
+ *
34
+ * A page's rows live at `page_row × slot_words`, and everything that used to make that address a
35
+ * question is gone with the question: no `OffsetAllocator`, no growth, no ceiling, no shadow that
36
+ * can grow before the buffer it mirrors. `VIRTUAL_GEOMETRY_DESIGN.md` §6 asked for this from the
37
+ * start and format §11.14 is what finally permits it — `nominal_page_size` is a bound every page is
38
+ * within, `GPUVirtualGeometryPool.register` refuses a container that does not promise it, and
39
+ * {@link vgeo_runtime_page_slot_words} turns that promise into a stride. A page whose rows do not
40
+ * fit the stride is refused; see that function for why the stride is a choice and what it was
41
+ * measured against.
42
+ *
43
+ * The page's own header is the first {@link VGEO_PAGE_ROW_WORDS} words of its slot rather than a
44
+ * row of a second table, which saves the traversal a storage binding it does not have to spare
45
+ * (RUNTIME_PLAN §4.9) and makes an install one contiguous range.
46
+ *
47
+ * ## This tier answers its own activation question
48
+ *
49
+ * {@link refresh_selectable} computes selectability from the arena and the lookup — this tier's
50
+ * residency — and consults no reader. It used to publish `VGeoContainerReader.is_selectable`, which
51
+ * is a fact about the **RAM** tier, into the flag the kernel reads while the descent was gated on
52
+ * the **VRAM** lookup. VRAM is a strict subset, so a group could be flagged selectable while a page
53
+ * holding one of its parents had no row here, and the parent's own ancestor then drew a coarse
54
+ * stand-in over the surface the group was drawing. That is REVIEW_LEDGER C1, and what closes it is
55
+ * the group row carrying its parent count (VGEO_GROUP_ROW_OFFSET.PARENT_COUNT) so that the rule
56
+ * can be evaluated where the residency it is about lives.
57
+ *
58
+ * **Nothing here decides anything.** What to install, what to evict — all of that is the pool's.
59
+ * What this refuses is only what it cannot represent.
60
+ *
61
+ * @author Alex Goldring
62
+ * @copyright Company Named Limited (c) 2026
63
+ */
64
+ export class VGeoRuntimeTables {
65
+ /**
66
+ * Fixed-stride page slots: `[page header][node rows][group rows][cluster rows]`, one per row.
67
+ * @type {Uint32Array}
68
+ */
69
+ #arena;
70
+
71
+ /**
72
+ * The same memory as {@link #arena}, for the halves of a row that are distances rather than
73
+ * indices.
74
+ * @type {Float32Array}
75
+ */
76
+ #arena_floats;
77
+
78
+ /**
79
+ * Container page index to page row, or {@link VGEO_PAGE_ROW_NONE}, one block per geometry.
80
+ *
81
+ * Its own buffer and its own allocator, so a block's offset is a small dense number. That is
82
+ * what lets the cut index its per-want dedup stamps by `lookup offset + child page index`
83
+ * without a second per-geometry field or a stamp array the size of the arena.
84
+ *
85
+ * The allocator here is not the one the review took out: it hands out one block per *geometry*,
86
+ * at registration, and a scene registers a handful of assets where it installs and evicts
87
+ * thousands of pages.
88
+ *
89
+ * @type {Uint32Array}
90
+ */
91
+ #lookup;
92
+
93
+ /**
94
+ * `geometry row * VGEO_GEOMETRY_ROW_WORDS + field`. Read by the instance-collect pass and
95
+ * folded into instance records, never bound by the traversal.
96
+ * @type {Uint32Array}
97
+ */
98
+ #geometries;
99
+
100
+ /**
101
+ * @type {OffsetAllocator}
102
+ */
103
+ #lookup_allocator;
104
+
105
+ /**
106
+ * @type {number}
107
+ */
108
+ #slot_words;
109
+
110
+ /**
111
+ * @type {number}
112
+ */
113
+ #page_capacity;
114
+
115
+ /**
116
+ * Page rows in use, so an eviction can tell a resident row from a free one without a second
117
+ * structure and {@link resident_page_count} is a size rather than a scan.
118
+ * @type {Set<number>}
119
+ */
120
+ #resident_rows = new Set();
121
+
122
+ /**
123
+ * Resident page rows per geometry row — what {@link refresh_selectable} runs over, and what an
124
+ * eviction has to look at to find a page's VRAM children.
125
+ * @type {Map<number, Set<number>>}
126
+ */
127
+ #geometry_rows = new Map();
128
+
129
+ /**
130
+ * Lookup allocation per geometry row, so an unregistered geometry gives its lookup back.
131
+ * @type {Map<number, Allocation>}
132
+ */
133
+ #geometry_allocations = new Map();
134
+
135
+ /**
136
+ * Page rows that are free, newest first. A row is a slot and carries no identity, so re-using
137
+ * the most recently freed one keeps the live rows dense and the table's high-water mark — which
138
+ * is what sizes the touch-stamp region every frame — from creeping.
139
+ * @type {number[]}
140
+ */
141
+ #free_page_rows = [];
142
+
143
+ /**
144
+ * Per page row, how many times it has been freed.
145
+ *
146
+ * A row is reused the moment it is freed, and feedback is at least a frame behind the cut that
147
+ * produced it, so a want or a touch naming row 7 may be naming the page that was in row 7 when
148
+ * the cut ran rather than the one there now. The host snapshots a row's generation while it
149
+ * builds the graph and compares when the readback lands; a mismatch is a record about a page
150
+ * that no longer exists, and dropping it is the whole of the fix (REVIEW_LEDGER M10).
151
+ *
152
+ * CPU-side deliberately. Nothing on the device ever needs this — the kernel reads rows that are
153
+ * resident *now*, which is by construction the state it was dispatched against.
154
+ *
155
+ * @type {Uint32Array}
156
+ */
157
+ #generations;
158
+
159
+ /**
160
+ * @type {number}
161
+ */
162
+ #page_row_high_water = 0;
163
+
164
+ /**
165
+ * @type {number}
166
+ */
167
+ #geometry_row_high_water = 0;
168
+
169
+ /**
170
+ * Geometry rows an unregister gave back. A scene that swaps assets would otherwise walk the
171
+ * geometry table's high-water mark up until registration refused, with the table mostly empty.
172
+ * @type {number[]}
173
+ */
174
+ #free_geometry_rows = [];
175
+
176
+ /**
177
+ * Per geometry row: ordinals that are free, and the high-water mark of ordinals handed out.
178
+ *
179
+ * An ordinal is a page's dense index *within its geometry*, and it exists only to index the
180
+ * per-instance claim bitmask. Its capacity is therefore what one instance's slice of that
181
+ * bitmask costs, which is why the mark matters and why ordinals are recycled rather than
182
+ * counted upward.
183
+ *
184
+ * @type {Map<number, {free: number[], high_water: number}>}
185
+ */
186
+ #ordinals = new Map();
187
+
188
+ /**
189
+ * Clusters installed here whose `child_ref` named an entry outside their page's child table.
190
+ *
191
+ * Impossible in a container the validator accepts, reachable because validation is off by
192
+ * default, and worth counting rather than asserting on: the cluster is written as a leaf and
193
+ * drawn, which is what `vgeo_select_cut` does with the same input.
194
+ *
195
+ * @type {number}
196
+ */
197
+ #broken_child_refs = 0;
198
+
199
+ /**
200
+ * Scratch for {@link refresh_selectable}: live parent references per resident group, indexed by
201
+ * the group's word offset in the arena. One map reused across calls rather than one allocated
202
+ * per call, because activation runs on every install and every eviction.
203
+ * @type {Map<number, number>}
204
+ */
205
+ #live_refs = new Map();
206
+
207
+ /**
208
+ * @param {Object} [options]
209
+ * @param {number} [options.page_capacity] slots
210
+ * @param {number} [options.page_size] bytes a page of every container held here is bounded by;
211
+ * what the slot stride is derived from
212
+ * @param {number} [options.slot_words] the stride itself, overriding the derivation
213
+ * @param {number} [options.lookup_words]
214
+ * @param {number} [options.geometry_capacity] rows
215
+ */
216
+ constructor({
217
+ page_capacity = 1024,
218
+ page_size = VGEO_DEFAULT_PAGE_SIZE,
219
+ slot_words = vgeo_runtime_page_slot_words(page_size),
220
+ lookup_words = 1 << 16,
221
+ geometry_capacity = 256,
222
+ } = {}) {
223
+ assert.isPositiveInteger(page_capacity, 'page_capacity');
224
+ assert.isPositiveInteger(slot_words, 'slot_words');
225
+ assert.isNonNegativeInteger(lookup_words, 'lookup_words');
226
+ assert.isNonNegativeInteger(geometry_capacity, 'geometry_capacity');
227
+ assert.greaterThan(slot_words, VGEO_PAGE_ROW_WORDS, 'slot_words');
228
+
229
+ this.#slot_words = slot_words;
230
+ this.#page_capacity = page_capacity;
231
+
232
+ this.#lookup = new Uint32Array(lookup_words);
233
+ this.#geometries = new Uint32Array(geometry_capacity * VGEO_GEOMETRY_ROW_WORDS);
234
+ this.#generations = new Uint32Array(page_capacity);
235
+
236
+ const words = new Uint32Array(page_capacity * slot_words);
237
+
238
+ this.#arena = words;
239
+ this.#arena_floats = new Float32Array(words.buffer, words.byteOffset, words.length);
240
+
241
+ this.#lookup_allocator = new OffsetAllocator(lookup_words);
242
+ }
243
+
244
+ /**
245
+ * @returns {Uint32Array}
246
+ */
247
+ get lookup_words() {
248
+ return this.#lookup;
249
+ }
250
+
251
+ /**
252
+ * @returns {Uint32Array}
253
+ */
254
+ get arena_words() {
255
+ return this.#arena;
256
+ }
257
+
258
+ /**
259
+ * @returns {Uint32Array}
260
+ */
261
+ get geometry_words() {
262
+ return this.#geometries;
263
+ }
264
+
265
+ /**
266
+ * @returns {number}
267
+ */
268
+ get broken_child_refs() {
269
+ return this.#broken_child_refs;
270
+ }
271
+
272
+ /**
273
+ * @returns {number}
274
+ */
275
+ get slot_words() {
276
+ return this.#slot_words;
277
+ }
278
+
279
+ /**
280
+ * @returns {number}
281
+ */
282
+ get page_capacity() {
283
+ return this.#page_capacity;
284
+ }
285
+
286
+ /**
287
+ * One past the highest page row ever handed out — what sizes a per-page-row stamp region.
288
+ * @returns {number}
289
+ */
290
+ get page_row_high_water() {
291
+ return this.#page_row_high_water;
292
+ }
293
+
294
+ /**
295
+ * @returns {number}
296
+ */
297
+ get resident_page_count() {
298
+ return this.#resident_rows.size;
299
+ }
300
+
301
+ /**
302
+ * Whether a row could take a page without anything being evicted first.
303
+ * @returns {boolean}
304
+ */
305
+ get has_free_page_row() {
306
+ return this.#free_page_rows.length > 0 || this.#page_row_high_water < this.#page_capacity;
307
+ }
308
+
309
+ /**
310
+ * How many times this row has been freed — the token that tells a feedback record about the
311
+ * page that *was* here from one about the page that is.
312
+ *
313
+ * @param {number} page_row
314
+ * @returns {number}
315
+ */
316
+ generation_of(page_row) {
317
+ return this.#generations[page_row];
318
+ }
319
+
320
+ /**
321
+ * Give a container a geometry row and a page lookup of its own.
322
+ *
323
+ * The lookup is `total_pages` words and starts entirely absent, which is the correct state for
324
+ * a container nothing has been installed from: every cluster that refers into it clamps, draws
325
+ * coarse, and wants.
326
+ *
327
+ * @param {number} page_count the container's `total_pages`
328
+ * @param {number} [ordinal_capacity] initial claim width; grown by installs
329
+ * @returns {number} geometry row
330
+ * @throws {Error} when the lookup does not fit
331
+ */
332
+ register_geometry(page_count, ordinal_capacity = 0) {
333
+ assert.isNonNegativeInteger(page_count, 'page_count');
334
+ assert.greaterThan(page_count, 0, 'page_count');
335
+
336
+ const recycled = this.#free_geometry_rows.pop();
337
+
338
+ const row = recycled ?? this.#geometry_row_high_water;
339
+
340
+ if (recycled === undefined
341
+ && (row + 1) * VGEO_GEOMETRY_ROW_WORDS > this.#geometries.length) {
342
+ throw new Error(
343
+ `no room for a ${row + 1}th virtual geometry`
344
+ + ` in a table of ${this.#geometries.length / VGEO_GEOMETRY_ROW_WORDS}`
345
+ );
346
+ }
347
+
348
+ const allocation = this.#lookup_allocator.allocate(page_count);
349
+
350
+ if (allocation.offset === ALLOCATOR_NO_SPACE) {
351
+ throw new Error(
352
+ `no room for a ${page_count}-page lookup in ${this.#lookup.length} words`
353
+ );
354
+ }
355
+
356
+ if (recycled === undefined) {
357
+ this.#geometry_row_high_water = row + 1;
358
+ }
359
+
360
+ this.#geometry_allocations.set(row, allocation);
361
+
362
+ this.#lookup.fill(
363
+ VGEO_PAGE_ROW_NONE, allocation.offset, allocation.offset + page_count
364
+ );
365
+
366
+ const base = row * VGEO_GEOMETRY_ROW_WORDS;
367
+
368
+ this.#geometries[base + VGEO_GEOMETRY_ROW_OFFSET.ROOT_PAGE_ROW] = VGEO_PAGE_ROW_NONE;
369
+ this.#geometries[base + VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET] = allocation.offset;
370
+ this.#geometries[base + VGEO_GEOMETRY_ROW_OFFSET.PAGE_COUNT] = page_count;
371
+ this.#geometries[base + VGEO_GEOMETRY_ROW_OFFSET.ORDINAL_CAPACITY] = ordinal_capacity;
372
+
373
+ this.#ordinals.set(row, { free: [], high_water: ordinal_capacity });
374
+ this.#geometry_rows.set(row, new Set());
375
+
376
+ return row;
377
+ }
378
+
379
+ /**
380
+ * Drop a geometry: every page it has here, then its lookup and its row.
381
+ *
382
+ * @param {number} geometry_row
383
+ * @returns {number[]} the page rows that were evicted, so a caller holding per-row state can
384
+ * drop it too
385
+ */
386
+ unregister_geometry(geometry_row) {
387
+ const rows = this.#geometry_rows.get(geometry_row);
388
+
389
+ if (rows === undefined) {
390
+ return [];
391
+ }
392
+
393
+ const evicted = Array.from(rows);
394
+
395
+ for (const page_row of evicted) {
396
+ this.evict(page_row);
397
+ }
398
+
399
+ const allocation = this.#geometry_allocations.get(geometry_row);
400
+
401
+ if (allocation !== undefined) {
402
+ this.#lookup.fill(
403
+ 0,
404
+ allocation.offset,
405
+ allocation.offset + this.geometry_field(
406
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.PAGE_COUNT
407
+ )
408
+ );
409
+
410
+ this.#lookup_allocator.free(allocation);
411
+ this.#geometry_allocations.delete(geometry_row);
412
+ }
413
+
414
+ this.#geometries.fill(
415
+ 0,
416
+ geometry_row * VGEO_GEOMETRY_ROW_WORDS,
417
+ (geometry_row + 1) * VGEO_GEOMETRY_ROW_WORDS
418
+ );
419
+
420
+ this.#geometries[
421
+ geometry_row * VGEO_GEOMETRY_ROW_WORDS + VGEO_GEOMETRY_ROW_OFFSET.ROOT_PAGE_ROW
422
+ ] = VGEO_PAGE_ROW_NONE;
423
+
424
+ this.#geometry_rows.delete(geometry_row);
425
+ this.#ordinals.delete(geometry_row);
426
+ this.#free_geometry_rows.push(geometry_row);
427
+
428
+ return evicted;
429
+ }
430
+
431
+ /**
432
+ * @param {number} geometry_row
433
+ * @param {number} field one of {@link VGEO_GEOMETRY_ROW_OFFSET}
434
+ * @returns {number}
435
+ */
436
+ geometry_field(geometry_row, field) {
437
+ return this.#geometries[geometry_row * VGEO_GEOMETRY_ROW_WORDS + field];
438
+ }
439
+
440
+ /**
441
+ * @param {number} geometry_row
442
+ * @param {number} page_index container page index
443
+ * @returns {number} page row, or {@link VGEO_PAGE_ROW_NONE}
444
+ */
445
+ page_row_of(geometry_row, page_index) {
446
+ const base = this.geometry_field(
447
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET
448
+ );
449
+
450
+ return this.#lookup[base + page_index];
451
+ }
452
+
453
+ /**
454
+ * @param {number} page_row
455
+ * @param {number} field one of {@link VGEO_PAGE_ROW_OFFSET}
456
+ * @returns {number}
457
+ */
458
+ page_field(page_row, field) {
459
+ return this.#arena[page_row * this.#slot_words + field];
460
+ }
461
+
462
+ /**
463
+ * Word offset of a page's node rows — the slot, past its header.
464
+ *
465
+ * @param {number} page_row
466
+ * @returns {number}
467
+ */
468
+ node_base(page_row) {
469
+ return page_row * this.#slot_words + VGEO_PAGE_ROW_WORDS;
470
+ }
471
+
472
+ /**
473
+ * @param {number} page_row
474
+ * @returns {number}
475
+ */
476
+ group_base(page_row) {
477
+ return this.node_base(page_row)
478
+ + this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.NODE_COUNT) * VGEO_NODE_ROW_WORDS;
479
+ }
480
+
481
+ /**
482
+ * @param {number} page_row
483
+ * @returns {number}
484
+ */
485
+ cluster_base(page_row) {
486
+ return this.group_base(page_row)
487
+ + this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT) * VGEO_GROUP_ROW_WORDS;
488
+ }
489
+
490
+ /**
491
+ * Make a decoded page resident: one slot, one lookup word.
492
+ *
493
+ * The page installs **not selectable** — every group's flag is clear — because a page that
494
+ * arrives drawing is a page that draws before the ancestors it hangs off have. Activation is
495
+ * {@link refresh_selectable}.
496
+ *
497
+ * @param {number} geometry_row
498
+ * @param {VGeoPage} page as `vgeo_read_page` decoded it
499
+ * @param {number} meshlet_base row of this page's cluster 0 in the meshlet metadata pool
500
+ * @returns {number} page row
501
+ * @throws {Error} when no slot is free — the caller's cue to evict, since what to drop is a
502
+ * budget question and this object holds no budget — or when the page's rows do not fit one
503
+ * @see broken_child_refs for what the last install reported about the page it wrote
504
+ */
505
+ install(geometry_row, page, meshlet_base) {
506
+ assert.defined(page, 'page');
507
+ assert.isNonNegativeInteger(meshlet_base, 'meshlet_base');
508
+
509
+ const node_count = page.node_count;
510
+ const group_count = page.group_cluster_first.length;
511
+ const cluster_count = page.cluster_count;
512
+
513
+ const words = VGEO_PAGE_ROW_WORDS
514
+ + vgeo_runtime_page_block_words(node_count, group_count, cluster_count);
515
+
516
+ if (words > this.#slot_words) {
517
+ throw new Error(
518
+ `virtual geometry page ${page.index} needs ${words} words and this tier's slots`
519
+ + ` hold ${this.#slot_words}. Its ${node_count} nodes, ${group_count} groups and`
520
+ + ` ${cluster_count} clusters are legal and unusually dense for their page size;`
521
+ + ` construct the pool with slot_words of at least ${words}.`
522
+ );
523
+ }
524
+
525
+ const page_row = this.#take_page_row();
526
+
527
+ if (page_row === -1) {
528
+ throw new Error(
529
+ `virtual geometry page table is full at ${this.#page_capacity} slots`
530
+ );
531
+ }
532
+
533
+ const ordinal = this.#take_ordinal(geometry_row);
534
+
535
+ const base = page_row * this.#slot_words;
536
+
537
+ this.#broken_child_refs += vgeo_runtime_write_page_block(
538
+ this.#arena, this.#arena_floats, base + VGEO_PAGE_ROW_WORDS, page
539
+ );
540
+
541
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.NODE_COUNT] = node_count;
542
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.GROUP_COUNT] = group_count;
543
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.CLUSTER_COUNT] = cluster_count;
544
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.MESHLET_BASE] = meshlet_base;
545
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.PAGE_INDEX] = page.index;
546
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.ORDINAL] = ordinal;
547
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.GEOMETRY_ROW] = geometry_row;
548
+ this.#arena[base + VGEO_PAGE_ROW_OFFSET.DECODED_SIZE] = page.decoded_size;
549
+
550
+ this.#resident_rows.add(page_row);
551
+ this.#geometry_rows.get(geometry_row).add(page_row);
552
+
553
+ const lookup_base = this.geometry_field(
554
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET
555
+ );
556
+
557
+ this.#lookup[lookup_base + page.index] = page_row;
558
+
559
+ if (page.index === 0) {
560
+ this.#geometries[
561
+ geometry_row * VGEO_GEOMETRY_ROW_WORDS + VGEO_GEOMETRY_ROW_OFFSET.ROOT_PAGE_ROW
562
+ ] = page_row;
563
+ }
564
+
565
+ return page_row;
566
+ }
567
+
568
+ /**
569
+ * How many words of a slot an install of this page would take, header included.
570
+ *
571
+ * @param {VGeoPage} page
572
+ * @returns {number}
573
+ */
574
+ slot_words_for(page) {
575
+ return VGEO_PAGE_ROW_WORDS + vgeo_runtime_page_block_words(
576
+ page.node_count, page.group_cluster_first.length, page.cluster_count
577
+ );
578
+ }
579
+
580
+ /**
581
+ * Drop a resident page.
582
+ *
583
+ * The lookup word goes back to {@link VGEO_PAGE_ROW_NONE} first, and that single word is the
584
+ * whole of what makes the eviction safe: every cluster anywhere that refines into this page
585
+ * reads it, finds nothing, and clamps — drawing the coarser surface it was already drawing
586
+ * before the page arrived. Nothing is patched and nothing is scanned.
587
+ *
588
+ * The slot's **header** is zeroed and its rows are not. A row nothing points at is unreachable
589
+ * — the lookup is the only way in and no queue item survives a frame — and leaving the rows
590
+ * costs nothing while zeroing them would dirty a slot's worth of words per eviction.
591
+ *
592
+ * @param {number} page_row
593
+ * @returns {boolean} whether the row was resident
594
+ */
595
+ evict(page_row) {
596
+ if (!this.#resident_rows.has(page_row)) {
597
+ return false;
598
+ }
599
+
600
+ const geometry_row = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GEOMETRY_ROW);
601
+ const page_index = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.PAGE_INDEX);
602
+
603
+ const lookup_base = this.geometry_field(
604
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET
605
+ );
606
+
607
+ this.#lookup[lookup_base + page_index] = VGEO_PAGE_ROW_NONE;
608
+
609
+ if (page_index === 0) {
610
+ this.#geometries[
611
+ geometry_row * VGEO_GEOMETRY_ROW_WORDS + VGEO_GEOMETRY_ROW_OFFSET.ROOT_PAGE_ROW
612
+ ] = VGEO_PAGE_ROW_NONE;
613
+ }
614
+
615
+ this.#give_ordinal(geometry_row, this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.ORDINAL));
616
+
617
+ const base = page_row * this.#slot_words;
618
+
619
+ this.#arena.fill(0, base, base + VGEO_PAGE_ROW_WORDS);
620
+
621
+ this.#resident_rows.delete(page_row);
622
+ this.#geometry_rows.get(geometry_row)?.delete(page_row);
623
+
624
+ this.#free_page_rows.push(page_row);
625
+ this.#generations[page_row]++;
626
+
627
+ return true;
628
+ }
629
+
630
+ /**
631
+ * The page rows this geometry holds, which is also what an eviction candidate's children are
632
+ * looked for among.
633
+ *
634
+ * @param {number} geometry_row
635
+ * @returns {Set<number>}
636
+ */
637
+ pages_of(geometry_row) {
638
+ return this.#geometry_rows.get(geometry_row) ?? EMPTY_ROWS;
639
+ }
640
+
641
+ /**
642
+ * Whether any page resident **in this tier** refines out of this one.
643
+ *
644
+ * Design §6's eviction rule reads "no child page resident in this tier", and this answers it out
645
+ * of the arena rather than out of the RAM reader's child table — which returned "no children"
646
+ * the moment the RAM tier dropped the page, orphaning every VRAM-resident child under it
647
+ * (REVIEW_LEDGER M9).
648
+ *
649
+ * @param {number} page_row
650
+ * @returns {boolean}
651
+ */
652
+ has_resident_child(page_row) {
653
+ const geometry_row = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GEOMETRY_ROW);
654
+
655
+ const lookup_base = this.geometry_field(
656
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET
657
+ );
658
+
659
+ const cluster_count = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.CLUSTER_COUNT);
660
+ const cluster_base = this.cluster_base(page_row);
661
+
662
+ for (let cluster = 0; cluster < cluster_count; cluster++) {
663
+ const child_page_index = this.#arena[
664
+ cluster_base + cluster * VGEO_CLUSTER_ROW_WORDS
665
+ + VGEO_CLUSTER_ROW_OFFSET.CHILD_PAGE_INDEX
666
+ ];
667
+
668
+ if (child_page_index === VGEO_NO_CHILD_PAGE) {
669
+ continue;
670
+ }
671
+
672
+ const child_row = this.#lookup[lookup_base + child_page_index];
673
+
674
+ if (child_row !== VGEO_PAGE_ROW_NONE && child_row !== page_row) {
675
+ return true;
676
+ }
677
+ }
678
+
679
+ return false;
680
+ }
681
+
682
+ /**
683
+ * Recompute one geometry's group selectability over **this tier's** residency.
684
+ *
685
+ * `VIRTUAL_GEOMETRY_DESIGN.md` §6: a group is selectable iff every one of its parent-cluster
686
+ * references is live — the referencing cluster's page resident here, and that cluster's own
687
+ * group selectable here. Groups with no parents at all are the top of the DAG and are selectable
688
+ * the moment their page is. That is a closure over the resident set, so it is computed as one:
689
+ * seed the parentless groups and cascade down, counting a reference only when the group making
690
+ * it is already selectable.
691
+ *
692
+ * Recomputed whole rather than propagated incrementally, because an install can turn groups on
693
+ * several pages away and an eviction can turn them off again, and because the cost is the same
694
+ * order as the install that provoked it — one pass over the geometry's resident clusters. The
695
+ * incremental form is a later optimization with a counter to keep correct; the one thing it
696
+ * must not be is a *second* implementation of the rule.
697
+ *
698
+ * @param {number} geometry_row
699
+ * @returns {number[]} page rows whose group flags changed, for the caller to upload
700
+ */
701
+ refresh_selectable(geometry_row) {
702
+ const rows = this.#geometry_rows.get(geometry_row);
703
+
704
+ if (rows === undefined || rows.size === 0) {
705
+ return [];
706
+ }
707
+
708
+ const arena = this.#arena;
709
+ const live = this.#live_refs;
710
+
711
+ live.clear();
712
+
713
+ const lookup_base = this.geometry_field(
714
+ geometry_row, VGEO_GEOMETRY_ROW_OFFSET.LOOKUP_WORD_OFFSET
715
+ );
716
+
717
+ /**
718
+ * Word offsets of group rows to turn on, as a stack.
719
+ * @type {number[]}
720
+ */
721
+ const queue = [];
722
+
723
+ for (const page_row of rows) {
724
+ const group_count = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT);
725
+ const group_base = this.group_base(page_row);
726
+
727
+ for (let group = 0; group < group_count; group++) {
728
+ const row = group_base + group * VGEO_GROUP_ROW_WORDS;
729
+
730
+ live.set(row, 0);
731
+
732
+ if (arena[row + VGEO_GROUP_ROW_OFFSET.PARENT_COUNT] === 0) {
733
+ queue.push(row);
734
+ }
735
+ }
736
+ }
737
+
738
+ /**
739
+ * @type {Set<number>}
740
+ */
741
+ const selectable = new Set();
742
+
743
+ while (queue.length > 0) {
744
+ const group_row = queue.pop();
745
+
746
+ if (selectable.has(group_row)) {
747
+ continue;
748
+ }
749
+
750
+ selectable.add(group_row);
751
+
752
+ const page_row = Math.floor(group_row / this.#slot_words);
753
+
754
+ const cluster_base = this.cluster_base(page_row);
755
+
756
+ const first = arena[group_row + VGEO_GROUP_ROW_OFFSET.CLUSTER_FIRST];
757
+ const count = arena[group_row + VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT];
758
+
759
+ for (let i = 0; i < count; i++) {
760
+ const cluster = cluster_base + (first + i) * VGEO_CLUSTER_ROW_WORDS;
761
+
762
+ const child_page_index = arena[
763
+ cluster + VGEO_CLUSTER_ROW_OFFSET.CHILD_PAGE_INDEX
764
+ ];
765
+
766
+ if (child_page_index === VGEO_NO_CHILD_PAGE) {
767
+ continue;
768
+ }
769
+
770
+ const child_row = this.#lookup[lookup_base + child_page_index];
771
+
772
+ if (child_row === VGEO_PAGE_ROW_NONE) {
773
+ // the child page is not in this tier; its groups stay blocked, which is exactly
774
+ // what makes the cluster above clamp and draw
775
+ continue;
776
+ }
777
+
778
+ const child_group = arena[cluster + VGEO_CLUSTER_ROW_OFFSET.CHILD_GROUP];
779
+
780
+ if (child_group >= this.page_field(child_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT)) {
781
+ // a child_group outside the child page's table; counting it would corrupt a
782
+ // neighbour's counter, and the traversal draws the referring cluster instead
783
+ continue;
784
+ }
785
+
786
+ const child = this.group_base(child_row) + child_group * VGEO_GROUP_ROW_WORDS;
787
+
788
+ const counted = live.get(child) + 1;
789
+
790
+ live.set(child, counted);
791
+
792
+ if (counted === arena[child + VGEO_GROUP_ROW_OFFSET.PARENT_COUNT]) {
793
+ queue.push(child);
794
+ }
795
+ }
796
+ }
797
+
798
+ /**
799
+ * @type {number[]}
800
+ */
801
+ const changed = [];
802
+
803
+ for (const page_row of rows) {
804
+ const group_count = this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT);
805
+ const group_base = this.group_base(page_row);
806
+
807
+ let dirty = false;
808
+
809
+ for (let group = 0; group < group_count; group++) {
810
+ const row = group_base + group * VGEO_GROUP_ROW_WORDS;
811
+
812
+ const flags = selectable.has(row) ? VGEO_GROUP_FLAG_SELECTABLE : 0;
813
+
814
+ if (arena[row + VGEO_GROUP_ROW_OFFSET.FLAGS] !== flags) {
815
+ arena[row + VGEO_GROUP_ROW_OFFSET.FLAGS] = flags;
816
+
817
+ dirty = true;
818
+ }
819
+ }
820
+
821
+ if (dirty) {
822
+ changed.push(page_row);
823
+ }
824
+ }
825
+
826
+ return changed;
827
+ }
828
+
829
+ /**
830
+ * Publish one group's selectability directly.
831
+ *
832
+ * {@link refresh_selectable} is what a runtime calls; this is for a spec that needs a state the
833
+ * rule happens not to produce on the assets measured so far — the cut's own-group term is
834
+ * correctness for exactly such a state, and a guard nothing can construct is a guard nothing can
835
+ * test. The next {@link refresh_selectable} overwrites it.
836
+ *
837
+ * @param {number} page_row
838
+ * @param {number} group
839
+ * @param {boolean} selectable
840
+ * @returns {void}
841
+ */
842
+ set_group_selectable(page_row, group, selectable) {
843
+ const word = this.group_base(page_row)
844
+ + group * VGEO_GROUP_ROW_WORDS
845
+ + VGEO_GROUP_ROW_OFFSET.FLAGS;
846
+
847
+ this.#arena[word] = selectable ? VGEO_GROUP_FLAG_SELECTABLE : 0;
848
+ }
849
+
850
+ /**
851
+ * @param {number} page_row
852
+ * @param {number} group
853
+ * @returns {boolean}
854
+ */
855
+ is_group_selectable(page_row, group) {
856
+ const word = this.group_base(page_row)
857
+ + group * VGEO_GROUP_ROW_WORDS
858
+ + VGEO_GROUP_ROW_OFFSET.FLAGS;
859
+
860
+ return (this.#arena[word] & VGEO_GROUP_FLAG_SELECTABLE) !== 0;
861
+ }
862
+
863
+ /**
864
+ * Word range of one page's slot, as `[start, count]` — what an install dirties.
865
+ *
866
+ * @param {number} page_row
867
+ * @returns {number[]}
868
+ */
869
+ slot_range(page_row) {
870
+ const base = page_row * this.#slot_words;
871
+
872
+ const words = VGEO_PAGE_ROW_WORDS + vgeo_runtime_page_block_words(
873
+ this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.NODE_COUNT),
874
+ this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT),
875
+ this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.CLUSTER_COUNT)
876
+ );
877
+
878
+ return [base, words];
879
+ }
880
+
881
+ /**
882
+ * Word range of one page's group rows — what an activation pass dirties.
883
+ *
884
+ * @param {number} page_row
885
+ * @returns {number[]}
886
+ */
887
+ group_range(page_row) {
888
+ return [
889
+ this.group_base(page_row),
890
+ this.page_field(page_row, VGEO_PAGE_ROW_OFFSET.GROUP_COUNT) * VGEO_GROUP_ROW_WORDS,
891
+ ];
892
+ }
893
+
894
+ /**
895
+ * Republish one page's meshlet base, after the shared meshlet pool relocated its batch.
896
+ *
897
+ * @param {number} page_row
898
+ * @param {number} meshlet_base
899
+ * @returns {boolean} whether it moved
900
+ */
901
+ set_meshlet_base(page_row, meshlet_base) {
902
+ const word = page_row * this.#slot_words + VGEO_PAGE_ROW_OFFSET.MESHLET_BASE;
903
+
904
+ if (this.#arena[word] === meshlet_base) {
905
+ return false;
906
+ }
907
+
908
+ this.#arena[word] = meshlet_base;
909
+
910
+ return true;
911
+ }
912
+
913
+ /**
914
+ * @returns {number} row, or -1 when every slot is taken
915
+ */
916
+ #take_page_row() {
917
+ const recycled = this.#free_page_rows.pop();
918
+
919
+ if (recycled !== undefined) {
920
+ return recycled;
921
+ }
922
+
923
+ const row = this.#page_row_high_water;
924
+
925
+ if (row >= this.#page_capacity) {
926
+ return -1;
927
+ }
928
+
929
+ this.#page_row_high_water = row + 1;
930
+
931
+ return row;
932
+ }
933
+
934
+ /**
935
+ * @param {number} geometry_row
936
+ * @returns {number}
937
+ */
938
+ #take_ordinal(geometry_row) {
939
+ const state = this.#ordinals.get(geometry_row);
940
+
941
+ const recycled = state.free.pop();
942
+
943
+ if (recycled !== undefined) {
944
+ return recycled;
945
+ }
946
+
947
+ const ordinal = state.high_water;
948
+
949
+ state.high_water = ordinal + 1;
950
+
951
+ this.#geometries[
952
+ geometry_row * VGEO_GEOMETRY_ROW_WORDS + VGEO_GEOMETRY_ROW_OFFSET.ORDINAL_CAPACITY
953
+ ] = state.high_water;
954
+
955
+ return ordinal;
956
+ }
957
+
958
+ /**
959
+ * An ordinal goes back to the free list and the capacity does **not** come down with it.
960
+ *
961
+ * The capacity is what one instance's slice of the claim bitmask costs, so lowering it would
962
+ * make the bitmask's layout a function of eviction order — and the layout is fixed for the
963
+ * frame by a prefix scan taken before any of this runs.
964
+ *
965
+ * @param {number} geometry_row
966
+ * @param {number} ordinal
967
+ */
968
+ #give_ordinal(geometry_row, ordinal) {
969
+ this.#ordinals.get(geometry_row)?.free.push(ordinal);
970
+ }
971
+ }
972
+
973
+ /**
974
+ * @type {Set<number>}
975
+ */
976
+ const EMPTY_ROWS = new Set();