@woosh/meep-engine 3.25.0 → 3.27.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 (295) hide show
  1. package/package.json +1 -1
  2. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +542 -0
  3. package/src/core/binary/lz4/LZ4_LEVEL.d.ts +33 -0
  4. package/src/core/binary/lz4/LZ4_LEVEL.d.ts.map +1 -0
  5. package/src/core/binary/lz4/LZ4_LEVEL.js +32 -0
  6. package/src/core/binary/lz4/lz4_compress_block_at_level.d.ts +33 -0
  7. package/src/core/binary/lz4/lz4_compress_block_at_level.d.ts.map +1 -0
  8. package/src/core/binary/lz4/lz4_compress_block_at_level.js +47 -0
  9. package/src/core/binary/lz4/lz4_compress_block_hc.d.ts +40 -0
  10. package/src/core/binary/lz4/lz4_compress_block_hc.d.ts.map +1 -0
  11. package/src/core/binary/lz4/lz4_compress_block_hc.js +1382 -0
  12. package/src/core/binary/lz4/lz4_decompress_block.d.ts +2 -1
  13. package/src/core/binary/lz4/lz4_decompress_block.d.ts.map +1 -1
  14. package/src/core/binary/lz4/lz4_decompress_block.js +31 -6
  15. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +66 -0
  16. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  17. package/src/core/geom/3d/shape/HeightMapShape3D.js +314 -20
  18. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  19. package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
  20. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  21. package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
  22. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  23. package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
  24. package/src/engine/physics/cloth/MEASUREMENTS.md +582 -0
  25. package/src/engine/physics/cloth/PLAN.md +330 -49
  26. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  27. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  28. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  29. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +65 -7
  30. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  31. package/src/engine/physics/cloth/ecs/ClothInstance.js +72 -8
  32. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +25 -4
  33. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  34. package/src/engine/physics/cloth/ecs/ClothSystem.js +75 -8
  35. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +41 -4
  36. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  37. package/src/engine/physics/cloth/ecs/ClothWorld.js +207 -63
  38. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +10 -0
  39. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -1
  40. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +17 -1
  41. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +27 -3
  42. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  43. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +28 -3
  44. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +75 -10
  45. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  46. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +131 -12
  47. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  48. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  49. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  50. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  51. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  52. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  53. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  54. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  55. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  56. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  57. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  58. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  59. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +12 -2
  60. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  61. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +12 -2
  62. package/src/engine/physics/cloth/solver/cloth_step_instance.js +1 -1
  63. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  64. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  65. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  66. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +23 -9
  67. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -1
  68. package/src/engine/physics/cloth/wind/AbstractClothWind.js +23 -9
  69. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +7 -5
  70. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -1
  71. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +12 -9
  72. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  73. package/src/engine/physics/cloth/wind/ClothFluidWind.js +5 -2
  74. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +7 -2
  75. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  76. package/src/engine/physics/cloth/wind/cloth_sample_air.js +12 -5
  77. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  78. package/src/shade/playground/vgeo_runtime/README.md +167 -22
  79. package/src/shade/playground/vgeo_runtime/index.html +112 -62
  80. package/src/shade/playground/vgeo_runtime/main.js +660 -83
  81. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +1 -1
  82. package/src/shade/playground/vgeo_viewer/sample_asset.js +19 -2
  83. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +11 -0
  84. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  85. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +16 -8
  86. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  87. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +16 -11
  88. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +73 -5
  89. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +26 -15
  90. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +5 -3
  91. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +18 -0
  92. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  93. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +20 -0
  94. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.d.ts.map +1 -1
  95. package/src/shade/renderer/geometry/virtual/build/vgeo_assemble_pages.js +56 -9
  96. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  97. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +1 -0
  98. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +22 -0
  99. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  100. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +24 -0
  101. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts +12 -0
  102. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts.map +1 -1
  103. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +13 -0
  104. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts +8 -1
  105. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts.map +1 -1
  106. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +11 -3
  107. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts +9 -0
  108. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts.map +1 -1
  109. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +10 -1
  110. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.d.ts +18 -0
  111. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.d.ts.map +1 -0
  112. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.js +17 -0
  113. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.d.ts +8 -0
  114. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.d.ts.map +1 -0
  115. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.js +7 -0
  116. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.d.ts +3 -0
  117. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +16 -0
  118. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.d.ts +14 -0
  119. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.d.ts.map +1 -0
  120. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.js +13 -0
  121. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.d.ts +11 -0
  122. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.d.ts.map +1 -0
  123. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.js +10 -0
  124. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +20 -0
  125. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -1
  126. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +20 -2
  127. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +4 -0
  128. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  129. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +4 -0
  130. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +29 -3
  131. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  132. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +81 -122
  133. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +40 -0
  134. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  135. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +44 -0
  136. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts +69 -0
  137. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts.map +1 -0
  138. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.js +236 -0
  139. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +14 -0
  140. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -1
  141. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +15 -0
  142. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.d.ts +31 -0
  143. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.d.ts.map +1 -0
  144. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.js +32 -0
  145. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +10 -0
  146. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -1
  147. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +21 -1
  148. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.d.ts +15 -0
  149. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.d.ts.map +1 -0
  150. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.js +84 -0
  151. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts +22 -0
  152. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts.map +1 -0
  153. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.js +96 -0
  154. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  155. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +55 -0
  156. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  157. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +202 -13
  158. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  159. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +147 -10
  160. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +637 -0
  161. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +33 -36
  162. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -1
  163. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +109 -193
  164. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +58 -28
  165. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  166. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +77 -105
  167. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +37 -0
  168. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +105 -22
  169. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +548 -0
  170. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +29 -50
  171. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  172. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +47 -489
  173. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts +84 -0
  174. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -0
  175. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +393 -0
  176. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts +234 -0
  177. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -0
  178. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +1363 -0
  179. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.d.ts +81 -0
  180. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.d.ts.map +1 -0
  181. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.js +89 -0
  182. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +53 -34
  183. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -1
  184. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +407 -124
  185. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +100 -0
  186. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -0
  187. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +219 -0
  188. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.d.ts +17 -0
  189. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.d.ts.map +1 -0
  190. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.js +47 -0
  191. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.d.ts +20 -0
  192. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.d.ts.map +1 -0
  193. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.js +19 -0
  194. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -1
  195. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +30 -12
  196. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  197. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +15 -0
  198. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -1
  199. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +18 -1
  200. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -1
  201. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +5 -15
  202. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.d.ts +3 -0
  203. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.d.ts.map +1 -0
  204. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.js +77 -0
  205. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts +3 -0
  206. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -0
  207. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +186 -0
  208. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  209. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +20 -8
  210. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  211. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +580 -188
  212. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +15 -18
  213. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -1
  214. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +19 -28
  215. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  216. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +45 -25
  217. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +1 -0
  218. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +11 -0
  219. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +1 -1
  220. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +0 -1
  221. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +12 -10
  222. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +1 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +8 -0
  224. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +7 -2
  225. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -1
  226. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +8 -3
  227. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +2 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +19 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -1
  230. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +1 -1
  231. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.d.ts +5 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.d.ts.map +1 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.js +22 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.d.ts +14 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.d.ts.map +1 -0
  236. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.js +13 -0
  237. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.d.ts +17 -0
  238. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.d.ts.map +1 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.js +16 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts +15 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts.map +1 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.js +16 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.d.ts +23 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.d.ts.map +1 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.js +28 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.d.ts +2 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.d.ts.map +1 -0
  248. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.js +13 -0
  249. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +10 -5
  250. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -1
  251. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +15 -6
  252. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +6 -1
  253. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -1
  254. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +38 -1
  255. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  256. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +4 -3
  257. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  258. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  259. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +6 -3
  260. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  261. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +6 -3
  262. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  263. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  264. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +12 -4
  265. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +2 -0
  266. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  267. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +58 -57
  268. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  269. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +3 -5
  270. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  271. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +3 -4
  272. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  273. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +3 -4
  274. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.d.ts.map +1 -1
  275. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.js +7 -7
  276. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  277. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +83 -78
  278. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  279. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +7 -1
  280. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  281. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +7 -1
  282. package/src/shade/renderer/scene/GPUSceneContext.d.ts +22 -2
  283. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  284. package/src/shade/renderer/scene/GPUSceneContext.js +59 -4
  285. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  286. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +92 -16
  287. package/src/shade/renderer/view/GPUViewContext.d.ts +5 -0
  288. package/src/shade/renderer/view/GPUViewContext.d.ts.map +1 -1
  289. package/src/shade/renderer/view/GPUViewContext.js +21 -0
  290. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  291. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +74 -3
  292. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.js +0 -723
  293. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.js +0 -50
  294. package/src/shade/renderer/shader/chunk/jobs/NOTES.md +0 -47
  295. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.js +0 -102
