@woosh/meep-engine 3.24.0 → 3.26.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 (349) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +59 -0
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +285 -0
  5. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
  7. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
  9. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
  11. package/src/engine/graphics3/PickingSystem.d.ts +3 -3
  12. package/src/engine/graphics3/PickingSystem.d.ts.map +1 -1
  13. package/src/engine/graphics3/PickingSystem.js +52 -55
  14. package/src/engine/graphics3/shader_picking_gather.d.ts +40 -0
  15. package/src/engine/graphics3/shader_picking_gather.d.ts.map +1 -0
  16. package/src/engine/graphics3/shader_picking_gather.js +120 -0
  17. package/src/engine/physics/cloth/MEASUREMENTS.md +1632 -0
  18. package/src/engine/physics/cloth/PLAN.md +738 -95
  19. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  20. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  21. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  22. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts +52 -0
  23. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.d.ts.map +1 -0
  24. package/src/engine/physics/cloth/ecs/ClothColliderSerializationAdapter.js +70 -0
  25. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts +12 -3
  26. package/src/engine/physics/cloth/ecs/ClothDynamics.d.ts.map +1 -1
  27. package/src/engine/physics/cloth/ecs/ClothDynamics.js +340 -331
  28. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +101 -29
  29. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  30. package/src/engine/physics/cloth/ecs/ClothInstance.js +110 -31
  31. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts +84 -0
  32. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.d.ts.map +1 -0
  33. package/src/engine/physics/cloth/ecs/ClothSerializationAdapter.js +141 -0
  34. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +71 -81
  35. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  36. package/src/engine/physics/cloth/ecs/ClothSystem.js +135 -856
  37. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts +48 -0
  38. package/src/engine/physics/cloth/ecs/ClothWorkerCore.d.ts.map +1 -0
  39. package/src/engine/physics/cloth/ecs/ClothWorkerCore.js +107 -0
  40. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +323 -0
  41. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -0
  42. package/src/engine/physics/cloth/ecs/ClothWorld.js +1248 -0
  43. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +192 -0
  44. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -0
  45. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +578 -0
  46. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts +2 -0
  47. package/src/engine/physics/cloth/ecs/cloth.worker.d.ts.map +1 -0
  48. package/src/engine/physics/cloth/ecs/cloth.worker.js +12 -0
  49. package/src/engine/physics/cloth/ecs/cloth_build_rows.d.ts.map +1 -1
  50. package/src/engine/physics/cloth/ecs/cloth_build_rows.js +2 -1
  51. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +85 -3
  52. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  53. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +366 -271
  54. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +95 -12
  55. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  56. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +170 -27
  57. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  58. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  59. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  60. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  61. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  62. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  63. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts +29 -0
  64. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.d.ts.map +1 -0
  65. package/src/engine/physics/cloth/ecs/cloth_worker_protocol.js +78 -0
  66. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  67. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  68. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts +17 -0
  69. package/src/engine/physics/cloth/ecs/makeClothWorker.d.ts.map +1 -0
  70. package/src/engine/physics/cloth/ecs/makeClothWorker.js +18 -0
  71. package/src/engine/physics/cloth/playground/README.md +73 -2
  72. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  73. package/src/engine/physics/cloth/playground/wind.html +166 -0
  74. package/src/engine/physics/cloth/playground/wind_build.d.ts +81 -0
  75. package/src/engine/physics/cloth/playground/wind_build.d.ts.map +1 -0
  76. package/src/engine/physics/cloth/playground/wind_build.js +141 -0
  77. package/src/engine/physics/cloth/playground/wind_main.d.ts +2 -0
  78. package/src/engine/physics/cloth/playground/wind_main.d.ts.map +1 -0
  79. package/src/engine/physics/cloth/playground/wind_main.js +681 -0
  80. package/src/engine/physics/cloth/solver/ClothState.d.ts +134 -79
  81. package/src/engine/physics/cloth/solver/ClothState.d.ts.map +1 -1
  82. package/src/engine/physics/cloth/solver/ClothState.js +273 -12
  83. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  84. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  85. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  86. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +45 -2
  87. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  88. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +46 -2
  89. package/src/engine/physics/cloth/solver/cloth_step.d.ts +21 -2
  90. package/src/engine/physics/cloth/solver/cloth_step.d.ts.map +1 -1
  91. package/src/engine/physics/cloth/solver/cloth_step.js +173 -4
  92. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts +40 -0
  93. package/src/engine/physics/cloth/solver/cloth_step_instance.d.ts.map +1 -0
  94. package/src/engine/physics/cloth/solver/cloth_step_instance.js +284 -0
  95. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts +107 -0
  96. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.d.ts.map +1 -0
  97. package/src/engine/physics/cloth/solver/cloth_wind_accelerate.js +263 -0
  98. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  99. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  100. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  101. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +102 -0
  102. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -0
  103. package/src/engine/physics/cloth/wind/AbstractClothWind.js +114 -0
  104. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +87 -0
  105. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -0
  106. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +185 -0
  107. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts +79 -0
  108. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -0
  109. package/src/engine/physics/cloth/wind/ClothFluidWind.js +213 -0
  110. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +106 -0
  111. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -0
  112. package/src/engine/physics/cloth/wind/cloth_sample_air.js +297 -0
  113. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  114. package/src/shade/device/mock/texture_copy.d.ts.map +1 -1
  115. package/src/shade/device/mock/texture_copy.js +28 -0
  116. package/src/shade/playground/vgeo_runtime/README.md +94 -0
  117. package/src/shade/playground/vgeo_runtime/index.html +62 -0
  118. package/src/shade/playground/vgeo_runtime/main.d.ts +2 -0
  119. package/src/shade/playground/vgeo_runtime/main.d.ts.map +1 -0
  120. package/src/shade/playground/vgeo_runtime/main.js +399 -0
  121. package/src/shade/playground/vgeo_viewer/sample_asset.js +1 -1
  122. package/src/shade/renderer/camera/Camera.d.ts +14 -0
  123. package/src/shade/renderer/camera/Camera.d.ts.map +1 -1
  124. package/src/shade/renderer/camera/Camera.js +7 -1
  125. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.d.ts.map +1 -1
  126. package/src/shade/renderer/geometry/GEOMETRY_METADATA_STRUCT.js +16 -1
  127. package/src/shade/renderer/geometry/GPUGeometryManager.d.ts.map +1 -1
  128. package/src/shade/renderer/geometry/GPUGeometryManager.js +29 -9
  129. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts +10 -0
  130. package/src/shade/renderer/geometry/GPUGeometryMetadata.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/GPUGeometryMetadata.js +13 -0
  132. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +46 -18
  133. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +70 -22
  134. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +97 -19
  135. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +12 -2
  136. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  137. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +14 -3
  138. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts +2 -2
  139. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +124 -6
  141. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts +22 -0
  142. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.d.ts.map +1 -0
  143. package/src/shade/renderer/geometry/virtual/build/vgeo_page_node_count.js +53 -0
  144. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +8 -1
  145. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  146. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +8 -1
  147. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts +16 -0
  148. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.d.ts.map +1 -0
  149. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FEATURE_IGNORABLE_BOUNDED_PAGES.js +15 -0
  150. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts +54 -0
  151. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.d.ts.map +1 -0
  152. package/src/shade/renderer/geometry/virtual/format/page/VGEO_DEFAULT_PAGE_SIZE.js +53 -0
  153. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +13 -2
  154. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +14 -2
  156. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +100 -0
  157. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  158. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +113 -0
  159. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.d.ts.map +1 -1
  160. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_header.js +5 -0
  161. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  162. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +108 -0
  163. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  164. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +23 -0
  165. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  166. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +40 -0
  167. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +228 -0
  168. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -0
  169. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +932 -0
  170. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +176 -0
  171. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -0
  172. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +425 -0
  173. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +241 -0
  174. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +574 -0
  175. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +120 -0
  176. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -0
  177. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +674 -0
  178. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +287 -0
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -0
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +976 -0
  181. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +42 -0
  182. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -0
  183. package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +96 -0
  184. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts +16 -0
  185. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.d.ts.map +1 -0
  186. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_CULL.js +15 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts +19 -0
  188. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -0
  189. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +83 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts +9 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.d.ts.map +1 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_WORKGROUP_SIZE.js +8 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +19 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -0
  195. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +160 -0
  196. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts +3 -0
  197. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +255 -0
  199. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts +3 -0
  200. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -0
  201. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +80 -0
  202. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts +3 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +144 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts +3 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -0
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +530 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +39 -0
  209. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -0
  210. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +54 -0
  211. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts +41 -0
  212. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.d.ts.map +1 -0
  213. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_if_enabled.js +56 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts +49 -0
  215. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -0
  216. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +328 -0
  217. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts +10 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.d.ts.map +1 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_OFFSET.js +51 -0
  220. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts +13 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.d.ts.map +1 -0
  222. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CLUSTER_ROW_WORDS.js +12 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +17 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts.map +1 -0
  225. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +99 -0
  226. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts +9 -0
  227. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.d.ts.map +1 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +8 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +7 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +27 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts +14 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.d.ts.map +1 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_NONE.js +13 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts +7 -0
  236. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.d.ts.map +1 -0
  237. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_OFFSET.js +35 -0
  238. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts +9 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.d.ts.map +1 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GEOMETRY_ROW_WORDS.js +8 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts +15 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.d.ts.map +1 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_FLAG_SELECTABLE.js +14 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +7 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +36 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +10 -0
  248. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -0
  249. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +9 -0
  250. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts +11 -0
  251. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.d.ts.map +1 -0
  252. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_OFFSET.js +54 -0
  253. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts +9 -0
  254. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.d.ts.map +1 -0
  255. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_INSTANCE_WORDS.js +8 -0
  256. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts +11 -0
  257. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.d.ts.map +1 -0
  258. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_LEAF_BIT.js +10 -0
  259. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts +7 -0
  260. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.d.ts.map +1 -0
  261. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_OFFSET.js +30 -0
  262. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts +9 -0
  263. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.d.ts.map +1 -0
  264. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NODE_ROW_WORDS.js +8 -0
  265. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts +13 -0
  266. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.d.ts.map +1 -0
  267. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_NO_CHILD_PAGE.js +12 -0
  268. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts +14 -0
  269. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.d.ts.map +1 -0
  270. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_NONE.js +13 -0
  271. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +11 -0
  272. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts.map +1 -0
  273. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +61 -0
  274. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts +9 -0
  275. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -0
  276. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +8 -0
  277. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts +13 -0
  278. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.d.ts.map +1 -0
  279. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_ITEM_WORDS.js +12 -0
  280. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts +9 -0
  281. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.d.ts.map +1 -0
  282. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_GROUP_BIT.js +8 -0
  283. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts +20 -0
  284. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.d.ts.map +1 -0
  285. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_WANT_WORDS.js +19 -0
  286. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts +23 -0
  287. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -0
  288. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +70 -0
  289. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +18 -0
  290. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -0
  291. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +25 -0
  292. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts +35 -0
  293. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.d.ts.map +1 -0
  294. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_slot_words.js +41 -0
  295. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +32 -0
  296. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -0
  297. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +138 -0
  298. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  299. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  300. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  301. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +9 -0
  302. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  303. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +12 -1
  304. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  305. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +12 -1
  306. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  307. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +10 -0
  308. package/src/shade/renderer/scene/GPUSceneContext.d.ts +18 -0
  309. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  310. package/src/shade/renderer/scene/GPUSceneContext.js +647 -594
  311. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  312. package/src/shade/renderer/scene/rows/GPUSceneRows.js +22 -0
  313. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts +35 -0
  314. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.d.ts.map +1 -0
  315. package/src/shade/renderer/shader/chunk/geometry/mat3/chunk_mat3_singular_values.js +91 -0
  316. package/src/engine/graphics3/instantiate_scene_bundle.d.ts +0 -54
  317. package/src/engine/graphics3/instantiate_scene_bundle.d.ts.map +0 -1
  318. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts +0 -39
  319. package/src/engine/graphics3/shade_node_to_entity_composition.d.ts.map +0 -1
  320. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts +0 -35
  321. package/src/shade/renderer/animation/compute_skin_world_bounds.d.ts.map +0 -1
  322. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts +0 -96
  323. package/src/shade/renderer/animation/compute_skinned_mesh_clip_bounding_sphere.d.ts.map +0 -1
  324. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts +0 -20
  325. package/src/shade/renderer/animation/pose/pose_evaluate_world.d.ts.map +0 -1
  326. package/src/shade/renderer/animation/skin_bind_transform.d.ts +0 -25
  327. package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +0 -1
  328. package/src/shade/renderer/animation/skin_frame_defect.d.ts +0 -34
  329. package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +0 -1
  330. package/src/shade/renderer/animation/skin_joint_matrix.d.ts +0 -24
  331. package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +0 -1
  332. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +0 -70
  333. package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +0 -1
  334. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts +0 -96
  335. package/src/shade/renderer/buffer/table/GPUReadbackStagingRing.d.ts.map +0 -1
  336. package/src/shade/renderer/scene/InstanceBatch.d.ts +0 -92
  337. package/src/shade/renderer/scene/InstanceBatch.d.ts.map +0 -1
  338. package/src/shade/renderer/scene/Mesh.d.ts +0 -55
  339. package/src/shade/renderer/scene/Mesh.d.ts.map +0 -1
  340. package/src/shade/renderer/scene/Node3D.d.ts +0 -159
  341. package/src/shade/renderer/scene/Node3D.d.ts.map +0 -1
  342. package/src/shade/renderer/scene/Node3DDirtyList.d.ts +0 -59
  343. package/src/shade/renderer/scene/Node3DDirtyList.d.ts.map +0 -1
  344. package/src/shade/renderer/scene/SceneNodeBinding.d.ts +0 -52
  345. package/src/shade/renderer/scene/SceneNodeBinding.d.ts.map +0 -1
  346. package/src/shade/renderer/scene/SkinnedMesh.d.ts +0 -27
  347. package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +0 -1
  348. package/src/shade/renderer/scene/TransformAuthority.d.ts +0 -19
  349. package/src/shade/renderer/scene/TransformAuthority.d.ts.map +0 -1
