@woosh/meep-engine 3.26.0 → 3.28.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 (449) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +542 -0
  4. package/src/core/binary/lz4/LZ4_LEVEL.d.ts +33 -0
  5. package/src/core/binary/lz4/LZ4_LEVEL.d.ts.map +1 -0
  6. package/src/core/binary/lz4/LZ4_LEVEL.js +32 -0
  7. package/src/core/binary/lz4/lz4_compress_block_at_level.d.ts +33 -0
  8. package/src/core/binary/lz4/lz4_compress_block_at_level.d.ts.map +1 -0
  9. package/src/core/binary/lz4/lz4_compress_block_at_level.js +47 -0
  10. package/src/core/binary/lz4/lz4_compress_block_hc.d.ts +40 -0
  11. package/src/core/binary/lz4/lz4_compress_block_hc.d.ts.map +1 -0
  12. package/src/core/binary/lz4/lz4_compress_block_hc.js +1382 -0
  13. package/src/core/binary/lz4/lz4_decompress_block.d.ts +2 -1
  14. package/src/core/binary/lz4/lz4_decompress_block.d.ts.map +1 -1
  15. package/src/core/binary/lz4/lz4_decompress_block.js +31 -6
  16. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts +15 -0
  17. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts.map +1 -0
  18. package/src/core/geom/3d/mat4/m4_linear_determinant.js +24 -0
  19. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts +4 -2
  20. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts.map +1 -1
  21. package/src/core/geom/3d/mat4/m4_normal_matrix3.js +21 -11
  22. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +14 -7
  23. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  24. package/src/core/geom/3d/shape/HeightMapShape3D.js +87 -78
  25. package/src/engine/Engine.d.ts.map +1 -1
  26. package/src/engine/Engine.js +38 -1
  27. package/src/engine/graphics3/GraphicsEngine.d.ts +5 -1
  28. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  29. package/src/engine/graphics3/GraphicsEngine.js +834 -830
  30. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  31. package/src/shade/RENDERER_CONTRACT.md +19 -10
  32. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +1 -1
  33. package/src/shade/playground/vgeo_runtime/README.md +186 -27
  34. package/src/shade/playground/vgeo_runtime/index.html +112 -62
  35. package/src/shade/playground/vgeo_runtime/main.js +678 -88
  36. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +18 -0
  37. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -0
  38. package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.js +23 -5
  39. package/src/shade/playground/vgeo_scene/README.md +408 -0
  40. package/src/shade/playground/vgeo_scene/index.html +144 -0
  41. package/src/shade/playground/vgeo_scene/main.d.ts +2 -0
  42. package/src/shade/playground/vgeo_scene/main.d.ts.map +1 -0
  43. package/src/shade/playground/vgeo_scene/main.js +1716 -0
  44. package/src/shade/playground/vgeo_viewer/sample_asset.d.ts.map +1 -1
  45. package/src/shade/renderer/Renderer.d.ts +4 -1
  46. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  47. package/src/shade/renderer/Renderer.js +5 -2
  48. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +1 -2
  49. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts +11 -0
  50. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.d.ts.map +1 -1
  51. package/src/shade/renderer/geometry/bvh/GPUGeometryBVHManager.js +16 -8
  52. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.d.ts.map +1 -1
  53. package/src/shade/renderer/geometry/meshlet/GPUMeshletManager.js +16 -11
  54. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
  55. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +3 -29
  56. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts +22 -0
  57. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts.map +1 -0
  58. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.js +49 -0
  59. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +77 -7
  60. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +36 -23
  61. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +17 -14
  62. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +19 -1
  63. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  64. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +21 -1
  65. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.d.ts.map +1 -1
  66. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +5 -9
  67. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts +32 -0
  68. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts.map +1 -0
  69. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +59 -0
  70. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -0
  71. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts +44 -0
  72. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts.map +1 -0
  73. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +36 -0
  74. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -0
  75. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_levels.js → level/vgeo_build_levels.js} +61 -95
  76. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts +19 -0
  77. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts.map +1 -0
  78. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.js → level/vgeo_build_root_group.js} +1 -1
  79. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +73 -0
  80. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.js → mesh/VirtualGeometryMesh.js} +7 -7
  82. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +29 -0
  83. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -0
  84. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_normals.js → mesh/vgeo_compute_vertex_normals.js} +3 -3
  85. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +31 -0
  86. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -0
  87. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_tangents.js → mesh/vgeo_compute_vertex_tangents.js} +5 -5
  88. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +46 -0
  89. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -0
  90. package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.js → mesh/vgeo_normalize_source.js} +9 -9
  91. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts +13 -0
  92. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts.map +1 -0
  93. package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.js → page/vgeo_assemble_pages.js} +63 -16
  94. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts +13 -0
  95. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts.map +1 -0
  96. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.js → page/vgeo_build_page_nodes.js} +4 -4
  97. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts +22 -0
  98. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts.map +1 -0
  99. package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.js → page/vgeo_page_node_count.js} +2 -2
  100. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +52 -0
  101. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -0
  102. package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.js → partition/VGeoFaceGraph.js} +1 -1
  103. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts +14 -0
  104. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -0
  105. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts +29 -0
  106. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts.map +1 -0
  107. package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.js → partition/vgeo_partition_graph.js} +2 -2
  108. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts +41 -0
  109. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts.map +1 -0
  110. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.js +92 -0
  111. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts +10 -0
  112. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts.map +1 -0
  113. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.js → partition/vgeo_split_face_set.js} +12 -55
  114. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts +15 -0
  115. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -0
  116. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set_packed.js → partition/vgeo_split_face_set_packed.js} +3 -3
  117. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts +12 -0
  118. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts.map +1 -0
  119. package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.js → sphere/vgeo_bounding_sphere.js} +3 -3
  120. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts +18 -0
  121. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts.map +1 -0
  122. package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.js → sphere/vgeo_containing_sphere.js} +3 -3
  123. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts +9 -0
  124. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts.map +1 -0
  125. package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.js → sphere/vgeo_sphere_inflate_to_contain.js} +1 -1
  126. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +24 -0
  127. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -0
  128. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +20 -0
  129. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -0
  130. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +14 -0
  131. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -0
  132. package/src/shade/renderer/geometry/virtual/build/{vgeo_extract_submesh.js → submesh/vgeo_extract_submesh.js} +5 -5
  133. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +12 -0
  134. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -0
  135. package/src/shade/renderer/geometry/virtual/build/{vgeo_gather_faces.js → submesh/vgeo_gather_faces.js} +2 -2
  136. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts +11 -0
  137. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts.map +1 -0
  138. package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.js → submesh/vgeo_narrow_indices.js} +3 -3
  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_build.d.ts.map +1 -1
  141. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +10 -11
  142. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts +22 -0
  143. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.d.ts.map +1 -1
  144. package/src/shade/renderer/geometry/virtual/format/VGeoWriteOptions.js +24 -0
  145. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts +8 -2
  146. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts.map +1 -1
  147. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +8 -2
  148. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts +12 -0
  149. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.d.ts.map +1 -1
  150. package/src/shade/renderer/geometry/virtual/format/dag/VirtualGeometryCluster.js +13 -0
  151. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts +8 -1
  152. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.d.ts.map +1 -1
  153. package/src/shade/renderer/geometry/virtual/format/frame/vgeo_encode_frame_blob.js +11 -3
  154. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts +9 -0
  155. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.d.ts.map +1 -1
  156. package/src/shade/renderer/geometry/virtual/format/header/VGEO_FORMAT_VERSION.js +10 -1
  157. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts +15 -0
  158. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts.map +1 -0
  159. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.js +15 -0
  160. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.d.ts +18 -0
  161. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.d.ts.map +1 -0
  162. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_ENTRY_COUNT.js +17 -0
  163. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.d.ts +8 -0
  164. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.d.ts.map +1 -0
  165. package/src/shade/renderer/geometry/virtual/format/page/VGEO_MAX_PARENT_REF.js +7 -0
  166. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.d.ts +3 -0
  167. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PAGE_OFFSET.js +16 -0
  168. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.d.ts +14 -0
  169. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.d.ts.map +1 -0
  170. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_ENTRY_SIZE.js +13 -0
  171. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.d.ts +11 -0
  172. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.d.ts.map +1 -0
  173. package/src/shade/renderer/geometry/virtual/format/page/VGEO_PARENT_PAGE_ENTRY_SIZE.js +10 -0
  174. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +28 -0
  175. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  176. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +7 -3
  177. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts +20 -0
  178. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.d.ts.map +1 -1
  179. package/src/shade/renderer/geometry/virtual/format/read/VGeoByteSource.js +20 -2
  180. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts +4 -0
  181. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.d.ts.map +1 -1
  182. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerHeader.js +4 -0
  183. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts +29 -3
  184. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.d.ts.map +1 -1
  185. package/src/shade/renderer/geometry/virtual/format/read/VGeoContainerReader.js +81 -122
  186. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts +40 -0
  187. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.d.ts.map +1 -1
  188. package/src/shade/renderer/geometry/virtual/format/read/VGeoPage.js +44 -0
  189. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts +69 -0
  190. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.d.ts.map +1 -0
  191. package/src/shade/renderer/geometry/virtual/format/read/VGeoPageFetcherCore.js +236 -0
  192. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts +14 -0
  193. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.d.ts.map +1 -1
  194. package/src/shade/renderer/geometry/virtual/format/read/VGeoReadOptions.js +15 -0
  195. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.d.ts +31 -0
  196. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.d.ts.map +1 -0
  197. package/src/shade/renderer/geometry/virtual/format/read/vgeo_byte_source_from_descriptor.js +32 -0
  198. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts +10 -0
  199. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.d.ts.map +1 -1
  200. package/src/shade/renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js +21 -1
  201. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.d.ts +15 -0
  202. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.d.ts.map +1 -0
  203. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_transfer.js +84 -0
  204. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts +22 -0
  205. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.d.ts.map +1 -0
  206. package/src/shade/renderer/geometry/virtual/format/read/vgeo_page_worker_serve.js +96 -0
  207. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  208. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +63 -3
  209. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  210. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +219 -17
  211. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  212. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +156 -11
  213. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +656 -0
  214. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts +33 -36
  215. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.d.ts.map +1 -1
  216. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryPool.js +109 -193
  217. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +109 -29
  218. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  219. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +173 -105
  220. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +37 -0
  221. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +109 -26
  222. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +548 -0
  223. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts +29 -50
  224. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  225. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +55 -487
  226. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts +84 -0
  227. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.d.ts.map +1 -0
  228. package/src/shade/renderer/geometry/virtual/runtime/VGeoPageFetcher.js +393 -0
  229. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts +234 -0
  230. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -0
  231. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +1388 -0
  232. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.d.ts +81 -0
  233. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.d.ts.map +1 -0
  234. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidencyOptions.js +89 -0
  235. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts +53 -34
  236. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.d.ts.map +1 -1
  237. package/src/shade/renderer/geometry/virtual/runtime/VGeoRuntimeTables.js +407 -124
  238. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts +100 -0
  239. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.d.ts.map +1 -0
  240. package/src/shade/renderer/geometry/virtual/runtime/VGeoViewCut.js +219 -0
  241. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.d.ts +17 -0
  242. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.d.ts.map +1 -0
  243. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_EXPAND.js +47 -0
  244. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.d.ts +20 -0
  245. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.d.ts.map +1 -0
  246. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_FLAG_COARSEN.js +19 -0
  247. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.d.ts.map +1 -1
  248. package/src/shade/renderer/geometry/virtual/runtime/cut/VGEO_CUT_PARAMS_STRUCT.js +30 -12
  249. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +8 -8
  250. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  251. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +11 -73
  252. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts +19 -0
  253. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -0
  254. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +98 -0
  255. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts +31 -0
  256. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -0
  257. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +99 -0
  258. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts +17 -0
  259. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts.map +1 -0
  260. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.js +39 -0
  261. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.d.ts.map +1 -1
  262. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_collect.js +18 -1
  263. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.d.ts.map +1 -1
  264. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_prepare.js +5 -15
  265. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.d.ts +3 -0
  266. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.d.ts.map +1 -0
  267. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_queue_remainder.js +77 -0
  268. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts +3 -0
  269. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -0
  270. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +130 -0
  271. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  272. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +32 -37
  273. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  274. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +576 -230
  275. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts +15 -18
  276. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.d.ts.map +1 -1
  277. package/src/shade/renderer/geometry/virtual/runtime/cut/vgeo_cut_feedback_layout.js +19 -28
  278. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.d.ts.map +1 -1
  279. package/src/shade/renderer/geometry/virtual/runtime/graph_vgeo_expand_instances_to_meshlets.js +45 -25
  280. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.d.ts +1 -0
  281. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js +11 -0
  282. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT_COUNT.js +1 -1
  283. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.d.ts +0 -1
  284. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_FEEDBACK_OFFSET.js +12 -10
  285. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.d.ts +1 -0
  286. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_OFFSET.js +8 -0
  287. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts +7 -2
  288. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.d.ts.map +1 -1
  289. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_GROUP_ROW_WORDS.js +8 -3
  290. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.d.ts +2 -0
  291. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_OFFSET.js +19 -0
  292. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.d.ts.map +1 -1
  293. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PAGE_ROW_WORDS.js +1 -1
  294. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.d.ts +5 -0
  295. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.d.ts.map +1 -0
  296. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_OFFSET.js +22 -0
  297. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.d.ts +14 -0
  298. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.d.ts.map +1 -0
  299. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_PARENT_ROW_WORDS.js +13 -0
  300. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.d.ts +17 -0
  301. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.d.ts.map +1 -0
  302. package/src/shade/renderer/geometry/virtual/runtime/layout/VGEO_QUEUE_TARGET_PARENT_BIT.js +16 -0
  303. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts +15 -0
  304. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.d.ts.map +1 -0
  305. package/src/shade/renderer/geometry/virtual/runtime/make_vgeo_page_worker.js +16 -0
  306. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.d.ts +23 -0
  307. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.d.ts.map +1 -0
  308. package/src/shade/renderer/geometry/virtual/runtime/vgeo_frontier_words.js +28 -0
  309. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.d.ts +2 -0
  310. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.d.ts.map +1 -0
  311. package/src/shade/renderer/geometry/virtual/runtime/vgeo_page.worker.js +13 -0
  312. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts +10 -5
  313. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.d.ts.map +1 -1
  314. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_page_block_words.js +15 -6
  315. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts +6 -1
  316. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.d.ts.map +1 -1
  317. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_write_page_block.js +38 -1
  318. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.d.ts.map +1 -1
  319. package/src/shade/renderer/global_illumination/sharc/shader/shader_sharc_evict.js +4 -3
  320. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  321. package/src/shade/renderer/loader/gltf/load_gltf.js +0 -19
  322. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  323. package/src/shade/renderer/material/chunk_material_draw_side.d.ts +14 -0
  324. package/src/shade/renderer/material/chunk_material_draw_side.d.ts.map +1 -0
  325. package/src/shade/renderer/material/chunk_material_draw_side.js +34 -0
  326. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  327. package/src/shade/renderer/material/standard/fragment_gbuffer.js +8 -1
  328. package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
  329. package/src/shade/renderer/particles/shaders/shader_particle_emit.js +6 -3
  330. package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
  331. package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +6 -3
  332. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  333. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +13 -1
  334. package/src/shade/renderer/postprocess/nss/NSS.d.ts +1 -1
  335. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts +19 -5
  336. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
  337. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +23 -5
  338. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.d.ts.map +1 -1
  339. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.js +12 -5
  340. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.d.ts.map +1 -1
  341. package/src/shade/renderer/rasterize/bucket/prepare_meshlet_draw_commands_by_material.js +12 -4
  342. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +22 -0
  343. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts.map +1 -0
  344. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -0
  345. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts +2 -0
  346. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.d.ts.map +1 -1
  347. package/src/shade/renderer/rasterize/bucket/shader_read_out_bucket.js +58 -57
  348. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +15 -0
  349. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -0
  350. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +35 -0
  351. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.d.ts.map +1 -1
  352. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_mesh_filter_2way.js +3 -5
  353. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.d.ts.map +1 -1
  354. package/src/shade/renderer/rasterize/cull/hzb/shader_hzb_meshlet_filter_1way_full.js +3 -4
  355. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts +10 -1
  356. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts.map +1 -1
  357. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.js +38 -29
  358. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts +5 -0
  359. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts.map +1 -1
  360. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.js +63 -55
  361. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.d.ts.map +1 -1
  362. package/src/shade/renderer/rasterize/expand/bucket/meshlet/shader_meshlets_extract_bucket.js +3 -4
  363. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.d.ts.map +1 -1
  364. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.js +1 -0
  365. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.d.ts.map +1 -1
  366. package/src/shade/renderer/rasterize/expand/pass0/pass_mesh_cull.js +7 -7
  367. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.d.ts.map +1 -1
  368. package/src/shade/renderer/rasterize/fast/graph_extract_meshes_from_bucket.js +83 -78
  369. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  370. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +44 -53
  371. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  372. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +6 -2
  373. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  374. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +6 -2
  375. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
  376. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +19 -2
  377. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +7 -4
  378. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  379. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +3 -2
  380. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.d.ts.map +1 -1
  381. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.js +15 -4
  382. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  383. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +16 -5
  384. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.d.ts.map +1 -1
  385. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +16 -5
  386. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  387. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +2 -1
  388. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.d.ts.map +1 -1
  389. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.js +4 -0
  390. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  391. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +16 -7
  392. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  393. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +85 -89
  394. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  395. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +28 -32
  396. package/src/shade/renderer/scene/GPUSceneContext.d.ts +22 -2
  397. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  398. package/src/shade/renderer/scene/GPUSceneContext.js +61 -4
  399. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +19 -0
  400. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts.map +1 -1
  401. package/src/shade/renderer/scene/NODE3D_STRUCT.js +34 -0
  402. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts +9 -0
  403. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts.map +1 -0
  404. package/src/shade/renderer/scene/chunk_node3d_flags.js +23 -0
  405. package/src/shade/renderer/scene/chunk_scene_database_access.d.ts.map +1 -1
  406. package/src/shade/renderer/scene/chunk_scene_database_access.js +15 -1
  407. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  408. package/src/shade/renderer/scene/rows/GPUSceneRows.js +9 -1
  409. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts +7 -0
  410. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts.map +1 -1
  411. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.js +16 -4
  412. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts +12 -0
  413. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts.map +1 -0
  414. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.js +18 -0
  415. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts +24 -0
  416. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts.map +1 -0
  417. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.js +33 -0
  418. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.d.ts.map +1 -1
  419. package/src/shade/renderer/shader/resource/ShaderResourceSetDescriptor.js +92 -16
  420. package/src/shade/renderer/view/GPUViewContext.d.ts +5 -0
  421. package/src/shade/renderer/view/GPUViewContext.d.ts.map +1 -1
  422. package/src/shade/renderer/view/GPUViewContext.js +21 -0
  423. package/src/shade/wgsl/emulator/WGSLLanguageCore.d.ts.map +1 -1
  424. package/src/shade/wgsl/emulator/WGSLLanguageCore.js +74 -3
  425. package/src/shade/playground/vgeo_viewer/README.md +0 -150
  426. package/src/shade/playground/vgeo_viewer/cut_geometry.js +0 -460
  427. package/src/shade/playground/vgeo_viewer/index.html +0 -77
  428. package/src/shade/playground/vgeo_viewer/main.js +0 -1343
  429. package/src/shade/playground/vgeo_viewer/select_cut.js +0 -481
  430. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +0 -258
  431. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +0 -1
  432. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.js +0 -723
  433. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +0 -49
  434. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +0 -1
  435. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.js +0 -50
  436. package/src/shade/renderer/loader/gltf/fix_up_material_sides.js +0 -109
  437. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.js +0 -78
  438. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +0 -100
  439. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.js +0 -26
  440. package/src/shade/renderer/shader/chunk/jobs/NOTES.md +0 -47
  441. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts +0 -3
  442. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts.map +0 -1
  443. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.js +0 -102
  444. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.js → level/VGeoLevel.js} +0 -0
  445. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.js → level/VGeoLevelOptions.js} +0 -0
  446. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.js → level/VGeoLevelSummary.js} +0 -0
  447. /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.js → partition/vgeo_graph_components.js} +0 -0
  448. /package/src/shade/renderer/geometry/virtual/build/{VGeoGatheredFaces.js → submesh/VGeoGatheredFaces.js} +0 -0
  449. /package/src/shade/renderer/geometry/virtual/build/{VGeoSubmesh.js → submesh/VGeoSubmesh.js} +0 -0