@@ -0,0 +1,637 @@
1
+ # Virtual Geometry — the cut becomes state
2
+
3
+ **2026-09-14, revision 2** after an adversarial pass over revision 1 (ten findings; the ones that
4
+ changed the design are marked *(rev 2)*, and §6.1 — which rev 1 called the blocking question — is
5
+ answered against the builder and the container validator).
6
+
7
+ A design, not yet a commitment: nothing here is built. It supersedes nothing until it does, and what
8
+ it proposes to change in `RUNTIME_PLAN.md` is §4 — not the cut *rule*, whose proofs stand unchanged,
9
+ but the claim that the rule is evaluated from the root every frame.
10
+
11
+ It also asks for one **format** change (§2.1). That is deliberate and it is the reason this is
12
+ written before any code: `.vgeo` has no upward edge, and whether it gets one decides which of two
13
+ very different runtimes we build.
14
+
15
+ ## 0. Why — measured, not argued
16
+
17
+ A `.sgpt` capture of `playground/vgeo_runtime/` (4090, torus 12,288 faces, τ=1, one instance) and a
18
+ sweep driven through CDP against the live page. **Read medians**: this machine throws ~400 µs stalls
19
+ that land on unrelated passes too — `mesh frustum cull` has median 3.07 µs and max 468.99 µs in the
20
+ same capture — so a mean over frames is mostly environment.
21
+
22
+ The frame runs **16 traverse dispatches** — two expansion sites, `max_iterations` 8 each. Their cost
23
+ is not spread across them:
24
+
25
+ | ordinal | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 |
26
+ |---|---:|---:|---:|---:|---:|---:|---:|---:|
27
+ | median, µs | **40.96** | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 |
28
+
29
+ One pass carries it and the rest are at the 1.02 µs timestamp tick. And that pass is **not the
30
+ geometry**. Holding the scene still and sweeping `descent_steps`, with `items` fixed at 78 and
31
+ `drawn` fixed at 31:
32
+
33
+ | `descent_steps` | 4 | 8 | 16 | 32 | 64 | 128 |
34
+ |---|---:|---:|---:|---:|---:|---:|
35
+ | first traverse, 1 instance | 11.3 | 18.4 | — | 36.9 | 52.2 | — |
36
+ | first traverse, 256 instances | 13.3 | 25.6 | 38.9 | 46.1 | 62.5 | 94.2 |
37
+
38
+ **A dispatch costs ≈ 9 µs + ~0.5 µs × `descent_steps` + work.** The slope is the same at one
39
+ instance and at 256, so it is neither memory latency nor an occupancy problem: it is the serial
40
+ dependent chain of ~16 subgroup operations every step executes whether or not the wave still holds
41
+ anything — two full scans to pack what the wave keeps, two more for the spill, the five-shuffle
42
+ owner search, six payload shuffles, elect and broadcast. A dispatch lasts one wave's step loop and
43
+ every wave runs that loop concurrently, so idle steps are paid at full price, once.
44
+
45
+ The other half of the measurement is the one that changes the target. At `descent_steps` 8, adding
46
+ instances of the same geometry:
47
+
48
+ | instances | 1 | 16 | 64 | 256 | 1024 |
49
+ |---|---:|---:|---:|---:|---:|
50
+ | items descended | 78 | 1,248 | 4,992 | 19,968 | 79,872 |
51
+ | clusters drawn | 31 | 496 | 1,984 | 7,936 | 31,744 |
52
+ | cut, µs/frame | 97.2 | 131.0 | 156.8 | 159.7 | **160.7** |
53
+
54
+ `QUEUE_REMAINING` 0 and nothing dropped throughout. **1024 instances cost what 64 do.** The cut is
55
+ not a per-instance cost and the 100s-to-1000s target is already met; what it is, is a *constant*,
56
+ and the constant is the depth of the DAG — which grows like log2(triangles) and is a property of
57
+ the asset rather than of the scene. Deeper asset, bigger constant: the same kernel on a Zorah
58
+ container runs 62.5 µs a dispatch where the torus runs 41.
59
+
60
+ **What that table does not say *(rev 2)*.** Every instance sat at the same transform, so all 1024
61
+ were at the same distance, selected the same level, and read the same rows — best-case L2 and a
62
+ single working set. The item counts are honest (79,872 items really were descended) but the memory
63
+ behaviour is not representative of 1024 instances spread through a scene at different depths.
64
+
65
+ **Redone with spread transforms *(§7.2)*.** Same torus, instances on a grid three radii apart, the
66
+ camera pulled back each time to frame the whole grid:
67
+
68
+ | instances | 1 | 16 | 64 | 256 | 1024 |
69
+ |---|---:|---:|---:|---:|---:|
70
+ | items, stacked | 65 | 1,040 | 4,160 | 16,640 | 66,560 |
71
+ | items, spread | 65 | 512 | 1,664 | 1,222 | 1,968 |
72
+ | pages visited, spread | 6 | 80 | 265 | 348 | 984 |
73
+ | cut µs/frame, spread | 116.6 | 162.7 | 158.7 | 146.3 | **100.2** |
74
+
75
+ The flatness survives, and it is not the L2 that saves it: spreading instances over a scene and
76
+ framing them all puts each one further away, so each selects a **coarser** level and the descent is
77
+ shorter. 1024 spread instances descend 1,968 items where 1024 stacked ones descend 66,560, and the
78
+ cut halves rather than growing. The two tables are therefore not the same workload — the stacked one
79
+ is the pathological case where every instance is near — and neither of them is a per-instance cost.
80
+ That is the claim §0 needed, and it now rests on both.
81
+
82
+ **The free part first — and it is a quarter, not a third.** `drawn` is bit-identical at every
83
+ `descent_steps` that drains, so the knob slices the descent and cannot change the selected set: the
84
+ default is a pure schedule choice. What bounds it is **not** `descent_steps × max_iterations`
85
+ exceeding the DAG depth. A wave keeps one child per lane and spills the rest, so a dispatch advances
86
+ what it held by `descent_steps` levels and hands everything else to the next dispatch, and the
87
+ dispatches a cut needs are
88
+
89
+ > `ceil(depth / descent_steps)` **plus its spill generations**
90
+
91
+ which is a breadth term the depth model has no room for. Measured over a full orbit of the deepest
92
+ converted Zorah container — 27.9M faces, 17,172 pages, the 39 levels — at τ=1, `max_iterations` 8:
93
+
94
+ | `descent_steps` | 32 | 24 | 20 | 16 | 12 | 10 | 8 |
95
+ |---|---:|---:|---:|---:|---:|---:|---:|
96
+ | dispatches needed | 6 | 6 | 7 | 8 | 8 | >8 | >8 |
97
+ | worst `QUEUE_REMAINING` | 0 | 0 | 0 | 0 | 0 | 14 | 208 |
98
+
99
+ So **8 is not available at `max_iterations` 8** — it leaves the descent holding work, which §4.5
100
+ makes a hole rather than coarseness — and 24 is the most the default can be lowered for no dispatch
101
+ margin at all: the same six dispatches 32 needs, with a quarter fewer idle steps inside each. That
102
+ is what §7.1 took.
103
+
104
+ The ~3× was the torus, and the torus does no work: at 12,288 faces the cut *is* the per-step
105
+ constant. On the container the second traverse alone is 309 µs of real descent, so the eight steps
106
+ saved move the frame by **3.3%** — 591.8 → 572.4 µs over the close orbit, three repeats each and the
107
+ two sets do not overlap. At the far orbit the same pair does not separate at all: repeats of one
108
+ setting span 340–549 µs, because residency keeps growing across a session and a later run is simply
109
+ holding more pages. That is why the table above is dispatch counts. Dispatch counts are integers,
110
+ they are what the safety question is actually asked in, and they do not drift.
111
+
112
+ It does not fix the shape either: a still camera still re-derives the same answer from node 0 every
113
+ frame, 39 levels deep, once per active bucket per view (§3).
114
+
115
+ ## 1. What is kept between frames
116
+
117
+ **The frontier**: per (view, instance), the set of **groups** the cut has descended to.
118
+
119
+ Groups, not clusters, because that is the granularity the rule itself has. A cluster is drawn iff
120
+
121
+ > `own group selectable AND project(parent) > tau AND (project(self) <= tau OR child group not selectable)`
122
+
123
+ and of those, the first two terms are **group-uniform** — every cluster of a group is consumed by
124
+ one step and carries that step's sphere and error (§2, verified) — while only the third varies per
125
+ cluster. So a group is uniformly *reached*, and within it each cluster independently draws or
126
+ refines. The frontier is the reached set; the per-cluster test over it is the cheap half, and the
127
+ profile says so: 78 items descended to find 31 drawn clusters cost 41 µs. **Finding the pages is the
128
+ entire cost. Testing the clusters is free.**
129
+
130
+ **It reseeds the descent; it does not replace it.** The seed pass today emits one item per instance
131
+ naming its root page's node 0. Seeded from the frontier instead, every other pass stays exactly as
132
+ it is — the subgroup descent, the spill, the claim, the draw, the wants. What changes is that
133
+ `descent_steps` then has to cover the *delta* between frames, one or two levels, rather than the
134
+ depth of the DAG. The measured law stops working against us: 0.5 µs × 2 instead of × 32.
135
+
136
+ **A stale frontier is never wrong.** Any consistent cut of the DAG is watertight — the same argument
137
+ the clamp rests on. Too coarse costs detail, too fine costs triangles; neither costs a hole, as long
138
+ as a group moves as a unit, which the group-uniform terms guarantee. Three consequences, and they
139
+ are what make this buildable:
140
+
141
+ - the per-frame update can be **budgeted** — N moves per view per frame, with no correctness cliff
142
+ when the budget binds;
143
+ - validation can be **conservative** — when in doubt, fall back, never repair;
144
+ - **hysteresis is free** (refine at τ, coarsen at ~0.85τ), which removes the flip-flop a per-frame
145
+ cut cannot avoid. Note that TAA jitter is *not* a source of that churn here: `vg_project` reads
146
+ the eye position and `projection_k`, and a sub-pixel clip-space offset changes neither. The churn
147
+ is camera motion, and it is two-directional — pan around a house and near walls refine while the
148
+ far side coarsens, in one geometry, in the same frame.
149
+
150
+ **Only refinement is budgeted *(rev 2)*.** Coarsening strictly shrinks the cut and the draw that
151
+ follows it, and its destinations are resident by §2's invariant, so refusing it is the expensive
152
+ direction: a camera pulling back against a bound coarsen budget would hold the finest level on
153
+ screen for as many frames as the budget takes to drain, which is a sustained frame-time spike rather
154
+ than a transient. Coarsen every entry that asks; budget only the descent.
155
+
156
+ ### 1.1 Keying — what a frontier entry belongs to *(rev 2)*
157
+
158
+ `instance_slot` is **not** an identity. `shader_vgeo_cut_collect` assigns it with
159
+ `atomicAdd(&claim_widths.count, 1u)`, so it is a compaction index whose order is decided by GPU
160
+ scheduling: an identical scene can hand the same instance a different slot on the next frame. A
161
+ frontier stored against a slot would attach itself to a different instance, silently, and draw one
162
+ object's cut through another's transform.
163
+
164
+ The frontier is therefore keyed by the instance's **mesh row** — stable, already in the instance
165
+ record as `VGEO_INSTANCE_OFFSET.MESH_INDEX`, and the same "entity is the row" identity the rest of
166
+ the scene state uses. The per-frame instance list maps slot → mesh row in one read, so a reseeded
167
+ seed pass resolves its frontier without a second structure. The store itself is a dense table
168
+ indexed by mesh row, like every other GPU-managed component.
169
+
170
+ That also answers where the frontier *lives*: not in a frame-graph transient, which is recycled, but
171
+ in a runtime-owned buffer with the pool and the tables.
172
+
173
+ ## 2. Walking up
174
+
175
+ Coarsening is half of every camera movement, and mixed with refinement inside a single instance.
176
+ A design that can only descend would have to answer every coarsen by re-descending from the root —
177
+ the cost this whole document is trying to remove, paid on the common case. **So either the runtime
178
+ can walk up, or keeping a cut is not worth doing.**
179
+
180
+ **How often it actually moves the frontier, measured *(§7.2)*.** Less than "half of every movement"
181
+ suggests, and the reason is worth knowing before the kernel is written. When the camera pulls back,
182
+ a frontier group's clusters stop being drawn and their parents' clusters must be drawn instead — but
183
+ **47% of frontier entries already draw some clusters and refine others** (measured below), so the
184
+ parent group is usually *already in the frontier* and the coarsen changes which of its clusters draw
185
+ rather than adding an entry. Only the coarsen whose destination is not already there needs the
186
+ upward edge, and that is 0.0–0.8 entries a frame at a walking camera.
187
+
188
+ So §2.1 stays: when the edge is needed there is no other way to find the destination, and a
189
+ re-descent from the root is the alternative. But the coarsen path is not a hot path, which is a
190
+ reason to write it for clarity rather than for throughput.
191
+
192
+ Today it cannot, and not merely when a page is missing. The cluster record is
193
+ `self_sphere, parent_sphere, self_error, parent_error, child_ref, child_group, …`: the parent fields
194
+ are *metrics of the consuming step*, not a reference to what consumes it. The group record carries
195
+ `parent_cluster_count` — a count, deliberately. Every edge in `.vgeo` points down.
196
+
197
+ Three things are already in place, and they are what make the addition small.
198
+
199
+ **One step per group, and the decision is local — verified *(rev 2)*.** `vgeo_build_levels` builds a
200
+ group from exactly one step (`group.clusters = step.child_clusters`), writes that step's error and
201
+ sphere into every child's `parent_error`/`parent_sphere`, and writes the *same* values into every
202
+ parent's `self_error`/`self_sphere` while pointing each parent's `child_group` at that group. The
203
+ container validator then enforces it per (parent, child) pair with exact equality — "one group step
204
+ writes one value on both sides" — and separately that `parent_error ≥ self_error` per cluster.
205
+ Monotonicity is imposed at construction rather than hoped for: the step's error is floored by
206
+ `max_child_error(child_clusters)`, and its sphere is built to contain both what it consumed and what
207
+ it produced.
208
+
209
+ So the coarsen test a cluster reads is **bit-identical** to its parents' own draw test — the same
210
+ floats, so the same IEEE result, with no tolerance argument and no vote. Every cluster of a group
211
+ agrees, every parent of that group agrees, and none of them has to communicate to find out. That is
212
+ the Nanite locality property, and here it is a validated invariant rather than an aspiration.
213
+
214
+ **Tested, not only argued *(§7.5)*.** A CPU oracle that starts at the pages nothing refines into and
215
+ walks up through §6.3's table selects the *same multiset of clusters* as `vgeo_select_cut` descending
216
+ from the root, over 7 camera positions × 4 thresholds on a 160×80 torus. It never compares two
217
+ floats for nearness: it reads `parent_error`/`parent_sphere` off the child and
218
+ `self_error`/`self_sphere` off the parent and requires the same IEEE answer. That is the whole design
219
+ resting on one property, and the property holds on real build output.
220
+
221
+ **The predicate for "may I walk up" exists and is maintained.** `VGEO_GROUP_FLAG_SELECTABLE` is
222
+ "every parent reference is live", kept incrementally by `VGeoRuntimeTables` — an install counts the
223
+ references a resident parent page's selectable groups make into the arriving page, an eviction takes
224
+ them back out (`RUNTIME_PLAN_2026_09_14` §3). A group in the cut is selectable by construction, so
225
+ **its parents are resident, and selectable in turn, all the way up**. Coarsening a drawn group is
226
+ therefore always legal; a cascade of several levels is legal. The kernel still checks — the check is
227
+ one lookup — but the check is not expected to fail, and when it does the answer is §4's fallback.
228
+
229
+ **The coarsen decision needs no new data.** Per the verified identity above, `project(parent) ≤ τ` on
230
+ a cluster of G *is* the parents' `project(self) ≤ τ`. The format change is one edge, not a new
231
+ metric.
232
+
233
+ ### 2.1 The format change — **landed**, as `format_version` 4 (§7.3)
234
+
235
+ What follows is the proposal as written; the shipped shape matches it, with the offsets settled and
236
+ the width question answered below. `VGEO_FORMAT.md` §6.3, §6.4 and §11.15–16 are now the reference.
237
+
238
+ One thing the plan did not anticipate: the parent tables are not optional per page, so they are part
239
+ of what a page *costs*, and `vgeo_assemble_pages` lays pages out against `nominal_page_size` before
240
+ the writer ever sees them. The assembler therefore has to budget for a group's parent entries while
241
+ deciding which page the group lands in — bounded by one parent page per entry, since the exact
242
+ parent-page count depends on the placement being decided. Without that, a build simply refuses
243
+ itself: the first attempt put a 16 KB-bounded page at 16,396 bytes.
244
+
245
+ The other is that nothing has to be rebuilt. `parent_table_offset` reads zero on every container
246
+ written before this, which is exactly "no upward edge", so a version-4 reader takes old files and a
247
+ version-3 reader takes new ones. `min_reader_version` did not move. **The 2,975 converted Zorah
248
+ containers are all version 3**, so the coarsen path of §7.5 and the frontier of §7.6 meet assets
249
+ with no parent tables and have to fall back rather than assume — the same fallback §4 already
250
+ defines, reached for a different reason.
251
+
252
+ Mirror the child table, minus everything a parent does not need. A child reference must carry a
253
+ fetch range because the child may be absent; a parent reference never does, because a parent of a
254
+ drawn group is resident by the invariant above. So this is 4 bytes an entry, not 48.
255
+
256
+ **Group record (§6, 8 B — no growth).** The `reserved u16` at offset 6 becomes `parent_first`, an
257
+ index into the page's group-parent table. `parent_cluster_count` at offset 4 is already the length.
258
+
259
+ | offset | type | field | |
260
+ |---:|---|---|---|
261
+ | 0 | u16 | cluster_first | unchanged |
262
+ | 2 | u16 | cluster_count | unchanged |
263
+ | 4 | u16 | parent_cluster_count | unchanged — now also the length of the list below |
264
+ | 6 | u16 | **parent_first** | was `reserved` |
265
+
266
+ **Group-parent table (new, in the decoded page).** `parent_entry_count` × 4 B:
267
+
268
+ | offset | type | field | |
269
+ |---:|---|---|---|
270
+ | 0 | u16 | parent_ref | index into the page's parent-page table, or `SAME_PAGE` |
271
+ | 2 | u16 | parent_cluster | cluster index within that page |
272
+
273
+ **Parent-page table (new, in the decoded page).** `parent_page_count` × 4 B, deduplicated, one entry
274
+ per distinct page holding a parent: just `page_index u32`, resolved through `vg_lookup` exactly as
275
+ the cut already resolves a child. It lives in the *decoded* page rather than beside the plaintext
276
+ child table, because nothing needs it before decode — no fetch range is computed from it — and in
277
+ the blob it compresses with the other tables (§5: tables are 7% of page bytes at 1.6×). It is not
278
+ optional per page, so it also raises `decoded_size` and therefore the RAM tier's budget by the same
279
+ fraction *(rev 2)*.
280
+
281
+ **PageHeader (§6, 56 B — no growth).** Two of the eight reserved bytes at offset 24 become
282
+ `parent_table_offset u32`; two more `parent_entry_count u16` and `parent_page_count u16`.
283
+
284
+ **`parent_first` is capped, not widened — answered *(§7.3)*.** The concern was real:
285
+ `parent_entry_count` is Σ `parent_cluster_count` over a page's groups and `group_count` is itself a
286
+ u16, so the arithmetic does not forbid overflow the way `child_ref` does. The measurement says the
287
+ distance is not close. Across **8,899 pages of 30 converted Zorah containers**, from 1 MB to 120 MB:
288
+
289
+ | | mean | worst |
290
+ |---|---:|---:|
291
+ | parent entries per page | 17.4 | **26** |
292
+ | parent pages per page | 4.9 | 18 |
293
+ | parents of one group | — | 5 |
294
+
295
+ The u16 bound stands **2,521× above the worst real page**, and 40 of 8,899 pages carry no parent
296
+ entry at all. So the cap is `VGEO_MAX_PARENT_ENTRY_COUNT` and the group record keeps its 8 bytes.
297
+ The cost is also smaller than §2.1 estimated: 17.4 + 4.9 entries at 4 B is about 90 bytes against a
298
+ 56–256 KB page, a tenth of a percent rather than "well under 1%".
299
+
300
+ **Cost.** Sum over groups of `parent_cluster_count` × 4 B, plus 4 B per distinct parent page. At the
301
+ converted assets' shape — a few parents per group — that is single-digit bytes per group against
302
+ 56–256 KB pages: well under 1%, and compressible.
303
+
304
+ **Arena (runtime).** The page slot gains a fourth section:
305
+ `[header][node rows][group rows][cluster rows][parent rows]`, addressed by a `vg_parent_base` that
306
+ follows the existing pattern, with `PARENT_ENTRY_COUNT` in the page header row and `PARENT_FIRST` in
307
+ the group row. The slot stride is already a uniform (`page_slot_words`), so nothing else moves.
308
+
309
+ **Build invariants to add (§11).** Every parent reference resolves to a cluster whose `child_group`
310
+ is this group; `parent_cluster_count` equals the list length; the list is deduplicated and sorted by
311
+ (page_index, cluster), so the election in §3 is deterministic on the GPU.
312
+
313
+ ## 3. The frame
314
+
315
+ Per view, one dispatch over the frontier, replacing the seed:
316
+
317
+ 1. **Validate** (§4). An entry that fails takes its instance out of the incremental path for this
318
+ frame.
319
+ 2. **Test.** For each cluster of each frontier group, the existing rule, unchanged — draw, clamp,
320
+ want.
321
+ 3. **Move.** `project(self) > τ` and the child group selectable → the child group enters the
322
+ frontier, under the refine budget. `project(parent) ≤ τ` → its parents' groups enter,
323
+ unbudgeted (§1). Both are the existing descent code, seeded differently.
324
+
325
+ **G leaves only when every one of its clusters has moved *(§7.2)*.** Rev 2 wrote "and G leaves
326
+ it", which is wrong: `project(self)` is per cluster, not group-uniform — §1's own list says so —
327
+ and a group whose clusters disagree has to stay in the frontier for the ones still drawing while
328
+ its child groups enter for the ones refining. That is not an edge case. Measured on a converted
329
+ container at a walking camera, **47% of frontier entries draw some clusters and refine others**
330
+ (34.6% on a smaller one), so removing G on the first cluster that refines would drop about half
331
+ the frontier's drawn surface every frame. An entry's departure is therefore a count reaching
332
+ zero, not a flag being set, and that is one more word per entry rather than a different design.
333
+
334
+ The budget is **one bound per view per frame**, streaming: entries are taken in frontier order
335
+ from a persistent cursor, so a frame that runs out resumes where it stopped rather than starving
336
+ whatever sorts last. One frontier serves **every expansion site of a view** — it describes where
337
+ the camera is, not what the depth prepass concluded — and no site can move an entry another
338
+ already moved, because their instance sets are disjoint. See §6.3 for what "disjoint" was
339
+ verified to mean, and for the two things rev 2 got wrong about those sites.
340
+ 4. **Draw** is unchanged, and so is everything downstream of `out_meshlets`.
341
+
342
+ **Dedup is a refine-side problem only *(rev 2)*.** A cluster names exactly one `child_group`, so
343
+ distinct groups have disjoint parent sets and coarsening can never append the same group twice — no
344
+ claim, no election. Refinement can: `parent_cluster_count > 1` is precisely several clusters naming
345
+ one child group. Because §2 makes their decisions bit-identical, the append can be elected rather
346
+ than deduplicated — a refining cluster appends the child group only if it is the **first entry of
347
+ that group's parent list**, which §2.1's sorted, deduplicated table makes a single comparison. No
348
+ bitmask, and nothing sized by instances × groups.
349
+
350
+ A camera cut is the one case where a bounded number of refine steps is not enough. It is also the
351
+ case the view already detects — `indicate_view_change` exists, and per-view previous-camera state is
352
+ already kept — so the answer is to drop the frontier and take today's root descent for that frame,
353
+ at today's cost, which is exactly what §4's fallback already does for a different reason. That
354
+ signal is a TAA hint rather than a statement about the cut, so the durable form is a counter: an
355
+ instance whose frontier wants to move more than K entries in one frame is cheaper to re-descend
356
+ than to walk *(rev 2)*.
357
+
358
+ ## 4. Validation, without coupling pages to cuts
359
+
360
+ Do not maintain page → cut back-references. The entry names a `page_row`; let the row say whether it
361
+ still holds the occupant the entry was written against.
362
+
363
+ **Use the generation, not `visible_from` *(rev 2)*.** Rev 1 proposed mirroring
364
+ `GPUVirtualGeometryPool#visible_from[page_row]` — "the frame whose flush publishes the row's current
365
+ occupant" — and comparing `visible_from[r] ≤ F`. That works for the *feedback* path, where F is the
366
+ frame the cut ran and the record is read on the CPU, but on the GPU it makes every entry carry a
367
+ frame number and every check an ordering argument, including the case of a row installed twice
368
+ within one frame. `VGeoRuntimeTables#generations[page_row]` is a plain counter bumped on install:
369
+ the entry stores the generation it saw, validity is one equality, and there is no clock. Mirror that
370
+ word into the arena page header at install — one `u32`, written where every other page field is.
371
+
372
+ Together with `VGEO_GROUP_FLAG_SELECTABLE` for the parent side, it covers both failure modes: the
373
+ row was refilled, or the way up was evicted.
374
+
375
+ **The fallback is not repair, and it cannot be deferred.** An invalid entry disqualifies its
376
+ *instance* for the frame, which then seeds from the root — today's path, today's cost, already
377
+ correct. It must happen in the **same frame**: a dropped entry's surface is not covered by anything
378
+ else, because the stale cut had already descended past its ancestors, so postponing the re-descent
379
+ is a hole rather than a coarse stand-in *(rev 2)*. Steady state is incremental; residency churn
380
+ degrades to what we have now, which is also exactly when the cut is changing anyway.
381
+
382
+ **What that costs, stated honestly *(rev 2)*.** Because the fallback is same-frame, the graph must
383
+ keep encoding a ladder deep enough for a worst-case root descent every frame, even on frames where
384
+ no instance uses it. The frontier removes the ~41 µs first traverse; it does **not** remove the ~50
385
+ µs of empty prepare/traverse pairs that the ladder costs when every dispatch is empty. So the
386
+ expected win is roughly half of today's cut, not all of it, and the next lever after this is the
387
+ ladder itself — fusing prepare into traverse, or finding a bound that lets the encode shrink. Rev 1
388
+ claimed the ladder could collapse to a single pair; it cannot, and §6.5 is struck.
389
+
390
+ ## 5. Per view, and CSM
391
+
392
+ Frontier entries are groups, and the measured shape is **4.0 clusters per group, 3.2 of them
393
+ selected** (§7.2, converted container) rather than the ~8 rev 2 assumed — so a scene drawing 31,744
394
+ clusters is ~10,000 entries, not ~4,000. An entry is `(page_row, group, generation)` plus the mesh
395
+ row it belongs to and the departure count §3 now needs — 16 B packed, or 12 B if the table is
396
+ segmented per mesh row rather than carrying it — so that scene is **120–160 KB per view**, and a
397
+ scene drawing a million clusters is ~5 MB per view. Five views (main plus four cascades) is tens of
398
+ MB rather than single-digit. Still a fraction of what storing the cut per *cluster* would cost, and
399
+ still affordable, but it is the number to size the buffer from.
400
+
401
+ Note that "per view" is the frontier's granularity and **not** the granularity the runtime can
402
+ express today: §6.3 found the params buffer is frame-global, collapsed across sites with a `Math.min`,
403
+ and `projection_k` is set once in `begin_frame` from one view's screen height. Per-view anything is
404
+ work §7.6 has to do first, and it is not small.
405
+
406
+ Cascades are the good case: a snapped cascade that did not move needs no update at all, and its
407
+ frontier is stable for many frames. But a cascade that *does* snap changes its projection wholesale,
408
+ which flips many decisions at once — that is the worst case for the budget, and it is exactly when
409
+ the counter in §3 should give up and re-descend rather than walk *(rev 2)*.
410
+
411
+ Per-view state is not novel here — the HZB and the previous camera are already per view — but the
412
+ closer precedent is `VGeoRuntimeTables`: selectability is a derived structure over the page DAG that
413
+ is *kept and patched on install and evict* rather than recomputed. This is that pattern applied to
414
+ the view-dependent half, with the same eviction hooks.
415
+
416
+ ## 6. Open — the things still to settle
417
+
418
+ 1. ~~Do all parents of a group come from one consuming step?~~ **Answered *(rev 2)*: yes.** One step
419
+ builds one group; both sides of the step carry bit-identical error and sphere; the validator
420
+ enforces it. Parents may still live in several *pages* — that is what
421
+ `parent_cluster_count`'s "`SAME_PAGE` included" means — which is why §2.1's table carries page
422
+ references rather than cluster indices alone.
423
+ 2. ~~Frontier dedup.~~ **Answered *(rev 2)*: election over the sorted parent list, refine side
424
+ only** (§3). No bitmask.
425
+ 3. ~~Is the frontier per view or per (view, bucket)?~~ **Answered: per view, and the partition is
426
+ verified — with two corrections *(§7.2)*.**
427
+
428
+ The load-bearing half holds. A view's expansion sites receive **disjoint** instance sets, and it
429
+ is disjoint by construction at every step: `shader_instances_rasterization_bucket_sort` derives
430
+ `bucket_index` from the mesh's one material and does a single `atomicAdd` and a single write, so
431
+ a mesh row lands in exactly one bucket; `shader_hzb_mesh_filter_2way` is one `if`/`else` on one
432
+ predicate with one append in each arm, so within a bucket positive and maybe cannot both hold a
433
+ row; and phase two consumes only `instances_maybe` through a one-way filter that appends a
434
+ subset. No instance is cut twice, so one frontier per view is safe and nothing can move an entry
435
+ another site already moved.
436
+
437
+ **There are not two sites — and that is a defect, not a shape to design around.** The
438
+ phase-one seam call sits *inside* the `ShadeDrawMode × ShadeDrawSide` loop of
439
+ `graph_rasterize_partial_opaque`, so a standard view records one cut per **active opaque bucket**
440
+ plus one for phase two. The engine already says so where rev 2 did not read it —
441
+ `GPUVirtualGeometryRuntime`: "once per bucket per HZB pass per view".
442
+
443
+ **Phase two already does it the right way**, which is what makes this a fix rather than a design
444
+ question. `graph_rasterize_remaining_opaque` filters its instances once, expands statics once,
445
+ cuts once, filters meshlets once, and only then calls `graph_sort_meshlets_by_bucket_id` and
446
+ loops the buckets to rasterize each slice with its own primitive state. Phase one is the outlier,
447
+ and the asymmetry is visible in its own buffers: `instances_maybe` is a single shared collection
448
+ every bucket iteration appends into, while `instances_positive` is created fresh per bucket.
449
+
450
+ What per-bucket costs: a cut is a whole ladder, and an idle prepare/traverse pair measures
451
+ **3.07 µs** on this machine (§0), so `max_iterations` 8 is ~25 µs of ladder per site before any
452
+ work, plus collect, seed and remainder. Nine active opaque buckets is nine of those where one
453
+ would do — and nine static expansions of the same instance set as well. **Decided: one cut per
454
+ view, bucket the meshlets afterwards**, which is the phase-two shape applied to phase one.
455
+
456
+ **The sets are disjoint but do not cover.** An instance reaches zero sites when it is
457
+ non-opaque, when its bucket is inactive, when phase two's re-test finds it occluded, or when the
458
+ 2-way filter overflows. That is harmless here for the reason §1 gives — an instance not cut this
459
+ frame simply does not move its entries, and a stale frontier is never wrong — but "partition" was
460
+ the wrong word and the design should not lean on coverage anywhere else.
461
+ 4. ~~Budget shape.~~ **Answered: per view, and streaming.** One bound on entries moved per view per
462
+ frame, held across instances rather than divided among them, with a persistent cursor into the
463
+ frontier so that an instance the bound cut off is where the next frame starts. Coarsening is
464
+ unbudgeted (§1), so the bound is on the descent alone.
465
+ 5. ~~What `descent_steps` becomes.~~ Struck *(rev 2)*: §4 shows the ladder has to stay encoded for
466
+ the fallback, so the knob is a tuning question for the delta path (2–4) and a separate one for
467
+ the fallback path (8), not a collapse.
468
+ 6. ~~**`parent_first` width** — cap or widen.~~ **Answered: cap *(§7.3)*.** The histogram is in
469
+ §2.1 — worst page 26 entries against a 65,535 bound, so the group record keeps its 8 bytes.
470
+ 7. ~~What the probe measures next.~~ **Answered *(§7.2)*.** Spread transforms are in §0. The churn
471
+ number, measured by running the CPU reference's descent at consecutive camera poses and diffing
472
+ the reached group sets — a frontier entry being a group with at least one selected cluster — on a
473
+ converted container of 1.2M faces and 694 pages:
474
+
475
+ | motion | radii/frame | frontier | entered refine | entered coarsen | left | moves/frame |
476
+ |---|---:|---:|---:|---:|---:|---:|
477
+ | orbit | 0.002 | 168 | 0.1 | 0.0 | 0.1 | 0.08% |
478
+ | orbit | 0.01 | 176 | 0.7 | 0.0 | 0.4 | 0.44% |
479
+ | orbit | 0.05 | 196 | 2.6 | 0.8 | 3.1 | 1.71% |
480
+ | dolly | 0.005 | 26 | 0.2 | 0.0 | 0.1 | 0.78% |
481
+ | dolly | 0.02 | 37 | 1.1 | 0.0 | 0.4 | 2.93% |
482
+ | dolly | 0.05 | 136 | 13.5 | 0.1 | 6.6 | 10.02% |
483
+
484
+ **The budget is small.** Under 2% of the frontier moves per frame at any orbit speed worth
485
+ rendering, and a dolly fast enough to cross the object in twenty frames reaches 10%. A bound of
486
+ ~16 entries a view a frame covers everything but the fastest dolly, and §3's counter is what
487
+ catches that one. Refinement dominates entries by better than 10:1 for the reason §2 now gives.
488
+
489
+ Two caveats on these numbers. The container did not fit the RAM tier — 232 of 694 pages — so a
490
+ frontier here is smaller than a fully resident one would be; and every entering group was
491
+ classified as arriving from a neighbour it had in the previous frontier (the "other" column was
492
+ 0.0 throughout), which says the churn measured is the camera and not the stream.
493
+
494
+ ## 7. Order, and what each step leaves green
495
+
496
+ 1. ~~`descent_steps` default 32 → 8~~ — **done as 32 → 24**, because the confirmation failed:
497
+ 8 leaves `QUEUE_REMAINING` at 55–208 on the deepest converted Zorah container, and the model
498
+ that predicted otherwise is corrected in §0. 24 needs the same six dispatches 32 does at every
499
+ camera tried, so it costs no margin; the selected set is bit-identical, which is what makes it
500
+ no behaviour change. Independent of the rest, as planned.
501
+ 2. ~~The probe additions in §6.7~~ — **done.** Spread transforms fold into §0, the churn table
502
+ into §6.7, and the partition verification into §6.3. Three of them changed the design rather
503
+ than confirming it: mixed groups mean a frontier entry leaves on a count reaching zero and not
504
+ on one cluster refining (§3); a view has one site per active opaque bucket plus one, not two
505
+ (§6.3); and "per view" is not a granularity the runtime can express yet (§5).
506
+ 3. ~~Format: the parent tables~~ — **done**, as `format_version` 4. `VGEO_FORMAT.md` §6.3, §6.4,
507
+ §11.15 and §11.16; the cap decision in §2.1; the writer builds the edge, the assembler budgets
508
+ for it, the reader parses it and the validator holds it to the cluster records. Round-trip
509
+ coverage both ways: the reader's tables must equal the relation rebuilt from `child_group`, and
510
+ three corruptions — a parent entry naming the wrong cluster, a run that does not tile, a
511
+ parent-page table that is not ascending — are each caught. Nothing consumes the tables yet, and
512
+ the runtime is green at 301.
513
+ 4. ~~Arena: the fourth section, `PARENT_FIRST`, and the generation mirror~~ — **done.** The slot
514
+ is `[header][node rows][group rows][cluster rows][parent rows]`, addressed by `vg_parent_base`;
515
+ a parent row resolves to a global page index at install exactly as a cluster row does, so a walk
516
+ up reads the shape the descent already reads. The group row took a fifth word rather than
517
+ padding to eight, and the page row took `PARENT_ENTRY_COUNT` — which is what a coarsening kernel
518
+ checks instead of asking an asset its version — and `GENERATION`. `slot_range` learned the
519
+ section, or the parent rows would never have been uploaded. Still nothing consumes them.
520
+
521
+ **A verification surface went away mid-session, and §7.5 needs it.** The `vgeo_runtime`
522
+ playground draws nothing in headless Chrome: `begin_frame` runs, the feedback resource is open,
523
+ the adapter is an NVIDIA Lovelace with subgroups, the container builds and its root page installs
524
+ with a selectable group — and `VGEO_CUT_STAT.PAGES` stays 0, so the seed never reaches its first
525
+ line and nothing reports an error.
526
+
527
+ It is **not** any of steps 1–4. It reproduces in a detached worktree at 5289ade4f, a commit whose
528
+ only change is Markdown, and the probes in §0 and §6.7 above were taken through this same surface
529
+ earlier the same day. What changed in between is the browser: Chrome stable installed
530
+ **153.0.8010.37 at 21:28**, between the last probe that drew (21:07) and the first that did not.
531
+ Dawn in 153 passes indirect-dispatch validation through immediate data and overwrites the
532
+ application's, so every `var<immediate>` read by a shader launched with
533
+ `dispatchWorkgroupsIndirect` reads 65,535 — and `shader_hzb_mesh_filter_2way` reads
534
+ `bucket_index` that way, emits no survivors, and hands the seam an empty instance list. That is a
535
+ browser regression with its own investigation; the one thing that does not fit is that Canary
536
+ 155.0.8057.0 reproduced it here too, which is worth handing back to whoever owns that.
537
+
538
+ So §7.5's gate is the agreement spec, which is what this plan asked for anyway: a coarsen path
539
+ can be validated and run against the CPU reference under the emulator, and cannot be compiled by
540
+ Tint until a browser on this machine draws again.
541
+ 5. ~~A `coarsen` path in the traversal kernel~~ — **done**, behind `VGEO_CUT_FLAG_COARSEN`, and
542
+ the gate holds: a cut seeded at the leaf pages and walked up draws exactly what the CPU oracle
543
+ walks up to, which is exactly what `vgeo_select_cut` descends to. The same run with the flag off
544
+ draws **nothing**, so the walk is doing the work and not the seed.
545
+
546
+ The path is small because the kernel already had the branch. `vg_visit_cluster` identified
547
+ "below the cut" and returned nothing; now, under the flag, the group's first member expands into
548
+ its parent list instead. The election is free — `project(parent)` is group-uniform because every
549
+ cluster of a group shares one consuming step, so member 0 reaches the same answer as the rest and
550
+ the rest defer to it with nothing communicated. Each child is one entry of the parent rows, and
551
+ it claims the page it resolves to when it is *executed* rather than when produced: a group with
552
+ several parents would otherwise make its producer claim each, count only the ones it won, and
553
+ hand any lane a rule for building the k-th won page out of five numbers. One extra queue hop
554
+ costs less, on a path §6.7 measures at under one entry a frame.
555
+
556
+ **What it cost, and §7.6 needs to know.** The node metric's `max_parent_error` cull is a
557
+ **descent-only** accelerator and the flag turns it off. Its premise — a subtree nothing can be
558
+ drawn in is a subtree worth skipping — is exactly wrong for a walk up, where being below the cut
559
+ is what sends a group to its parents. With the cull on, a cut seeded at the leaves drew nothing
560
+ at all: the subtree carrying the whole walk was pruned at its first node, silently. A node has no
561
+ parent list of its own (the upward edge is per group), so there is nothing to coarsen from at
562
+ node granularity and entering the subtree is the only way to reach the groups that can.
563
+
564
+ So a page walked *up* through is evaluated whole, with no LOD pruning. That is affordable exactly
565
+ because §1's frontier means the walk is one or two levels rather than the depth of the DAG — it
566
+ is an argument for the frontier, not against the coarsen path, but it does mean the fallback in
567
+ §4 must stay a *descent* and never a walk up from below.
568
+ 6. The frontier: storage keyed by mesh row (§1.1), the reseeded seed pass, validation, the budget.
569
+ Behind a flag, with the root descent as the fallback path it already is.
570
+
571
+ **6a — per-view state. Done.** A view owns `VGeoViewCut` beside its HZB and its previous camera,
572
+ and that is where a frontier will live. It carries this view's params buffer, written at the seam
573
+ where the view is known. One buffer per view rather than offsets into a shared one: views are
574
+ few, a uniform is tens of bytes, and packing them buys nothing against giving every per-view
575
+ value a second index to be wrong in. `begin_frame` takes one destructured argument and no
576
+ projection, because a frame does not have one.
577
+
578
+ That fixed a live defect on its own. `projection_k` is screen height over field of view, and one
579
+ buffer on the runtime made it a property of the frame, written by whichever view opened it — so a
580
+ shadow cascade selected a level of detail for a screen it is not drawn to. Pinned: two views
581
+ cutting in one frame must disagree about it, must each get what their own height and lens imply,
582
+ and must not be reading the same bytes.
583
+
584
+ **6b — one cut per view. Not done, and it is the next thing.** §6.3 has the shape and the
585
+ precedent: phase one restructures into the phase-two order — a shared positive collection, one
586
+ static expansion, one cut, one meshlet filter, `graph_sort_meshlets_by_bucket_id`, then the
587
+ bucket loop for rasterization alone. It is a change to the main opaque path for *all* geometry
588
+ rather than to anything under `virtual/`, and it cannot be looked at: §7.4's note explains why no
589
+ browser on this machine currently draws. That combination is why it is written down here rather
590
+ than taken.
591
+
592
+ **6c — the frontier. Done, in its simplest form, and that form is not the one §1 describes.**
593
+
594
+ A cut records the **page rows it drew from** into a per-view bitmask, and
595
+ `shader_vgeo_cut_reseed` puts those rows back on the queue the next frame instead of node 0
596
+ of the root. Measured on the agreement spec's torus with the camera unmoved: **19 wavefront
597
+ generations become 4**, `DRAWN` identical at 161. The ladder is what a cut is paid in, so
598
+ that is the whole of why a cut is kept.
599
+
600
+ **Pages, not groups.** §1 asks for a set of groups; this is a set of page rows. A page is the
601
+ unit the claim is built on and the unit a visit evaluates whole, so a reseeded page needs no
602
+ new item kind, no entry format, no allocator and no dedup beyond the claim the descent
603
+ already takes — and a 4,096-row tier is 128 words a view against the 120–160 KB §5 sizes a
604
+ group-level frontier at. Every group of a reseeded page is re-tested by the pure rule, and
605
+ testing one the cut did not need costs the test.
606
+
607
+ **Recorded at the draw, not at the claim.** Recording at the claim names every page the
608
+ descent passed *through*, and reseeding from that set is more work than the root descent it
609
+ replaces. That was the first version, and the "fewer pages" test is what caught it. A page
610
+ that drew something is a page the cut is standing on.
611
+
612
+ **Items is not the meter.** The coarsen flag turns off the node metric's LOD cull (§7.5), so
613
+ a reseeded page is evaluated whole and tests slightly *more* clusters — 463 against 463 → 476
614
+ — while visiting far fewer pages. Pages are what a fetch and a claim cost; wavefronts are what
615
+ the ladder costs.
616
+
617
+ **It spends the traversal's last storage binding.** The engine requires ten per stage of an
618
+ adapter and refuses one that offers fewer; the compile spec now pins traverse at exactly ten
619
+ and says there is no eleventh. Anything that kernel needs next must displace something or be
620
+ folded into a buffer it already binds.
621
+
622
+ What it gives up, and what is left:
623
+
624
+ - **Per-instance keying (§1.1) is not there.** Two instances of one geometry share a page
625
+ set — a superset of each one's own, so correct — and the reseed offers every page to every
626
+ instance, one lane per (instance, page row). Cheap at a handful of instances; not what a
627
+ thousand should do. Keying by mesh row is the change that lifts it, and it is where a group
628
+ frontier and §5's sizing come back.
629
+ - **The seam captures the frontier and does not yet read it.** Choosing reseed over seed per
630
+ view, and dropping a frontier whose tier has evicted since it was written — the pool's
631
+ eviction count against the one it was captured at, which is a host-side integer compare and
632
+ needs none of §4's per-entry generation — is the next step.
633
+ - **No budget.** §3's bound on entries moved per frame is not implemented, and §6.7 is why it
634
+ can wait: under 2% of a frontier moves per frame at any orbit speed worth rendering, and
635
+ the failure mode of no budget is a frame that does more work, never a wrong cut.
636
+ 7. Delete nothing until a capture on the Zorah container shows the incremental path beating the root
637
+ descent on the same scene.