@@ -0,0 +1,574 @@
1
+ # Virtual Geometry — the GPU runtime
2
+
3
+ > **Two sections are rewritten, and the rest stands.** An adversarial review on 2026-09-11 found
4
+ > ~35 distinct defects in what this document described; they were worked to closure on 2026-09-12
5
+ > and [REVIEW_LEDGER.md](./REVIEW_LEDGER.md) records each one against its commit. What that campaign
6
+ > changed *here* is **§3.1**, where the variable `OffsetAllocator` became fixed-stride page slots
7
+ > with the page header inside the slot, and **§4.9**, whose binding list named a buffer the kernel
8
+ > does not bind and omitted one it does. §4's proofs, §4.2–§4.8 and §5 are as they were, and §8 —
9
+ > the author's own review — is kept for the record with the ledger's corrections noted in it.
10
+ >
11
+ > The ledger is still the place to start for what was wrong and how it was found. It is no longer a
12
+ > list of open defects.
13
+
14
+ **Status: built.** This was the plan for what
15
+ [VIRTUAL_GEOMETRY_DESIGN.md](../VIRTUAL_GEOMETRY_DESIGN.md) §7 describes — the selection buffer, the
16
+ compute cut pass, the VRAM page pool, the wants/touches loop, and the seam through which a VG
17
+ instance enters the meshlet pipeline — and it is kept as written plus §8, the adversarial review,
18
+ because the review is where most of the design actually came from.
19
+
20
+ What it describes now exists: `cut/` is the kernels, `layout/` the record formats,
21
+ `VGeoRuntimeTables` the layout with no device in it, `GPUVirtualGeometryPool` the VRAM tier,
22
+ `VGeoFeedbackLoop` the readback, and `graph_vgeo_expand_instances_to_meshlets` the seam. The cut is
23
+ held to `vgeo_select_cut` exactly, and `playground/vgeo_runtime/` runs it on a device.
24
+
25
+ Everything below the runtime was already built when this was written. The builder produces a DAG,
26
+ the container holds it, the reader streams it a page at a time with residency and activation,
27
+ `VGeoResidencyManager` owns the RAM budget, and `playground/vgeo_viewer/select_cut.js` selects a
28
+ correct cut on the CPU against a partially resident container. What had never run was any of it on
29
+ the GPU, and the one piece of M0 that is engine-facing — the expansion seam — had never been built
30
+ at all.
31
+
32
+ ## 0. The shape of the answer, in one paragraph
33
+
34
+ A cluster is drawn iff `project(parent) > τ AND (project(self) ≤ τ OR child group not selectable)`.
35
+ That is **a pure function of one cluster record**. Design §4 pins it, and every hard problem in a
36
+ GPU cut comes from noticing which half of that sentence you are leaning on. Lean on the
37
+ *traversal* (descend the DAG, stop when fine enough) and you must dedup visits, because the DAG is
38
+ many-parented and a group reached twice is drawn twice. Lean on the *predicate* (evaluate it
39
+ everywhere) and you need no dedup at all, but you pay the resident set every frame, which is what
40
+ §4 rejects. The runtime does both, in the only combination that has neither cost:
41
+
42
+ > **Traverse the hierarchy to decide which clusters to look at; evaluate the predicate to decide
43
+ > whether to draw them.**
44
+
45
+ Visiting a cluster twice then costs a duplicate *record*, so the traversal dedups at **page**
46
+ granularity only — one bit per (instance, resident page), which is affordable — and never at
47
+ cluster or group granularity, which would not be. Visiting a cluster the traversal "should not"
48
+ have reached costs nothing but the test, because the predicate is local and gives the same answer
49
+ from every direction. §4 below proves the two halves that matter: no cluster is drawn twice, and
50
+ no cluster the flat rule would draw is missed.
51
+
52
+ ## 1. What is already there, and what each piece is worth
53
+
54
+ | piece | state | what the runtime does with it |
55
+ |---|---|---|
56
+ | `format/read/VGeoPage` | reads the page header, group table, cluster table | extended (§2) — `parent_sphere`, `aabb`, `payload_size`, the node table and every metric are in the file and unread |
57
+ | page node tree (format §6.1) | written, validated, **no reader** | the sub-page cull; plan §2.3's open item half-closes here |
58
+ | child-table traversal metric (format §5, §6.2) | written, validated, **no reader** | **still no reader, and the review below says why** (§4.8) — it is a prefetch structure, not a cut structure |
59
+ | `VGeoContainerReader` | residency, activation, quarantine, cancel | the RAM tier; the runtime never fetches, it *wants* |
60
+ | `VGeoResidencyManager` | byte budget, touch-LRU, shared fetch gate | the runtime hands it wants and touches and nothing else |
61
+ | `GPUMeshletManager` | data + metadata pools, `add_batch`/`remove_batch` | the VRAM tier's storage; format §8 makes a page payload a **copy** into the data pool |
62
+ | `MESHLET_DEFINITION_STRUCT` | `(meshlet index, mesh index)` | the cut's output record type — unchanged, which is what makes the seam one call |
63
+ | `graph_expand_meshes_to_meshlets` | four call sites, three of them opaque | the seam: a VG expansion runs beside each opaque one and appends into the same collection (§5) |
64
+ | `playground/vgeo_viewer/select_cut.js` | the rule on the CPU | **the oracle**. The GPU cut is held to agreeing with it exactly |
65
+
66
+ ## 2. Phase 1 — the page decode has holes, and they are load-bearing
67
+
68
+ `vgeo_read_page` reads two thirds of a cluster record and none of the node table. Everything it
69
+ skips is something the GPU runtime needs:
70
+
71
+ - `parent_sphere` (offset 24) and `parent_error` — `parent_error` is read, `parent_sphere` is not.
72
+ Half the cut rule is `project(parent_sphere, parent_error) > τ`. The CPU traversal never needed
73
+ it because arriving through a parent *is* that test; a predicate evaluated per cluster needs the
74
+ sphere.
75
+ - `aabb` (offset 56) — this is the meshlet metadata's `bounds_box`, which the HZB filter and the
76
+ small-primitive cull read. Without it a VG meshlet cannot be culled.
77
+ - `payload_size` (offset 96) — the number of bytes to copy into the data pool.
78
+ - the **node table** (§6.1) and the page's own **metric** (§6) — the sub-page hierarchy. The page's
79
+ own metric arrives for free: format §11.10 makes it equal to node 0's, and the traversal always
80
+ starts a page at node 0.
81
+ - the **child-table metrics** (§5) are read too, because a reader that decodes two thirds of a
82
+ structure is worse than one that decodes all of it — but see §4.8: the cut does not use them.
83
+
84
+ Phase 1 is a pure addition to `VGeoPage` and `vgeo_read_page`, with the existing reader spec
85
+ extended to hold the new columns against the bytes the writer emitted. Nothing that reads the page
86
+ today changes.
87
+
88
+ **Cost check — measured, 2026-09-12.** Per resident page this adds `4·4 + 6·4 + 4` bytes per cluster
89
+ and `32` per node of CPU-side typed arrays. The gate this paragraph set was never discharged, so
90
+ here it is: summing every typed array a decoded `VGeoPage` holds that is not a view over the page's
91
+ own bytes, on a 240×120 torus,
92
+
93
+ | page size | pages | overhead per page | mean | of the decoded bytes |
94
+ |---:|---:|---:|---:|---:|
95
+ | 64 KB | 44 | 0.2–4.1 KB | 2.9 KB | 6.1% |
96
+ | 128 KB | 26 | 0.2–8.4 KB | 4.8 KB | 6.0% |
97
+ | 544 KB | 13 | 0.2–33.9 KB | 9.4 KB | 5.9% |
98
+
99
+ It is a **fraction** — six per cent, flat across page size — rather than a per-page constant, which
100
+ is the more useful shape to know: the overhead is per cluster and per node, and a page's cluster
101
+ count is what its size buys. The residency audit's "6.4–11.9 KB against a 544 KB page" is the mean
102
+ of the same distribution and not its range; the widest page here is 33.9 KB, and a document quoting
103
+ the narrow band as a bound would be wrong by 2.8× at the top.
104
+
105
+ ## 3. Phase 2 — the VRAM tier
106
+
107
+ ### 3.1 What a resident page costs on the GPU
108
+
109
+ One `GPUMeshletManager.add_batch` (payload bytes → data pool, synthesized 40-byte metadata rows →
110
+ metadata pool) **plus one fixed-stride slot** of the arena:
111
+
112
+ ```
113
+ [ page header ][ node rows ][ group rows ][ cluster rows ]
114
+ ```
115
+
116
+ A page's slot is at `page_row × slot_words`. There is no allocator, no growth path, no ceiling, and
117
+ no page table: the header is the first eight words of the page's own slot, so the traversal reads it
118
+ out of a buffer it already binds.
119
+
120
+ **This is design §6's "fixed-size page-slot allocation", and it took a format change to be
121
+ available.** The first draft here used a variable `OffsetAllocator`, on the argument that pages
122
+ carry whole groups and a group may be oversized, so a hard slot size would refuse a legal file —
123
+ format §11.5's escape. §11.14 closed that: `nominal_page_size` is a bound every page is within,
124
+ announced by a feature bit, and the builder refuses a page size no group fits rather than writing a
125
+ page over it. `GPUVirtualGeometryPool.register` refuses a container that does not carry the
126
+ guarantee or carries it at a different size than the pool holds.
127
+
128
+ **The stride is a choice, and the numbers behind it are worth carrying.** The provable worst case —
129
+ a page of many minimal clusters — puts the runtime rows at about half the page size, which would
130
+ over-provision the arena by fifteen times. The measured maximum, over a 160×80 torus built at 16,
131
+ 32, 64 and 128 KB pages, is **3.9%**. `vgeo_runtime_page_slot_words` takes an eighth, and an install
132
+ that does not fit is refused and counted rather than overrunning its slot — the same direction
133
+ `register` takes for a container built at the wrong page size, and for the same reason.
134
+
135
+ The page header, in its slot:
136
+
137
+ | field | why |
138
+ |---|---|
139
+ | `node_count`, `group_count`, `cluster_count` | sub-table extents; each sub-table is a count away from the one before it |
140
+ | `meshlet_base` | the metadata-pool row of this page's cluster 0 — `meshlet_index = meshlet_base + local`. The one field that moves without the page moving: the shared meshlet pool relocates batches when it compacts, and the pool follows its `changed` signal |
141
+ | `geometry_row`, `ordinal` | the claim index (§4.3) and the page-lookup base |
142
+ | `page_index` | the container's global page index — what a touch reports |
143
+ | `decoded_size` | what this page cost the tier, so an eviction refunds what the install charged rather than re-deriving it from a RAM tier that may have dropped the page |
144
+
145
+ The group row carries its `parent_count` for the same reason: **this tier answers its own activation
146
+ question**. Selectability is "every parent reference is live", and which references are live depends
147
+ on which pages are resident *in the tier asking* — so the RAM tier's answer is not this one's, and
148
+ publishing it here is what drew a group under a coarse stand-in on the ordinary streaming path.
149
+
150
+ And a per-geometry **page lookup**: `total_pages` words mapping a container page index to a page
151
+ table row, or `VGEO_PAGE_ROW_NONE`. 80 KB for a 20,000-page asset, one word written per install
152
+ and per evict, and it is what removes every scatter from the install path: nothing else in the
153
+ runtime has to be patched when a page arrives.
154
+
155
+ > **Why a lookup and not a pointer.** The obvious alternative is to store the child page's *row*
156
+ > in each referring cluster row. That makes an install a scatter over every cluster that refers
157
+ > into the arriving page — across pages, unbounded, and re-done on every eviction. One indirection
158
+ > in the shader is cheaper than that every time.
159
+
160
+ ### 3.2 Selectability
161
+
162
+ The cut rule's clamp reads *selectable*, not *resident*, and `VGeoContainerReader.is_selectable`
163
+ already computes it. The group row carries the bit; after an install or evict the reader's
164
+ activation cascade is reflected into the group rows of the affected reader. The CPU keeps the
165
+ bits in a shadow `Uint32Array` and uploads the dirty span — a page has a handful of groups, and a
166
+ thousand resident pages is a few kilobytes.
167
+
168
+ ### 3.3 Budgets and eviction
169
+
170
+ `GPUVirtualGeometryPool` holds a VRAM byte budget, a touch stamp per page row, and evicts on
171
+ design §6's rule — LRU on last-touched under a protect window no shorter than feedback latency,
172
+ and never a page with resident children *in this tier*. The tier's child check is the pool's own,
173
+ not the reader's: VRAM residency is a subset of RAM residency, so `resident_child_count` answers a
174
+ different question.
175
+
176
+ Uploads are budgeted per frame in bytes. The root page is pinned.
177
+
178
+ **The binding limit is `maxStorageBufferBindingSize`, not `maxBufferSize`** — design §6 says so and
179
+ names `GPUMeshletManager` as clamping against the wrong one. The VG arena clamps against the
180
+ binding limit, and the failure policy is a refused install with telemetry, never a silently
181
+ invalid buffer.
182
+
183
+ ## 4. Phase 3 — the cut pass
184
+
185
+ ### 4.1 Shape
186
+
187
+ A **wavefront BFS over node visits**, ping-ponged through two queues with an indirect dispatch per
188
+ iteration. Not persistent threads: WebGPU gives no forward-progress guarantee across workgroups,
189
+ and an MPMC queue serviced by persistent threads is unbounded cross-workgroup waiting.
190
+
191
+ A queue item is `(instance_slot, page_row, node_index)` — one lane each.
192
+
193
+ ```
194
+ seed: one item per visible VG instance = (instance, root page row, node 0)
195
+ iteration: node test → interior: enqueue children
196
+ → leaf: for each group, for each cluster: the predicate
197
+ ```
198
+
199
+ Enqueueing a *child page* means enqueueing `(instance, child_page_row, 0)` — its node tree's root.
200
+ Uniform items, one kernel, no special cases.
201
+
202
+ ### 4.2 What one node visit does
203
+
204
+ 1. Read the node metric — sphere and `max_parent_error` — in object space, against the instance's
205
+ object-space eye and frustum (design §3.1: one matrix per instance, never one per cluster).
206
+
207
+ *Object space costs one correction.* Error and distance are both object-space, so a **uniform**
208
+ instance scale cancels out of `r_e · k / d` exactly and the projection needs no scale term at
209
+ all. A non-uniform one does not cancel, and the runtime inflates `r_e` by
210
+ `max axis scale / min axis scale` — conservative, so it over-refines rather than under-refines,
211
+ and it is exactly 1 for every instance that is not stretched.
212
+ 2. **Frustum-cull** the sphere. A node's sphere contains every `parent_sphere` under it, and
213
+ monotonicity makes `parent_sphere ⊇ self_sphere ⊇ the cluster's vertices`, so a node outside
214
+ the frustum discards a region.
215
+ 3. **LOD-cull**: if `project(sphere, max_parent_error) ≤ τ`, nothing under this node can satisfy
216
+ `project(parent) > τ`, so the entire subtree is skippable (format §6.2 — this is the field's
217
+ whole purpose).
218
+ 4. Interior → enqueue the `child_count` children. Leaf → enqueue a **group-chunk item** per group
219
+ per `VGEO_CUT_CLUSTER_CHUNK` clusters (§4.6).
220
+ 5. A group-chunk item first asks whether **its own group is selectable**, and stops if it is not.
221
+ This is not decoration — see §4.4, where leaving it out double-draws.
222
+ 6. Per cluster, the predicate:
223
+ - `project(parent_sphere, parent_error) ≤ τ` → not drawn, and no descent.
224
+ - `project(self_sphere, self_error) ≤ τ`, or `child_ref == NO_CHILD` → **draw**.
225
+ - resolve the child page: `SAME_PAGE` → this row; otherwise
226
+ `lookup[geometry.lookup_base + child_page_index]`.
227
+ - not resident → **draw** (clamp) and **want**.
228
+ - resident but the child group not selectable → **draw** (clamp), no want: there is nothing to
229
+ fetch, the group is waiting on a page holding another of its parents, and some other cluster
230
+ in this same cut is clamped directly against that page.
231
+ - otherwise → do not draw; claim the child page and enqueue its node 0.
232
+
233
+ ### 4.3 Dedup, and why page granularity is enough
234
+
235
+ The page DAG is many-parented — 82 of 91 pages on a 98k sphere have more than one parent — so a
236
+ descent that did not dedup would enqueue a page once per parent and draw its whole cut several
237
+ times over. The claim is **one bit per (instance, resident page)**, set with `atomicOr` at
238
+ enqueue, indexed `claim_base[instance] + page_row.ordinal`, where `ordinal` is the page's dense
239
+ index *within its geometry* and `claim_base` comes from a prefix scan over visible VG instances of
240
+ their geometry's ordinal capacity. `graph_prefix_scan_csdldf` already does this scan for
241
+ mesh→meshlet expansion; this is the same pattern.
242
+
243
+ Cluster-granular dedup is what this avoids, and it is worth being explicit about why it would not
244
+ do: the resident cluster count is the number the design is trying not to pay per frame.
245
+
246
+ ### 4.4 The correctness proofs, and the hole the first draft had
247
+
248
+ Everything above only works if three things hold. All three go in the kernel's own doc comment,
249
+ because they are the reason its shape is what it is.
250
+
251
+ **The predicate is not the one design §4 writes down.** §4's per-cluster form is
252
+
253
+ > `project(parent) > τ AND (project(self) ≤ τ OR child group non-selectable)`
254
+
255
+ and evaluating exactly that, per cluster, **double-draws**. Take a cluster `C` whose own group `G`
256
+ is resident but not selectable — its clusters' parents are split across pages and one of those
257
+ pages has not arrived. The clamp draws `C`'s parent `P`, correctly, standing in over that surface.
258
+ But `C` itself satisfies §4's form — `project(C.parent) > τ` and `project(C.self) ≤ τ` — so a
259
+ flat evaluation draws it as well, underneath `P`, and the depth buffer picks between two surfaces
260
+ a few microns apart. This is exactly the failure `select_cut.spec.js` was written to catch, and
261
+ the CPU traversal never meets it because *not entering a non-selectable group* is how it descends.
262
+ A predicate has to say so out loud:
263
+
264
+ > `own group selectable AND project(parent) > τ AND (project(self) ≤ τ OR child group
265
+ > non-selectable)`
266
+
267
+ Design §6 states the fact — "resident-but-inactive groups draw nothing" — without stating it as a
268
+ term of the rule, because §4 was written for a traversal. The runtime evaluates the rule, so the
269
+ runtime carries the term. This is the single most important line in the kernel.
270
+
271
+ **No cluster is drawn twice.** A cluster is evaluated exactly once per (instance, page visit), and
272
+ a page is visited at most once per instance by the claim. It is drawn only from its own page.
273
+ Therefore at most one record per (instance, cluster). ∎
274
+
275
+ **No cluster the flat rule would draw is missed.** Let `C` satisfy `project(C.parent) > τ`. Take
276
+ any parent cluster `P` of `C`'s group. Every cluster of a group shares the consuming step's sphere
277
+ and error, so `project(P.self) = project(C.parent) > τ`. Monotonicity gives
278
+ `project(P.parent) ≥ project(P.self) > τ`, so `P` satisfies the predicate's first half, and by
279
+ induction on the level (the root page is always visited) `P`'s page is visited and `P` is
280
+ evaluated. `P` is not drawn — it fails `project(self) ≤ τ` — so it either enqueues `C`'s page or
281
+ clamps, and it clamps only when `C`'s group is not selectable, in which case `C` must not be drawn.
282
+ Neither cull can intervene: a node's sphere contains `P`'s `parent_sphere` and its
283
+ `max_parent_error` is at least `P.parent_error`, so `project(node) ≥ project(P.parent) > τ` and
284
+ the LOD cull does not fire; the frustum cull fires only when `C` is off-screen too. ∎
285
+
286
+ The second proof is why *over*-visiting is harmless and *under*-visiting is not, which is the
287
+ asymmetry the whole design leans on.
288
+
289
+ One qualifier on the frustum cull, since the proof waves at it: a culled node can leave a genuine
290
+ hole, because the parent that would have stood in is not drawn either (it fails
291
+ `project(self) ≤ τ`) and the child is never reached. Everything involved is inside the culled
292
+ sphere, so the hole is in geometry the frustum rejected. The agreement spec (§6) therefore runs
293
+ with culling **off**, which is also how `vgeo_select_cut` defaults, and the culls are held against
294
+ a separate claim: turning them on removes only clusters whose `self_sphere` is outside the
295
+ frustum.
296
+
297
+ ### 4.5 Outputs
298
+
299
+ - **Draw records** — `MESHLET_DEFINITION_STRUCT(meshlet_base + local, mesh_index)`, appended by
300
+ `atomicAdd` into the same meshlet collection the static expansion writes. Bounds-checked; an
301
+ overflow increments a stat rather than corrupting the count.
302
+ - **Wants** — `(source_page_row, child_ref, child_page_index, priority)`, deduped by an atomic frame
303
+ stamp indexed the same way as the page lookup. Naming the page the want was *discovered through*
304
+ as well as the page wanted is what **unblocks** format §10's in-flight-pin item: §10 had it open
305
+ for want of anybody able to name the parent, because the format stores parent counts rather than
306
+ parent references. The record names it. Nothing pins anything yet, and §10 is still open — this
307
+ document claimed it closed, which it is not (REVIEW_LEDGER D4). Priority is the clamped cluster's
308
+ projected error, taken from the first writer — documented as a v1 heuristic; the residency manager
309
+ maxes across frames.
310
+ - **Touches** — page rows, deduped by an atomic frame stamp per page row.
311
+ - **Stats** — the fourteen of `VGEO_CUT_STAT`. These are what `M1`'s "selection cost measured" means
312
+ and what the playground asserts on.
313
+
314
+ Every append is bounded, and every bound degrades in the same direction: **coarser or missing, never
315
+ doubled**. A meshlet collection that fills drops records and counts them; a claim allocation that
316
+ does not fit drops the *instance* rather than running it unclaimed, because an unclaimed instance
317
+ is the one failure that draws twice. τ is the dial that makes the first case go away and the
318
+ scene's VG instance count is the dial for the second.
319
+
320
+ **The one exception, and it is now visible.** A queue item that does not fit, or a descent that runs
321
+ out of iterations with work still queued, is a subtree never descended — and the parent that would
322
+ have stood in over it was not drawn either, because it failed `project(self) ≤ τ`. That is a hole,
323
+ not a coarser surface, and it was the one degradation nothing counted: §4.7 below promised the
324
+ kernel recorded it and no such counter existed (REVIEW_LEDGER M8, D1). `QUEUE_DROPPED` is the items
325
+ lost at the append; `QUEUE_REMAINING` is what was outstanding when the descent stopped, left behind
326
+ by one extra prepare pass the seam records after the wavefront loop for exactly this; and
327
+ `WAVEFRONTS` is how deep it went, so reaching `max_iterations` is legible rather than inferred.
328
+
329
+ ### 4.6 Group chunks, and the serial loop that is not allowed to be unbounded
330
+
331
+ A node leaf covers at most 4 groups, and the obvious kernel walks their clusters in the lane that
332
+ popped the leaf. A group may hold up to 4096 clusters (format §11.5), so that lane can be handed
333
+ 16,384 clusters — legal input, and a loop nothing bounds. So a leaf does not walk clusters at all:
334
+ it enqueues one **group-chunk item** per group per `VGEO_CUT_CLUSTER_CHUNK` clusters, all through
335
+ a single `atomicAdd`. A typical group is 4–32 clusters and produces exactly one, so the common
336
+ case pays one extra queue item and gains a factor of four in width at the bottom of the descent;
337
+ the pathological case is bounded instead of unbounded.
338
+
339
+ An item is therefore `(instance, page_row, target, first_cluster)`, with bit 31 of `target`
340
+ distinguishing a node index from a group index. One kernel, one queue, two shapes.
341
+
342
+ ### 4.7 Iteration bound
343
+
344
+ Depth along a path is page depth × (node depth + one group step) — 12 and 1–3 measured, so about
345
+ 48. The kernel runs a configurable `max_iterations` (default 64) of indirect dispatch. A dispatch of
346
+ an empty queue is an indirect dispatch of zero workgroups.
347
+
348
+ "Records whether the queue was non-empty at the end" is what this paragraph promised and what it did
349
+ not do: nothing anywhere recorded it, which made the descent's one hole-shaped failure the only one
350
+ with no reading (REVIEW_LEDGER M8). `VGEO_CUT_STAT.QUEUE_REMAINING` is that number now, left behind
351
+ by one extra prepare pass after the loop, and `WAVEFRONTS` says how many iterations actually ran.
352
+ Measured on the playground's 13-page torus through a real adapter: fourteen wavefronts at τ = 0.5,
353
+ eight at τ = 16, and nothing outstanding at any threshold.
354
+
355
+ ### 4.8 What the review took out: the child-table metric is not a cut structure
356
+
357
+ The plan's first draft claimed the child-table metric copy as the cut's pre-fetch cull, which is
358
+ what plan §2.3 and format §6.2 both predict. Held against the kernel above, it buys **nothing**,
359
+ and the reason is worth writing down so nobody adds it back:
360
+
361
+ A want is emitted from a cluster that has already passed `project(parent) > τ` and
362
+ `project(self) > τ`. The child page's metric covers that cluster's child group among others, so
363
+ its `max_parent_error` is at least that group's `parent_error` and its sphere contains that
364
+ group's `parent_sphere` — and the projection is monotone in both. So `project(child page metric)`
365
+ is **always** greater than τ at exactly the moment the question is asked. The page-level test
366
+ cannot reject what a per-cluster test has just accepted, because the per-cluster test is strictly
367
+ tighter. The same argument kills the frustum half: the cluster's own `self_sphere` is a tighter
368
+ frustum test than the child page's union sphere, available locally, for free.
369
+
370
+ Where the metric copy *is* load-bearing is **prefetch** — walking a resident page's child table to
371
+ rank pages nobody has asked for yet, which by definition has no cluster-level evidence to be
372
+ tighter than. That is plan §3's territory and not this campaign's. `min_self_error` is in the same
373
+ position: it ranks a want, and the cut never asks it anything.
374
+
375
+ So plan §2.3 half-closes. The node tree gets its consumer; the child-table metric keeps waiting,
376
+ for a named reason rather than for want of anyone to build it.
377
+
378
+ ### 4.9 Binding budget
379
+
380
+ The engine requires a device to offer 10 storage buffers per stage and refuses one with fewer, so
381
+ a kernel that declares more cannot run anywhere. The traversal declares **eight**:
382
+
383
+ `vg_lookup`, `vg_arena`, `instances`, `queue_in`, `queue_out`, `claim`, `feedback`, `out_meshlets`
384
+ — plus one uniform.
385
+
386
+ This list was wrong in two directions when it was written, which is why
387
+ `vgeo_cut_shaders.compile.spec.js` now asserts the exact count of every kernel rather than only the
388
+ bound. It named `stamps`, which is not a buffer — the touch and want stamps are *regions of*
389
+ `feedback`, which is what makes them per-frame and shared across every view and bucket — and it
390
+ omitted `vg_lookup`, which the descent is gated on. And `vg_pages` is gone: a page's header is the
391
+ first words of its own arena slot (§3.1), so the table that used to hold it is not a binding any
392
+ more. That is the headroom this kernel did not otherwise have.
393
+
394
+ It remains the reason the node/group/cluster tables share one arena rather than being three buffers,
395
+ which is worth recording because "a table each" is the obvious first draft.
396
+
397
+ `claim` and `stamps` are separate because their lifetimes are: the claim is per *invocation* and
398
+ cleared, since each expansion site sees a different instance set; the touch and want stamps are
399
+ per *frame* and shared across every view and bucket, so they are a persistent buffer compared
400
+ against a monotonic frame counter and never cleared at all. Merging them would either clear the
401
+ stamps (duplicating every touch and want across buckets) or stop clearing the claim (drawing every
402
+ VG instance's cut once per bucket).
403
+
404
+ ## 5. Phase 4 — the seam
405
+
406
+ ```js
407
+ const gr_bucket_meshlets = graph_expand_meshes_to_meshlets({ graph, input_meshes, ... });
408
+
409
+ graph_vgeo_expand_instances_to_meshlets({
410
+ graph, input_meshes, output_meshlets: gr_bucket_meshlets, view, vg, limits
411
+ });
412
+ ```
413
+
414
+ One call beside each of the **three opaque** expansion sites, appending into the same collection.
415
+ Everything downstream — the HZB meshlet filter, the material bucket, the indirect draw, the
416
+ G-buffer — is untouched, because the record type and the metadata pool are the ones it already
417
+ reads.
418
+
419
+ Three, not the four `graph_expand_meshes_to_meshlets` has (REVIEW_LEDGER D3). The fourth is
420
+ `prepare_meshlet_draw_commands_by_material`, which serves the transparent and alpha-tested buckets
421
+ and is handed a scene and an optional camera *resource* rather than a view context — where a cut
422
+ needs the camera's uniform buffer to put each instance into object space. DESIGN §9 scopes non-opaque
423
+ VG instances out of v1, and `GPUSceneRows.attach_mesh` says so out loud when one is attached, because
424
+ a VG instance in that path expands to zero meshlets and draws nothing at all.
425
+
426
+ And one thing the seam has to refuse: a rasterization path recording on a **different**
427
+ `FrameGraph`. `GPUSceneShadowmapContext` reaches `graph_rasterize_scene_fast_raw`, which carries the
428
+ seam, on a graph of its own — so a cube face inside an open frame would run a whole cut at the main
429
+ camera's projection scale into a resource handle belonging to a graph that has never heard of it.
430
+ `graph_vgeo_expand_if_enabled` checks the graph against the one the frame was opened on and skips.
431
+
432
+ Three things make that a one-line seam rather than a new pipeline:
433
+
434
+ - **The instance list is already the right one.** Each expansion site is handed a set of visible
435
+ mesh instances for one bucket and one HZB pass. Those sets are disjoint (the two-way filter
436
+ splits positive from maybe), so running the cut over each of them costs one cut per view in
437
+ total, and a VG instance cannot be cut twice.
438
+ - **VG meshes expand to zero static meshlets.** `geometry.meshlets_count` is 0 for a VG geometry,
439
+ so `shader_meshes_to_meshlet_counts` already contributes nothing for them.
440
+ - **`GEOMETRY_METADATA_STRUCT` gains one field**, `vg_row`, `0xFFFFFFFF` for everything that is not
441
+ virtual. That is how a shader tells a VG instance from a static one.
442
+
443
+ ## 6. Phase 5 — proof
444
+
445
+ Four tiers, in the order they catch things.
446
+
447
+ 1. **WGSL parse + validate + binding budget**, as every shader module in this tree has.
448
+ 2. **The agreement spec — the one that matters.** Build a torus with `vgeo_build`, install it into
449
+ a real `VGeoContainerReader`, mirror it into a CPU model of the arena, and run the cut kernels
450
+ under `ComputeShaderEmulator`. Assert the **drawn cluster multiset is exactly** what
451
+ `vgeo_select_cut` returns, over a sweep of camera positions and τ, both fully resident and with
452
+ pages withheld. Multiset, not set: a duplicate is the failure mode that reads as corrupt
453
+ geometry rather than as a missing page, and a set comparison cannot see it.
454
+
455
+ **At 64 KB pages, not the 8 KB `select_cut.spec.js` uses** (REVIEW_LEDGER D2). The 8 KB fixture
456
+ is the one that puts a group's parents on either side of a page boundary thirteen times over,
457
+ which is what *that* spec is for — and it is the one fixture in which this spec's headline case
458
+ cannot occur: measured, 8 KB puts exactly one group in every page of this torus, so a blocked
459
+ group is a blocked page, no page is ever mixed, and the cut's own-group term is unreachable.
460
+ At 64 KB the median page holds eight groups and the state is ordinary.
461
+
462
+ What the spec withholds matters as much as what it builds. Withholding from the **reader** makes
463
+ a page missing from both tiers, which is the easy case; withholding from the **tables** alone
464
+ makes VRAM a strict subset of RAM, which is the ordinary streaming state and the one every test
465
+ here used to skip. See C1.
466
+ 3. **`SoftwareGPUDevice`** for the pool's orchestration — install, evict, budget, page-row reuse
467
+ and the meshlet manager's allocations — where no shader has to run. Not the arena's growth:
468
+ there is none, and has not been since the fixed-stride conversion (REVIEW_LEDGER C3). The
469
+ same tier is what holds the **seam**, which records passes rather than running them:
470
+ `graph_vgeo_expand.spec.js` compiles the cut onto a real `FrameGraph` and asserts what came out
471
+ of the recording, and `VGeoFeedbackLoop.spec.js` runs a readback that really lands.
472
+ 4. **The playground**, `playground/vgeo_runtime/`, drawing a streaming container through the real
473
+ meshlet pipeline with a `__state()` a headless Chrome session can assert on: drawn triangles
474
+ track τ and resolution and not source triangle count; clamped falls to zero as pages arrive;
475
+ the same τ sweep is watertight.
476
+
477
+ ## 7. Order, and what each step leaves green
478
+
479
+ | step | landed | proved by |
480
+ |---|---|---|
481
+ | 1 | page decode: metrics, node table, `parent_sphere`, `aabb`, `payload_size` | `vgeo_read_page.spec.js`, against the writer's bytes and the payload's own positions |
482
+ | 2 | record structs + the arena + the pool | `GPUVirtualGeometryPool.spec.js` on a `SoftwareGPUDevice` — whole-buffer comparison against the tables, since a dirty range that was not marked leaves a page pointing at another page's clusters |
483
+ | 3 | the cut kernels | `vgeo_cut_agreement.spec.js`, against `vgeo_select_cut`, multisets, seven cameras × four thresholds, resident, RAM-withheld and **VRAM-withheld**, one geometry and two, culling on and off, and the collect pass's own record rather than a fabricated one |
484
+ | 4 | the seam + `vg_row` + `VirtualGeometry` | `graph_vgeo_expand.spec.js`, on what the seam records onto a real `FrameGraph`: the passes, the resource each is bound to, the feedback version it publishes, and a graph that is not the frame's getting nothing. It says three opaque call sites — and **nothing else**; see below |
485
+ | 5 | feedback readback → residency | `VGeoFeedbackLoop.spec.js` on a real frame graph, plus the playground: 13 pages, one resident, streamed to 9 by the cut's own wants |
486
+ | 6 | playground + headless drive | an nvidia/lovelace adapter; the τ sweep in `playground/vgeo_runtime/README.md` |
487
+
488
+ **Step 4's row said "4,667 engine specs green", and that was not evidence** (REVIEW_LEDGER P7). Not
489
+ one of those specs executed a line of the seam: they were green before it existed and green after,
490
+ which is a statement about the seam not breaking anything rather than about the seam working. A row
491
+ of a proof table has to name a test that would go red.
492
+
493
+ Replacing it with "step 6, the headless drive" was closer and still not enough. The drive is a
494
+ whole-system check on one adapter and it cannot be run per commit; a seam defect that the drive's
495
+ six numbers happen not to move — the tail pass bound to the wrong queue at an odd `max_iterations`,
496
+ say — is invisible to it. The row above names `graph_vgeo_expand.spec.js`, which is the tier that
497
+ can go red on one line of the seam, and the drive stays what it is: the only tier that has ever
498
+ noticed the playground failing to boot at all (REVIEW_LEDGER, re-review 2026-09-12).
499
+
500
+ One thing that step 6 found and no earlier tier could: the feedback resource is **versioned**, and a
501
+ second expansion site that wrote version 0 again after the first had produced version 65 is reading
502
+ stale contents while claiming not to. The FrameGraph refuses to compile a graph that does it, which
503
+ is why `GPUVirtualGeometryRuntime.publish_feedback` exists at all.
504
+
505
+ ## 8. What the adversarial review changed
506
+
507
+ Kept here rather than silently folded in, because three of these are things a reader would
508
+ otherwise re-derive — and the first one is a bug the plan shipped with.
509
+
510
+ 1. **The predicate double-draws as design §4 writes it.** A resident-but-not-selectable group's
511
+ clusters satisfy it while their coarse stand-in is also drawn. The rule needs an *own group
512
+ selectable* term that a traversal gets for free and an evaluator does not. §4.4.
513
+ 2. **The child-table metric is not a cut structure.** The per-cluster test is strictly tighter at
514
+ the moment the want is emitted, so the page-level test can never reject anything. Its consumer
515
+ is prefetch. §4.8. Plan §2.3 predicted the cut would read both accelerators; it reads one.
516
+ 3. **A node leaf's cluster loop was unbounded** — 4 groups × 4096 clusters in one lane, on legal
517
+ input. Group-chunk queue items bound it and widen the common case. §4.6.
518
+ 4. **`claim` and `stamps` cannot share a buffer**, because one is per-invocation-and-cleared and
519
+ the other is per-frame-and-not. Merging them either duplicates every want across buckets or
520
+ draws every instance once per bucket. §4.9.
521
+ 5. **Object space needs a scale correction** for non-uniform instance scale, and *only* for
522
+ non-uniform: the uniform case cancels exactly, which is worth knowing before adding a term that
523
+ does nothing. §4.2.
524
+ 6. **Overflow must degrade in one direction.** A full meshlet collection drops records; a claim
525
+ allocation that does not fit drops the instance rather than running it unclaimed. §4.5.
526
+ 7. **A frustum cull can leave a real hole**, in geometry the frustum rejected. Named, and the
527
+ agreement spec runs with culling off for that reason rather than by accident. §4.4. What the
528
+ review did not say is that culling is **on** by default and therefore had no oracle at all, which
529
+ is how a near plane extracted with the wrong sign shipped (REVIEW_LEDGER M1, M2). It has one now:
530
+ turning culling on removes only clusters whose own sphere is outside the frustum.
531
+
532
+ ### 8.1 What §8 got wrong, and the sweep that found it
533
+
534
+ The adversarial review above is the author's own, and a second one on 2026-09-11 — twelve reviewers
535
+ over disjoint dimensions, every finding attacked by three verifiers — contradicted it in three
536
+ places and found ~35 distinct defects it had not. [REVIEW_LEDGER.md](./REVIEW_LEDGER.md) is that
537
+ sweep, what it cost to close, and what it itself got wrong.
538
+
539
+ Three of its findings are worth carrying here rather than only there, because they are about the
540
+ shape of the thing rather than about a line of it:
541
+
542
+ - **A tier has to answer its own residency questions.** Selectability, the bytes an eviction
543
+ refunds, and eviction's child rule were all taken from the RAM tier by the VRAM tier, and each of
544
+ them is a different answer over a different set. The VRAM tier holds what it needs to answer them
545
+ now (§3.1).
546
+ - **A readback is about a frame that has gone.** The layout it is decoded with, and the identity of
547
+ every page row it names, belong to that frame and not to this one. Both are snapshotted when the
548
+ reading is scheduled.
549
+ - **Proof tiers are not interchangeable.** Nothing below the headless drive exercises the
550
+ engine-integration path, and the suite being green says nothing about it. §6's fourth tier is a
551
+ required check.
552
+
553
+ ## 9. What this does **not** do, deliberately
554
+
555
+ - **No proxy geometry, no shadow-view cuts** (plan §3.2, §3.3). One cut per view is what §4 pins;
556
+ a view that does not justify a cut taking a proxy instead is the next campaign, not this one. The
557
+ seam enforces it rather than assuming it: a rasterization path recording on a `FrameGraph` other
558
+ than the one the frame was opened on is skipped (§5).
559
+ - **No position quantization** (plan §4.2). It is a `payload_encoding` id and a decode-shader
560
+ change, and it is orthogonal to every line here.
561
+ - **No GPU-driven allocation** (plan §3.4). The arena is CPU-allocated, one fixed-stride slot per
562
+ page, which is the shape that makes a device-side allocator a later swap rather than a rewrite —
563
+ and slots make it a smaller swap than variable blocks would have.
564
+ - **No slot-size classes.** One page size per pool, checked at `register` and refused otherwise
565
+ (§3.1). A tier that reconciled two page sizes would need a table of slot sizes, and that is a
566
+ later change than this one.
567
+ - **No non-opaque VG instances** (design §9). The cut runs beside the opaque expansions only, and a
568
+ VG instance with a transparent or alpha-tested material draws nothing — said out loud at
569
+ attachment rather than discovered on screen.
570
+ - **No compaction of `GPUMeshletManager` under VG churn.** Growth appends and moves nothing;
571
+ compaction is still a CPU-side rebuild and still has the unhandled address-migration TODO
572
+ downstream. VG pages are near-uniform in size, so the holes an eviction leaves are re-usable by
573
+ the next install, and the campaign to fix compaction properly is plan §3.4's.
574
+ - **No disk tier** (design §6). CDN + RAM + VRAM.