@@ -0,0 +1,548 @@
1
+ # Virtual Geometry runtime — residency that scales
2
+
3
+ **2026-09-14, revision 2** after a three-reviewer pass (scaling, correctness, integration; 56
4
+ findings, folded in below — the ones that changed the design are marked *(rev 2)*). Supersedes
5
+ `RUNTIME_PLAN.md` §3.2 (selectability), §3.3 (budgets and eviction), the touch half of §4.5, and
6
+ the whole of `virtual/residency/`. Everything else in that document stands: the slot layout, the
7
+ claim, the cut rule and its proofs, the seam.
8
+
9
+ ## 0. Why
10
+
11
+ The runtime was written for one asset and it scans instead of indexing. On the largest converted
12
+ Zorah container (`bdf6f961…`, 17,172 pages, 27.96 M source triangles, 920 MB) in
13
+ `playground/vgeo_runtime/` at 4,096 slots, a DevTools trace of zooming in and out shows this:
14
+
15
+ | | |
16
+ |---|---:|
17
+ | trace length | 5.5 s |
18
+ | main-thread tasks over 16 ms | 24 |
19
+ | longest task | 637 ms |
20
+ | of which `VGeoFeedbackLoop.#apply` | 575 ms |
21
+ | of which `VGeoRuntimeTables.refresh_selectable` | 450 ms |
22
+ | of which `GPUVirtualGeometryPool.#make_room` | 254 ms |
23
+
24
+ and the panel, zoomed well in, reads *refused 4,166,982 · pages visited 4,044 of 4,096 resident ·
25
+ clamped 1,334 · drawn 11,597* while the surface on screen is visibly the coarse level.
26
+
27
+ Three defects, each measured on the real asset in node (`scratchpad/measure.mjs`,
28
+ `decode.mjs`, `installpath.mjs` of the diagnosing session):
29
+
30
+ 1. **Selectability is recomputed over the whole geometry on every install and every evict.**
31
+ 4.19 ms per call at 4,096 resident pages (138,115 clusters, 39,450 groups) through a JS `Map`
32
+ and `Set`. The table is full, so every install evicts first: one promotion is two calls,
33
+ 8.4 ms. The loop promotes up to 8 MB a frame, 136 pages of this asset, so the bound is over a
34
+ second of CPU in one frame. The 637 ms task is about 68 promotions.
35
+ 2. **Eviction is a scan.** `#coldest` walks the touched map once per install; a refused install
36
+ costs the full walk, 0.55 ms at 4,096 rows, and `#apply` keeps asking for every RAM-resident
37
+ want after the first refusal even though nothing can change until the next readback. Refusals
38
+ alone were 850 ms of the 5.5 s. `VGeoResidencyManager.evict_to_budget` is the same defect in the
39
+ RAM tier: `#candidates()` builds and sorts an array of every resident page of every reader,
40
+ every time the budget binds. And `GPUMeshletManager.remove_batch` is `indexOf` + `splice` over
41
+ every batch record in the manager, so each eviction also pays a scan of every resident page and
42
+ every static batch *(rev 2)*.
43
+ 3. **The cut poisons its own LRU.** `vg_claim_page` stamps a child page the moment a parent
44
+ cluster refines into it; the frustum test runs afterwards on the child's *nodes*. Clusters are
45
+ never frustum-tested and projected error is a function of distance alone, so zoomed in on a
46
+ wall every cluster of the wall at that level refines whether it is on screen or not, and
47
+ claims — touches — its child page. 4,044 of 4,096 rows touched per frame; 3,081 of them hold a
48
+ resident child and are structurally unevictable; the rest are inside the protect window;
49
+ `#coldest` answers −1; the 858 pages in RAM and not in VRAM never get in. `drawn` is counted at
50
+ `vg_draw`, before `filter meshlets 2 way`, which is why 11,597 clusters do not look like 1.4 M
51
+ triangles: most are culled downstream.
52
+
53
+ And a fourth, not in the trace because the trace was mostly refusals, that bounds what any fix can
54
+ reach: **frame verification costs 1.0 ms per page.** Install path measured at 1.098 ms/page with
55
+ `verify_frames` on and 0.134 ms/page with it off; decode alone is 0.079 ms. The identity-seeded
56
+ xxh3 over a 56 KB frame runs at ~58 MB/s in JS. The playground turns verification on, and a
57
+ runtime should: a checksum is the one defence against a range that came back wrong. So the
58
+ per-page main-thread cost is the checksum, not the decode, and that is what the worker is for.
59
+
60
+ **The bar.** Nothing on the CPU may scale with what is resident, and nothing per frame may scale
61
+ with what was seen. Per-frame work is proportional to the readback the host chose to read, to the
62
+ pages that moved, and to stated per-reading budgets. That has to hold at 50,000 pages in RAM (3 GB
63
+ at this asset's 60 KB decoded per page) and 65,536 slots in VRAM, across every registered
64
+ geometry, with the same code.
65
+
66
+ Two things the bar does *not* cover, and which this document leaves where they are (§8): the
67
+ claim table, which is instances × slots in bits per expansion site and refuses instances past
68
+ `visible_instance_hint` rather than slowing down; and the lookup, whose word count is a
69
+ host-sized cap on pages across every registered geometry.
70
+
71
+ ## 1. What the CPU does in a frame, and the order of each step
72
+
73
+ | step | work | order |
74
+ |---|---|---:|
75
+ | read back | header + touch bits + `want_capacity` records | O(capacity/32 + want_capacity) bytes |
76
+ | touches | fold the bit vector into `last_used[row]`, checking each set bit's row identity (§2.4) | O(capacity/32) word loads + O(rows touched) |
77
+ | wants | partial select of the top records by priority out of at most `want_capacity` | O(want_capacity) |
78
+ | promote | up to `max_promotions_per_reading` pages from RAM: a slot write, a meshlet batch, the incremental cascade | O(promotions · clusters per page + cascade) |
79
+ | fetch | up to `max_fetches_per_reading` pages, each admitted against the RAM budget first, handed to the fetcher | O(fetches) |
80
+ | VRAM room | CLOCK hand over rows, under `max_eviction_tests_per_reading` | O(budget) |
81
+ | RAM room | CLOCK hand over RAM slots, under the same kind of budget | O(budget) |
82
+ | upload | the dirty ranges of the pages that moved | O(pages moved) |
83
+ | fetch/verify/decode | off the main thread | 0 on the main thread |
84
+
85
+ Nothing in the table is a Map of objects, nothing is a closure over the resident set, and every
86
+ row that is not proportional to the readback is proportional to a number in
87
+ `VGeoResidencyOptions`.
88
+
89
+ ## 2. The cut
90
+
91
+ ### 2.1 Frustum-test the cluster before it refines
92
+
93
+ `vg_visit_group` gains one test at the top of the per-cluster loop, under the same
94
+ `VGEO_CUT_FLAG_CULL` bit the node test uses: a cluster whose `SELF_SPHERE` is outside the frustum
95
+ is skipped — not drawn, not clamped, not refined, and above all not claiming its child page — and
96
+ counted in a new `VGEO_CUT_STAT.CULLED_CLUSTER` *(rev 2)*, beside the node counter.
97
+
98
+ `SELF_SPHERE` is the right sphere and no new row data is needed. FORMAT §7: `parent_sphere ⊇
99
+ self_sphere`, the node sphere is the union of the `parent_sphere`s under it, and RUNTIME_PLAN §4.4's
100
+ containment argument for the node test is built from exactly that. The tight `bounds_sphere`
101
+ exists in the format and is *not* in the row; if the looseness of the LOD sphere shows up as visited
102
+ pages it can be added later at four words per cluster row.
103
+
104
+ **Why this keeps §4.4's proofs** *(rev 2)*. Every parent cluster of one group was created by the
105
+ same step and shares one `self_sphere` (FORMAT §7, "shared per group step"), so the per-cluster
106
+ frustum decision is uniform across all of a group's parents, across pages. Either every parent of
107
+ a group refines into it or none does; a culled parent is not drawn, and it would not have been
108
+ drawn anyway (it fails `project(self) ≤ τ`). Culling removes visits and nothing else, the claim
109
+ is untouched, and "no cluster is drawn twice" holds unchanged.
110
+
111
+ The claim stays RUNTIME_PLAN §4.4's, at cluster granularity: turning culling on removes only
112
+ clusters whose `self_sphere` is outside the frustum, and never one inside it. The agreement spec's
113
+ existing test (`frustum culling removes only what is outside the frustum`) checks exactly that
114
+ against the records. The assertion it gains is **no page other than the root is claimed unless
115
+ some cluster refining into it has a `self_sphere` intersecting the frustum** *(rev 2)* — not "no
116
+ page whose every cluster is outside", which the kernel cannot promise: the claim is taken at the
117
+ parent, before the child's rows are seen, and a parent sphere can straddle a plane with every
118
+ child sphere on the far side of it. The seed claims and touches the root unconditionally.
119
+
120
+ The qualifier §4.4 wrote for the node test applies unchanged: a culled cluster's subtree is a hole
121
+ inside geometry the frustum rejected, and the parent that would stand in is not drawn either.
122
+ That is the deal the node test already makes.
123
+
124
+ ### 2.2 A touch is one bit
125
+
126
+ The touch list, its capacity and `TOUCH_COUNT` go. `vg_claim_page` and the seed do one
127
+ `atomicOr` into a bit vector of `page_row_capacity` bits at the head of the feedback buffer; the
128
+ per-row stamp words they used to `atomicExchange` on go with the list. Seed touches iff it
129
+ claimed; traverse touches iff the claim was won; no path touches without claiming or claims
130
+ without touching, so one bit per row is a faithful replacement.
131
+
132
+ What this buys: the readback of touches is `capacity / 8` bytes rather than up to 32 KB of list,
133
+ and it cannot overflow — with the list, a frame that touched more than 8,192 rows took the stamp
134
+ and lost the list entry, and the LRU treated pages on screen as cold.
135
+
136
+ ### 2.3 The readback reads the lists, and the want stamps stop being a transient
137
+
138
+ The frame's feedback transient becomes **header, touch bits, want records** and nothing else, so
139
+ `graph_read_buffer` reads the whole of it. At 4,096 slots and 2,048 wants that is 33 KB a frame;
140
+ today's layout is 344 KB, of which 256 KB are the want stamps — one word per lookup word, one per
141
+ page of every registered geometry — which the CPU never looks at.
142
+
143
+ The want stamps move out of the transient into a **persistent pool buffer** the size of the lookup
144
+ *(rev 2)*, and become frame-tagged: `vg_want` dedups with `atomicExchange(stamp, tag) != tag`
145
+ where `tag` is the runtime's frame number plus one, carried in the params. No per-frame clear:
146
+ the transient was `BufferResourceDescriptor.empty`, which is a clear over its full size every
147
+ frame, and a lookup-sized clear per frame is the one term in the old layout that scaled with
148
+ every registered geometry rather than with what was seen. The traverse binds one more storage
149
+ buffer, nine against the engine's floor of ten.
150
+
151
+ The want record is unchanged: `(source_row, child_ref, child_page_index, priority)`. The source
152
+ page is guaranteed to be in RAM when the want is serviced, which is §4.3 below, so the byte range
153
+ still comes out of its child table and no directory is needed.
154
+
155
+ `unregister` marks the geometry's own lookup range dirty, not the whole lookup *(rev 2)*.
156
+
157
+ ### 2.4 Identity without a snapshot
158
+
159
+ `#snapshot_generations` copies one word per row per readback. It goes. The pool records
160
+ `visible_from[row]` *(rev 2)*: the frame number of the flush that will publish the row's current
161
+ occupant. The pool's frame counter advances in `begin_frame` **before** the flush; an install
162
+ stamps `visible_from = frame + 1`; an evict stamps `0xFFFFFFFF`. A record from the cut of frame
163
+ `F` naming row `r` is valid iff `visible_from[r] <= F`. That is exact at every interleaving the
164
+ frame permits: an install landing after `begin_frame(F)`'s flush — a readback applied mid-frame, a
165
+ host registering a container inside its frame — is stamped `F + 1` and rejected by `F`'s reading;
166
+ one landing before it is stamped `F` and accepted; a freed row is rejected by every reading. Same
167
+ question as comparing generations, O(1) per record, and the touch fold applies it per set bit.
168
+ `generation_of` stays as the row's identity for anything else that needs it.
169
+
170
+ ### 2.5 Nothing else moves
171
+
172
+ The claim, the queue, the wavefront, the seed's instance handling, the collect pass and the
173
+ expansion seam are untouched. The arena stays read-only in every cut pass; the cut writes into the
174
+ frame's feedback transient and the want-stamp buffer and nowhere else.
175
+
176
+ ## 3. Selectability is maintained, not recomputed
177
+
178
+ Design §6's rule, unchanged: a group is selectable iff every one of its parent-cluster references
179
+ is live — the referencing cluster's page resident *in this tier* and that cluster's own group
180
+ selectable. §6 also says how: "carried by a counter instead of dependency lists … activation
181
+ cascades". `VGeoContainerReader` does exactly that for the RAM tier already (`#activate_page`,
182
+ `#cascade_activate`, `#pending_refs`). The VRAM tier will do the same, over the arena.
183
+
184
+ **State.** The live-reference count lives in the group row's `FLAGS` word, bits 16–31; bit 0 is
185
+ the selectable flag the kernels read and mask, as now. `PARENT_COUNT` is a u16 in the format, so
186
+ the count fits exactly. The count is uploaded with the row whenever the row is dirty, and the
187
+ kernels never read it — so a count change **does not mark the row dirty**; only a flip of bit 0
188
+ does *(rev 2)*. No parallel array, nothing per group on the CPU that is not already in the shadow.
189
+
190
+ **Reverse index.** Per geometry, `referrers: Map<page_index, page_index[]>` — for each page
191
+ index, the RAM-resident pages whose clusters reference it *(rev 2: RAM-resident pages, not VRAM
192
+ rows, so one index serves both tiers; VRAM ⊆ RAM)*. Written when a page enters RAM from its own
193
+ `child_page_index` list, removed when it leaves RAM. Entries are short in practice (parents per
194
+ page is single digits on the assets measured; `group_parent_count` is a u16, so a page holding a
195
+ popular group can legally have more, and the cost below is linear in it). A lookup is O(1) and a
196
+ page's entry is O(parents). This is the structure design §6 calls "the reverse-reference index
197
+ for not-yet-installed pages", kept for resident ones too.
198
+
199
+ **Install `C` into VRAM.** Seed every group of `C` with the count of VRAM-resident parent clusters
200
+ that reference it *and whose own group is selectable*: for each parent page `P` in `referrers[C]`
201
+ whose state is `VRAM`, walk `P`'s cluster rows and count those with `CHILD_PAGE_INDEX == C` by
202
+ child group. Same-page parents are not seeded; they arrive through the cascade (`SAME_PAGE` is in
203
+ no child table and the row carries the page's own index). Then Kahn: every group whose count
204
+ equals `PARENT_COUNT` (a root-level group's is 0) becomes selectable; for each of its clusters
205
+ whose child page is resident and whose `CHILD_GROUP` is inside the child page's group table — the
206
+ out-of-range guard `refresh_selectable` has today survives, because the count lives in the arena
207
+ and an unguarded increment would land in a cluster row *(rev 2)* — increment the child group's
208
+ count; a count that reaches its `PARENT_COUNT` is pushed. Cost is O(parents of `C` × clusters per
209
+ parent page) for the seed, plus the cascade. The cascade in one call is bounded by the resident
210
+ subtree under `C` that was waiting on `C` — small when pages install parent-first, and in every
211
+ case each group flips on once per residency, so the total over a page's lifetime is O(its
212
+ groups) *(rev 2)*.
213
+
214
+ **Evict `C` from VRAM.** Only a page with no VRAM-resident child page is evictable (the rule
215
+ below, unchanged), so no resident group depends on any group of `C`: nothing to decrement, nothing
216
+ to cascade. Drop the rows. `unregister` evicts everything of a geometry and skips the bookkeeping
217
+ entirely.
218
+
219
+ **Resident-child counts** *(rev 2)*. `resident_children` per VRAM row and per RAM slot replaces
220
+ the cluster walk `has_resident_child` does today. At install of `C` into a tier: for each parent
221
+ `P` in `referrers[C]` resident in that tier, `resident_children[P]++`; **and** `C`'s own count is
222
+ seeded from its child list, counting the children already resident in that tier — a parent that
223
+ arrives after its child (ordinary: a page is fetched through one parent while another parent page
224
+ is absent) would otherwise sit at zero, be evicted, and leave its child selectable with a parent
225
+ absent, which is C1 again. At evict, decrement each resident parent.
226
+
227
+ **On malformed counts.** A `PARENT_COUNT` larger than the references present is never reached and
228
+ the group never activates, as today. A count *smaller* than the references present flips the
229
+ group on when it is reached and leaves it on past it, as `refresh_selectable` does today; the
230
+ fixpoint oracle says *not selectable* for an over-counted group. That divergence exists now and
231
+ is preserved; the random-sequence spec runs on validator-accepted containers.
232
+
233
+ **Why not a per-frame vote in the traversal.** Considered and rejected: a group's parents refine
234
+ per *instance*, so a vote counter is per (instance, group), and that is instances × resident per
235
+ frame — the same product with the axes renamed. Selectability is instance-free, which is exactly
236
+ why it belongs in the row.
237
+
238
+ **Proof.** `refresh_selectable` does not survive as code. Its rule does, in the agreement spec's
239
+ `selectable_over` fixpoint oracle: a new `VGeoRuntimeTables.spec.js` drives random install/evict
240
+ sequences under the child rule, child-before-parent orders included, against the oracle after
241
+ every step, and the agreement spec's `vram_withheld` cases keep running against the incremental
242
+ flags. C1's regression stays pinned by the same oracle. `set_group_selectable` **stays** *(rev
243
+ 2)*: the agreement spec constructs the own-group-term state with it (P6, RUNTIME_PLAN §4.4's
244
+ "single most important line"), and its doc already says it is a spec verb the next cascade
245
+ overwrites; it writes bit 0 and leaves the count.
246
+
247
+ ## 4. One residency, two tiers, flat
248
+
249
+ `VGeoResidencyManager` and the policy half of `GPUVirtualGeometryPool` (`#owner`, `#touched`,
250
+ `#pinned`, `#make_room`, `#coldest`, `evictable`, `touch`, `install`'s room-making,
251
+ `register`'s root install) are replaced by one class, `VGeoResidency`, in `runtime/`. The pool
252
+ keeps the mechanics: tables, buffers, meshlet batches, dirty ranges, the frame counter and
253
+ `visible_from`, `register_geometry(reader)` / `unregister_geometry` for the row and the lookup,
254
+ and `install(reader, page)` / `evict(row)` as verbs that do one slot each and decide nothing —
255
+ `install` throws or returns −1 when there is no slot, and the caller made room or did not. The
256
+ reader keeps decode, its page map, and quarantine.
257
+
258
+ `GPUSceneContext.enable_virtual_geometry` constructs the pool, the runtime, **and the residency and
259
+ the feedback loop** *(rev 2)*, from an options object with a `residency` key rather than one
260
+ spread; the playground stops building any of them and wires only the two frame extensions. That
261
+ closes REVIEW_LEDGER U1: an engine host gets the loop by enabling virtual geometry.
262
+
263
+ ### 4.1 State
264
+
265
+ Per geometry, beside the lookup and the same length: `state: Uint32Array(total_pages)`. Values:
266
+ `ABSENT`, `FETCHING`, `QUARANTINED`, `RAM | slot`, `VRAM | row` — a tag in the top bits, an index
267
+ below. One word per page of every registered geometry, which is the size the lookup already is.
268
+ (The lookup itself stays GPU-shaped: row or `VGEO_PAGE_ROW_NONE`, nothing else, because the
269
+ kernels read it.)
270
+
271
+ VRAM rows, struct-of-arrays sized to `page_capacity`:
272
+
273
+ | array | what |
274
+ |---|---|
275
+ | `last_used` | frame of the last touch bit; **initialised to the install frame** *(rev 2)*, so a page promoted in this reading is inside the protect window and not this reading's next victim |
276
+ | `visible_from` | §2.4, owned by the pool |
277
+ | `resident_children` | count of VRAM-resident child pages (§3) |
278
+ | `pinned` | root pages, one bit |
279
+ | `bytes` | `DECODED_SIZE` mirror, so eviction refunds without reading the slot |
280
+
281
+ RAM slots, struct-of-arrays sized to `ram_slot_capacity`:
282
+
283
+ | array | what |
284
+ |---|---|
285
+ | `page` | the decoded `VGeoPage` (an object array; the one non-numeric column) |
286
+ | `geometry`, `page_index` | the key, for the CLOCK hand to find `state` |
287
+ | `last_in_vram` | the frame the page last left VRAM, or entered RAM |
288
+ | `bytes` | decoded size |
289
+
290
+ *(As built: no RAM-side `resident_children` and no second reverse index. The RAM hand checks a
291
+ candidate's own child list against the state words, O(children) per test, under the same test
292
+ budget — cheaper than an index kept on every install, and bounded by the same number.)*
293
+
294
+ Two `Map`s in the whole thing — `referrers` per geometry, and readers by geometry row — and
295
+ neither is walked per frame.
296
+
297
+ ### 4.2 Touches and last-use
298
+
299
+ The cut cannot touch a page it cannot see, so a page in RAM and not in VRAM has no touches; its
300
+ last use is the frame it left VRAM, written once at eviction, and it is protected by the same
301
+ window from that frame *(rev 2)*. The RAM tier's LRU therefore costs nothing per frame. The VRAM
302
+ tier's costs the fold of the bit vector. The clock both tiers read is the pool's frame, advanced in
303
+ `begin_frame` whether or not a readback is scheduled that frame — which is M12's requirement, and
304
+ it is met by the runtime rather than by the loop.
305
+
306
+ ### 4.3 Invariants, and who keeps them
307
+
308
+ - **VRAM ⊆ RAM.** The RAM CLOCK skips any slot whose `state` is `VRAM`. Today nothing enforces
309
+ this; the RAM tier can drop a page the cut is drawing from, after which the want that names it
310
+ as a source cannot be resolved and is silently skipped. With the invariant, a want's source page
311
+ is in RAM whenever the record is valid. The reader's public `evict`/`evict_all` stay for tools
312
+ and the viewer; a reader registered with a residency is the residency's, and driving its page
313
+ map from outside is documented as unsupported rather than reconciled per frame *(rev 2)*.
314
+ - **Parents before children, in each tier.** A row or slot with `resident_children > 0` is not
315
+ evictable.
316
+ - **The root is pinned** in both tiers; only `unregister` drops it (the reader's `evict_all`
317
+ keeps page 0 already).
318
+ - **Protect window.** A row is evictable only if `frame − max(last_used, install frame) ≥
319
+ protect_frames`; a RAM slot only if `frame − last_in_vram ≥ protect_frames`.
320
+
321
+ ### 4.4 Eviction is CLOCK, under a budget
322
+
323
+ Both tiers evict with a hand over their flat arrays: advance, test evictable (pinned, protect
324
+ window, children, byte or slot need), evict or skip. The hand persists across readings.
325
+
326
+ **The bound is a test budget, not a lap** *(rev 2)*. Under the child rule a cold leaf's eviction
327
+ makes its parent evictable, and the parent's row is behind the hand, so "one lap" is really one lap
328
+ per DAG level while the tier is full — and a lap is O(capacity), which the bar forbids per
329
+ reading. So a reading may run at most `max_eviction_tests_per_reading` tests of the hand across
330
+ both tiers; when the budget is spent the tier reports **nothing may go for this reading** and every
331
+ further promotion or admission in that reading is refused and counted. The hand carries on from
332
+ where it stopped next reading. A full tier therefore turns over at a rate the host chose, in
333
+ tests per reading, and never at a rate the resident count chose.
334
+
335
+ Not exact LRU, deliberately. The protect window already coarsens the order to "not recently", and
336
+ an exact order under 50,000 pages costs an update per touch per frame — the thing this document
337
+ exists to not do. `VTPhysicalCache` picks by linear scan at ≤ 1k slots; this is the same policy
338
+ made amortized so it holds at 65k.
339
+
340
+ ### 4.5 Servicing wants
341
+
342
+ Per reading: partial-select the top records by priority out of at most `want_capacity`, and walk
343
+ them in order until both per-reading budgets are spent. For each record, in this order *(rev 2)*:
344
+
345
+ 1. **stale** — `visible_from[source_row] > F`: drop.
346
+ 2. **already in VRAM** — the promotion happened one or two readings ago and the cut that emitted
347
+ this had not seen it: drop. (Any other order would mark a VRAM page `FETCHING`, fetch it, and on
348
+ arrival set it `RAM`, which the RAM CLOCK could then evict from under the cut.)
349
+ 3. **`FETCHING` or `QUARANTINED`**: drop. `FETCHING` is the cross-frame dedup — the GPU keeps
350
+ emitting the want while the page is missing, and the CPU drops it in O(1). `QUARANTINED` is what
351
+ stops a page that will not arrive from being fetched every reading; the reader's
352
+ `clear_quarantine` resets it to `ABSENT`.
353
+ 4. **`RAM`** — promote, under `max_promotions_per_reading` and the VRAM budgets, making room by
354
+ §4.4.
355
+ 5. **`ABSENT`** — admit against the RAM byte budget **before** the fetch, out of the record's
356
+ `decoded_size` and making room by §4.4 (FORMAT §10, and what `VGeoResidencyManager.#admit`
357
+ does today); if admitted and under `max_fetches_per_reading` and the fetch gate has room, mark
358
+ `FETCHING` and hand `(geometry, page_index, frame_offset, fetch_size, decoded_size, priority)`
359
+ to the fetcher; otherwise count and drop, and the next cut asks again.
360
+
361
+ Promotions and fetches have separate budgets so that a tier full of RAM-resident pages waiting to
362
+ be promoted is not starved by fetches or the reverse. Both are in **pages**, because each is a
363
+ cascade and a slot write and pages are near-uniform; `install_budget_bytes` goes.
364
+
365
+ The sort is not on the GPU. `want_capacity` bounds the list at 2,048 records, a partial select
366
+ over that is microseconds, and a `graph_radix_sort` over the capacity is twelve dispatches to
367
+ shave 28 KB off a readback. If `want_capacity` ever needs to grow past what a select can absorb,
368
+ the sort is the swap and the record shape already suits it: the priority is a positive float, so
369
+ its bit pattern is the key.
370
+
371
+ ### 4.6 Fetch, verify and decode off the main thread
372
+
373
+ Two classes *(rev 2)*:
374
+
375
+ - **`VGeoPageFetcherCore`** — pure logic over a byte source: read the range, retry with backoff
376
+ on the reader's `fetch_retries` / `fetch_retry_backoff_ms`, verify the frame against the asset
377
+ id, run `vgeo_read_page`, and answer either a page or a quarantine reason. It runs anywhere: in a
378
+ worker, or in-process. This is where the reader's `#fetch_page` / `#read_page` logic moves; the
379
+ reader's own `install` keeps calling it in-process, so tools and the viewer are unchanged.
380
+ - **`VGeoPageFetcher`** — a pool of **module workers**, spawned the way `makeClothWorker` does
381
+ (`new Worker(new URL('./vgeo_page.worker.js', import.meta.url), { type: 'module' })`, the form
382
+ the consumer's bundler detects), each running the core. `WorkerProxy` is not the shape: it spawns
383
+ classic workers from rollup bundles and posts without a transfer list. A `loopback` mode runs the
384
+ core in-process on the main thread's byte source, for specs (vitest's environment is `node` and
385
+ has no `Worker`) and for hosts that opt out.
386
+
387
+ A byte source has to be reconstructed inside a worker, per form: a URL source is rebuilt from
388
+ `{url, byte_length, init}` (one `HEAD` saved by passing the length); a `Blob`/`File` is posted
389
+ once per worker; an in-memory buffer is posted once per worker (a copy; a `SharedArrayBuffer`
390
+ would need COOP/COEP headers the dev server does not set). The reader's head prefix is not
391
+ consulted by a worker, so a page inside the head is re-fetched once — accepted, it is a few pages
392
+ per asset.
393
+
394
+ A page comes back as a plain object with ~30 typed arrays; `VGeoPage.transferable()` and
395
+ `VGeoPage.from_transfer()` carry it across with every buffer in the transfer list and restore the
396
+ class. The reader gains two verbs: `adopt(page)` — the success path, which puts a page decoded
397
+ elsewhere into its map — and `quarantine(page_index, reason)` — the failure path — so that
398
+ quarantine still lives in one place. A reply carries the request id, and a reply for a geometry
399
+ that was unregistered while it was in flight is dropped (geometry rows are recycled; the request
400
+ id is what tells two containers in one row apart).
401
+
402
+ The reader's own activation bookkeeping (`#activate_page`, `#pending_refs`, `is_selectable`) is
403
+ the RAM tier's answer to a question the runtime answers in the arena, and at 50,000 pages it is
404
+ tens of megabytes of nested Maps nobody reads. `VGeoReadOptions.track_selectability` *(rev 2)*
405
+ defaults to `true`; a residency opens its readers with it `false`, `adopt` then skips the cascade,
406
+ and `is_selectable` throws. The viewer keeps the default.
407
+
408
+ `ConcurrencyGate` stays as the cap across every geometry, in front of the workers; the residency
409
+ dispatches only while the gate has room, so the `FETCHING` population is bounded by the gate
410
+ plus one reading's `max_fetches_per_reading`. Failure is reported once per reading: if every fetch
411
+ a reading dispatched for one reader was quarantined, one line names the reader — the same fact
412
+ `failed_batch_count` reported per batch.
413
+
414
+ **The number that says this is required and not nice-to-have:** at 1.0 ms per verified page on
415
+ the main thread, 32 fetched pages a frame is 32 ms. Off the main thread it is the residency cost of
416
+ 32 arrivals, which is the cascade — tens of microseconds each.
417
+
418
+ ### 4.7 What the feedback loop becomes
419
+
420
+ `VGeoFeedbackLoop` keeps `read(graph)` — schedule the readback with the layout and frame closed
421
+ over, cap in flight, count skipped frames — and its `#apply` becomes one call into
422
+ `VGeoResidency.apply(words, layout, frame)`. The class stops owning fetch state, in-flight sets,
423
+ controllers, or budgets. `VGeoResidencyOptions` moves to `runtime/` and becomes the one place
424
+ every knob lives: `vram_budget_bytes`, `ram_budget_bytes`, `ram_slot_capacity`, `protect_frames`,
425
+ `max_promotions_per_reading`, `max_fetches_per_reading`, `max_eviction_tests_per_reading`,
426
+ `max_concurrent_fetches`, `worker_count`, `max_readbacks_in_flight`.
427
+
428
+ `VGeoResidency.register(reader)` registers the geometry with the pool, installs the root through
429
+ the ordinary promotion path (making room for it, or throwing the same error `pool.register`
430
+ throws today), and pins it in both tiers.
431
+
432
+ ## 5. What dies
433
+
434
+ | gone | replaced by |
435
+ |---|---|
436
+ | `VGeoRuntimeTables.refresh_selectable`, `#live_refs` | the incremental cascade in `install`/`evict` (§3); `set_group_selectable` stays as a spec verb |
437
+ | `VGeoRuntimeTables.has_resident_child` | `resident_children[row]` |
438
+ | pool `#owner`, `#touched`, `#pinned`, `#make_room`, `#coldest`, `evictable`, `touch`, `entry_of`, `budget_bytes`, `protect_window` | `VGeoResidency` |
439
+ | `VGeoResidencyManager`, `VGeoResidencyEntry`, `residency/` | `VGeoResidency`, `runtime/` |
440
+ | touch list, `touch_capacity`, `TOUCH_COUNT`, touch stamps | the touch bit vector |
441
+ | want stamps in the transient | the pool's frame-tagged stamp buffer |
442
+ | `#snapshot_generations`, `#still_the_same_page` | `visible_from[row]` |
443
+ | `install_budget_bytes` | `max_promotions_per_reading` |
444
+ | `#in_flight_pages`, `#controllers`, `#dispatch_frame`, `#dispatch_wants` in the loop | `state == FETCHING`, `VGeoPageFetcher` |
445
+ | `GPUMeshletManager.remove_batch`'s `indexOf` | a record → position index *(rev 2)* |
446
+ | the reader's `#fetch_page` / `#read_page` bodies | `VGeoPageFetcherCore`, which the reader calls in-process |
447
+
448
+ ## 6. Order, and what each step leaves green
449
+
450
+ Each step is one commit, pinned by a spec that was red before it, and the suite is green after it.
451
+ The headless drive on the Zorah asset runs after 1, 3 and 5.
452
+
453
+ | step | change | goes red first |
454
+ |---|---|---|
455
+ | 0 | `GPUMeshletManager.remove_batch` in O(1): a `Map` from record to position, or a swap-remove with the moved record's position updated *(rev 2)* | `GPUMeshletManager.spec.js`: removing one of 10,000 batches does not scale with the count (bounded by a measured ratio, interleaved as `paired-measurement-on-a-busy-machine` records) |
456
+ | 1 | §2: cluster frustum test + `CULLED_CLUSTER`; touch bits; transient = header + bits + wants; persistent frame-tagged want stamps; `visible_from` in the pool with the frame advanced in `begin_frame`; `read` reads the whole (smaller) transient | `vgeo_cut_agreement.spec.js`: the §2.1 assertion, and the seed/two-instance tests read bits instead of a list; `VGeoFeedbackLoop.spec.js`: a frame touching more rows than the old list held stamps every one of them; a readback's byte size has no stamp region in it; a record naming a row re-used since the cut is dropped, by `visible_from`, including a row re-used inside the reading's own frame; C5's layout test on a fixture crossing a 32-row boundary (the ten-page torus cannot move `want_base`; use the 160×80 one) |
457
+ | 2 | §3: incremental selectability and `resident_children` in the tables; `refresh_selectable` and `has_resident_child` deleted | new `VGeoRuntimeTables.spec.js`: 200 random install/evict steps under the child rule, child-before-parent orders included, agree with `selectable_over` after each; a parent installed after its child counts it; an out-of-range `CHILD_GROUP` is skipped; the agreement spec's withheld cases unchanged |
458
+ | 3 | §4.1–4.5, 4.7: `VGeoResidency`; pool loses policy; loop becomes `read` + one call; scene context constructs residency and loop; `residency/` deleted | new `VGeoResidency.spec.js` — see the migration table below |
459
+ | 4 | §4.6: `VGeoPageFetcherCore`, `VGeoPageFetcher`, `adopt`, `quarantine`, `track_selectability` | `VGeoPageFetcherCore.spec.js` in-process over an in-memory source: a page arrives decoded and verified; a bad checksum retries then quarantines once; abort abandons what is in flight; a reply for an unregistered geometry is dropped. The real worker is exercised at tier 4 only |
460
+ | 5 | proof (§7) | the numbers below, recorded in `playground/vgeo_runtime/README.md` |
461
+
462
+ Steps 0, 1 and 2 are independent of each other and of 3. Step 4 is independent of 2 but is
463
+ measured only with 3 in place.
464
+
465
+ ### 6.1 Where every existing pin goes *(rev 2)*
466
+
467
+ | today | pins | after |
468
+ |---|---|---|
469
+ | pool spec: `a page with a resident VRAM child may not go`, `a page with a VRAM child stays put after the RAM tier forgets it` | M9 | `VGeoResidency.spec.js`: same two, through `resident_children`, the second still calling `reader.evict` underneath |
470
+ | pool spec: `a budget too small … evicts the coldest and keeps drawing`, `a full slot table evicts …` | M17 | `VGeoResidency.spec.js`: promotion under a small byte budget and under a full table both make room by the CLOCK and keep the root |
471
+ | pool spec: `the bytes an eviction refunds …` | C4 | `VGeoResidency.spec.js`, through the `bytes` column, with `reader.evict` underneath |
472
+ | pool spec: `a page row is reused, and its generation says so` | M10 half | stays in the pool spec (`generation_of` stays) |
473
+ | pool spec: `a root page that cannot be made resident is a refusal …` | C7 host half | `VGeoResidency.spec.js`: `register` throws and rolls back when the root cannot be made resident |
474
+ | loop spec: `a touch moves both tiers' clocks` | design §6 | `VGeoResidency.spec.js`: a touch bit moves `last_used`; a VRAM eviction writes `last_in_vram` |
475
+ | loop spec: `the RAM tier's clock runs without the host winding it` | M12 | `VGeoResidency.spec.js`: the protect window is measured against the pool frame, which advances with `begin_frame` and not with readbacks |
476
+ | loop spec: `a readback landing after the layout moved …` | C5 | loop spec, on the larger fixture |
477
+ | loop spec: `a record naming a row that was re-used since the cut is dropped` | M10 | loop spec, by `visible_from`, at step 1 |
478
+ | loop spec: `a page still being fetched is not asked for again every frame` | M11 dedup | `VGeoResidency.spec.js`: `FETCHING` drops the record |
479
+ | loop spec: `a frame asks for no more pages than its limit …`, `the frame limit is one budget across every reader` | M11 limit | `VGeoResidency.spec.js`: `max_fetches_per_reading` across two readers |
480
+ | loop spec: `aborting the loop abandons what it started …` | M11 abort | `VGeoPageFetcherCore.spec.js` |
481
+ | loop spec: `a reader that cannot answer is reported once per batch …` | M11 report | `VGeoResidency.spec.js`: once per reading |
482
+ | loop spec: `a want for a page already in RAM is a VRAM promotion, not a fetch` | | `VGeoResidency.spec.js` |
483
+ | loop spec: `a saturated loop skips a frame …`, `a frame that was never opened …`, `the params a frame publishes …`, `a rasterization path on another graph …`, `the claim bitmask is sized …`, `a readback carries the counters …` | | loop spec, unchanged in substance |
484
+ | residency spec: `refuses a want it cannot make room for, and does not pay for it`, `makes room by evicting, then admits`, `stays inside the budget over a long tour`, `two wants for one page are one fetch and one charge`, `never drops the root page`, `drops nothing while a page has resident children`, `protects a page for as many frames as the feedback takes`, `drops the coldest touch first`, `says so rather than looping when nothing may be dropped`, `bounds the total, which no reader can`, `cost their own page and nothing else`, `are not asked for again until a caller with better information says so` | RAM tier | `VGeoResidency.spec.js`, each as a RAM-tier case; "coldest first" becomes "a colder slot goes before a hotter one the hand reaches later", which is what CLOCK promises |
485
+ | residency spec: `a reader that installed behind the manager's back is reconciled once a frame` | | dropped with a stated reason (§4.3: a registered reader is the residency's) |
486
+ | agreement spec: everything | C1, M1–M8, P2, P6 | unchanged, `residency_to_tables` loses its `refresh_selectable` call |
487
+ | `graph_vgeo_expand.spec.js` | seam | unchanged; it reads no layout and is not red at any step |
488
+
489
+ ## 7. Proof — measured 2026-09-14, every step landed
490
+
491
+ In node, on the real asset (`measure2.mjs` / `measure3.mjs` of the building session):
492
+
493
+ | | before | after |
494
+ |---|---:|---:|
495
+ | one promotion at a full 4,096-slot tier, eviction and activation included | 8.4 ms | 9.8 µs |
496
+ | the same at 65,536 slots with 16,000 pages resident | — | 13.3 µs |
497
+ | one refused promotion | 0.55 ms | 0 after the reading's test budget is spent |
498
+ | readback bytes per frame at 4,096 slots | 344 KB | 33 KB |
499
+ | per-frame GPU clear proportional to the lookup | 256 KB | 0 |
500
+ | frame verification and decode on the main thread | 1.1 ms per page | 0 |
501
+
502
+ Sixteen times the slots and four times the residency moved the per-operation cost by a third,
503
+ which is the cascade reaching deeper pages and not the tier's size. That is the scaling claim.
504
+
505
+ In headless Chrome on the same asset through `playground/vgeo_runtime/` at 4,096 slots and
506
+ 1258×646, a tour of eight rosettes held five seconds each, twice round, then the rosette of the
507
+ diagnosing screenshot at rest (`tour.mjs`; the table is `tour_out/rows.json`):
508
+
509
+ | | before (the diagnosing trace) | after |
510
+ |---|---:|---:|
511
+ | main-thread tasks over 50 ms | 24 in 5.5 s, longest 637 ms | 4 in 150 s, longest 115 ms — every one on a meshlet-pool compaction |
512
+ | `refused` over the run | 4,166,982 | 999, while the tier turned over 3,009 pages |
513
+ | `clamped` at rest, zoomed in | 1,334 | 0 |
514
+ | `pages visited` zoomed in, of 4,096 resident | 4,044 | 2,000–3,200 |
515
+ | frame time zoomed in | — | p50 8.3 ms, p95 12.6 ms |
516
+ | fetches on workers | 0 | 4,887 of 4,887 |
517
+
518
+ The picture at rest, zoomed in, is the fine level (`tour_out/rosette.png`).
519
+
520
+ **What the tour leaves open, in order.** The four remaining long tasks are
521
+ `GPUMeshletManager.compact()`, fired by a refused allocation with free space behind it — §8's
522
+ deferred item, and now the whole of the hitch. `pages visited` is still half to three quarters of
523
+ the resident set when zoomed in, because `self_sphere` is the LOD sphere and loose (§2.1); the
524
+ tight `bounds_sphere` is four words per cluster row and the next thing to measure. And under a
525
+ vite dev server the first seconds after a container opens stall while four worker module graphs
526
+ are served, which a bundled build does not pay.
527
+
528
+ ## 8. What this does not do
529
+
530
+ - **No GPU sort of the wants** (§4.5). The list is capped, and a CPU partial select is cheaper
531
+ than the dispatches.
532
+ - **No GPU-nominated eviction.** A device pass could sort rows by stamp and hand back the coldest
533
+ `K`, but CLOCK under a test budget needs no readback beyond the bits.
534
+ - **No exact LRU** (§4.4).
535
+ - **No directory read, no byte ranges in the want record.** The parent stays in RAM (§4.3), so
536
+ its child table stays the source of the range.
537
+ - **No change to the claim table.** It is instances × slots in *bits* per site, cleared per site,
538
+ and an instance past `visible_instance_hint` is refused by the seed (`INSTANCES_DROPPED`) rather
539
+ than slowed down — a correctness cliff the host sizes around. Sparse claiming is its own change.
540
+ - **No `lookup_words` growth.** The 65,536-word default is a cap on pages across every registered
541
+ geometry; `VGeoResidencyOptions` states it so a host sizes it. With the want stamps persistent
542
+ and frame-tagged (§2.3) nothing per frame scales with it any more; growing it is a new buffer.
543
+ - **No compaction fix in `GPUMeshletManager`.** RUNTIME_PLAN §9 already defers it. Under VG churn
544
+ a refused allocation with free space behind it triggers a rebuild that relocates every batch and
545
+ fires `changed` per page, which is O(resident) in one event. The drive reports how often it
546
+ fires (§7); if it is not zero at rest, VG pages need their own region of the pool, and that is
547
+ the next document.
548
+ - **No prefetch, no hysteresis, no disk tier.** Design §6's open items stay open.