@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
@@ -3,6 +3,7 @@ import { WebGPUArray } from "../../../../shader/type/WebGPUArray.js";
3
3
  import { CodeChunk } from "../../../../shader/compiler/CodeChunk.js";
4
4
  import { ComputeShader } from "../../../../shader/ComputeShader.js";
5
5
  import { ShaderResourceSetDescriptor } from "../../../../shader/resource/ShaderResourceSetDescriptor.js";
6
+ import { WebGPUExtensionType } from "../../../../../descriptor/WebGPUExtensionType.js";
6
7
  import { COLLECTION_U32_STRUCT_READ } from "../../../../gpu_primitive/COLLECTION_U32_STRUCT_READ.js";
7
8
  import { COLLECTION_U32_STRUCT_WRITE } from "../../../../gpu_primitive/COLLECTION_U32_STRUCT_WRITE.js";
8
9
  import { MESHLET_COLLECTION_WRITE_STRUCT } from "../../../../rasterize/MESHLET_COLLECTION_WRITE_STRUCT.js";
@@ -17,16 +18,20 @@ import { VGEO_NODE_ROW_OFFSET } from "../layout/VGEO_NODE_ROW_OFFSET.js";
17
18
  import { VGEO_NO_CHILD_PAGE } from "../layout/VGEO_NO_CHILD_PAGE.js";
18
19
  import { VGEO_PAGE_ROW_NONE } from "../layout/VGEO_PAGE_ROW_NONE.js";
19
20
  import { VGEO_PAGE_ROW_OFFSET } from "../layout/VGEO_PAGE_ROW_OFFSET.js";
21
+ import { VGEO_PARENT_ROW_OFFSET } from "../layout/VGEO_PARENT_ROW_OFFSET.js";
22
+ import { VGEO_QUEUE_TARGET_PARENT_BIT } from "../layout/VGEO_QUEUE_TARGET_PARENT_BIT.js";
20
23
  import { VGEO_QUEUE_TARGET_GROUP_BIT } from "../layout/VGEO_QUEUE_TARGET_GROUP_BIT.js";
21
24
  import { VGEO_WANT_WORDS } from "../layout/VGEO_WANT_WORDS.js";
22
25
  import { chunk_vgeo_cut_access } from "./chunk_vgeo_cut_access.js";
26
+ import { chunk_vgeo_cut_claim } from "./chunk_vgeo_cut_claim.js";
27
+ import { VGEO_CUT_EXPAND } from "./VGEO_CUT_EXPAND.js";
28
+ import { VGEO_CUT_FLAG_COARSEN } from "./VGEO_CUT_FLAG_COARSEN.js";
23
29
  import { VGEO_CUT_FLAG_CULL } from "./VGEO_CUT_FLAG_CULL.js";
24
30
  import { VGEO_CUT_PARAMS_STRUCT } from "./VGEO_CUT_PARAMS_STRUCT.js";
25
31
  import { VGEO_CUT_WORKGROUP_SIZE } from "./VGEO_CUT_WORKGROUP_SIZE.js";
26
32
 
27
33
  /**
28
- * One wavefront of the virtual-geometry cut — the pass design §7 calls for, and the reason every
29
- * structure under it exists.
34
+ * The virtual-geometry cut — design §7's pass, as a descent that lives inside one dispatch.
30
35
  *
31
36
  * ## The rule
32
37
  *
@@ -35,29 +40,67 @@ import { VGEO_CUT_WORKGROUP_SIZE } from "./VGEO_CUT_WORKGROUP_SIZE.js";
35
40
  * > `own group selectable AND project(parent) > tau AND (project(self) <= tau OR child group not
36
41
  * > selectable)`
37
42
  *
38
- * which is design §4's per-cluster form **plus its first term**. That term is not decoration, and
39
- * omitting it — as §4's own wording does, because §4 was written for a traversal that gets it for
40
- * free — double-draws: a group that is resident but not yet selectable has clusters satisfying the
41
- * rest of the rule while their coarse stand-in is drawn over the same surface, a few microns apart,
42
- * with the depth buffer choosing between them. That reads as corrupt geometry rather than as a
43
- * missing page, which is what makes it hard to attribute and worth stating first.
43
+ * which is design §4's per-cluster form together with its first term. That term is load-bearing
44
+ * and §4 states the rule without it: a group that is resident but not yet selectable has clusters
45
+ * satisfying the rest of the rule while their coarse stand-in is drawn over the same surface, a few
46
+ * microns apart, with the depth buffer choosing between them. That reads as corrupt geometry rather
47
+ * than as a missing page, which is what makes it hard to attribute and worth stating first.
44
48
  *
45
49
  * ## The shape
46
50
  *
47
- * A wavefront BFS, ping-ponged through two queues, one lane per item. Not persistent threads over
48
- * an MPMC queue: WebGPU guarantees no forward progress across workgroups, so a consumer waiting on
49
- * a producer in another workgroup is waiting without a bound.
51
+ * The loop over depth is *inside* this kernel: a dispatch is a spill generation rather than a level
52
+ * of the descent. Depth is what a cut costs. Measured on a 28M-face container at tau = 1 on a 4090:
53
+ * the descent converges at **39 levels**, carrying 15,027 items — an average frontier of 385 items,
54
+ * or **six workgroups of 64 on a 128-SM part** — and an empty prepare/traverse pair costs 3.9 us
55
+ * whatever it contains. Page depth grows like log2(triangles), so a dispatch per level gets *worse*
56
+ * with better assets and no better with a faster GPU.
50
57
  *
51
- * An item is a **node visit** or a **group-chunk visit**, distinguished by one bit of `target`. A
52
- * node tests its metric and either enqueues its children or, being a leaf, enqueues bounded chunks
53
- * of its groups' clusters. Chunks exist because a node leaf covers up to four groups of up to 4096
54
- * clusters each (format §11.5) — legal input, and 16,384 iterations in the lane that popped it if
55
- * the leaf walked them itself.
58
+ * What a lane cannot do is wait for another workgroup: WebGPU guarantees no forward progress across
59
+ * workgroups, so persistent threads over an MPMC queue — Nanite's answer — is unbounded waiting.
60
+ * What a lane *can* do is cooperate with the other lanes of its own subgroup, and that is enough:
56
61
  *
57
- * Descent crosses a page boundary by enqueueing the child page's **node 0**: uniform items, one
58
- * kernel, no special case for "entering a page". `SAME_PAGE` needs no special case either, because
59
- * the CPU resolved it to this page's own index at install, so it looks up a page that is trivially
60
- * resident and falls through to the only question it was really asking — is that group selectable.
62
+ * - Every lane executes its item and says how many children it produced. No lane walks its own
63
+ * subtree while its neighbours idle; production is a **count**, at constant cost.
64
+ * - `subgroupExclusiveAdd` over those counts numbers the wave's whole production densely. The first
65
+ * `subgroup_size` of them are taken back into the wave's own lanes — perfectly packed, one child
66
+ * per lane, whoever produced them — and the descent takes another step without touching memory.
67
+ * - Everything past a full wave **spills** to the global queue under one `atomicAdd`. Breadth the
68
+ * wave cannot hold becomes work for other waves rather than a serial loop inside this one.
69
+ *
70
+ * So the number of dispatches is the number of spill generations, and the narrow top of the DAG —
71
+ * twenty levels that move between 1 and 120 items and draw nothing — is one wave taking twenty
72
+ * steps in registers.
73
+ *
74
+ * This is Karis' "Variable sized work" (graphicrants, 2026-03) with its groupshared scatter
75
+ * removed. That scatter needs HLSL's `GroupMemoryBarrier()` — memory ordering with no execution
76
+ * sync, sound because a wave is in lockstep — and WGSL has no memory-only workgroup barrier.
77
+ * `workgroupBarrier` synchronizes execution as well, which pins every subgroup of the workgroup to
78
+ * the same number of steps and is a deadlock the moment they disagree; `workgroupUniformLoad` is
79
+ * that same control barrier plus a load, and what it returns is one value uniform across the
80
+ * workgroup, which is the opposite of the per-lane read a scatter is made of.
81
+ * {@link vg_child_owner} finds each item's producer by binary search over the same prefix sums
82
+ * instead: `log2(subgroup_size)` shuffles, no workgroup memory and no barrier anywhere in this
83
+ * file, so nothing here needs the workgroup to agree on anything.
84
+ *
85
+ * ## What WGSL will not let this do
86
+ *
87
+ * Every loop around a subgroup operation needs a **uniform trip count**, and nothing launders one —
88
+ * not `subgroupAdd`, not `subgroupMax`, not even `subgroupBroadcastFirst`, which launders a value
89
+ * for a use but not for control flow. Nor may such a loop carry a non-uniform `break`, `continue`
90
+ * or `return`. So the descent runs `params.descent_steps` times and masks; a wave that drains its
91
+ * subtree early pays the remaining steps at about ten instructions each rather than leaving. That
92
+ * makes `descent_steps` a tuning knob rather than a safety bound, and
93
+ * `VGEO_CUT_STAT.QUEUE_REMAINING` is what says when it is set too low.
94
+ *
95
+ * Divergence *inside* the loop is free: Tint models reconvergence, so a non-uniform `if` that opens
96
+ * and closes taints nothing after it, and the spill's per-lane loop is fine because it contains no
97
+ * subgroup operation of its own.
98
+ *
99
+ * ## One lane per cluster
100
+ *
101
+ * A leaf produces one item per cluster of its groups and the wave packs them, so what bounds a
102
+ * lane's work is the packing rather than a chunk size, and consecutive lanes read consecutive
103
+ * cluster rows instead of striding over runs of them.
61
104
  *
62
105
  * ## Why page-granular dedup is enough, and why it is necessary
63
106
  *
@@ -83,6 +126,11 @@ import { VGEO_CUT_WORKGROUP_SIZE } from "./VGEO_CUT_WORKGROUP_SIZE.js";
83
126
  * `max_parent_error` is at least `P.parent_error`, so the LOD cull cannot fire, and the frustum
84
127
  * cull fires only where `C` is off-screen anyway.
85
128
  *
129
+ * Descent crosses a page boundary by producing the child page's **node 0**: uniform items, one
130
+ * kernel, no special case for "entering a page". `SAME_PAGE` needs no special case either, because
131
+ * the CPU resolved it to this page's own index at install, so it looks up a page that is trivially
132
+ * resident and falls through to the only question it was really asking — is that group selectable.
133
+ *
86
134
  * ## Bounds
87
135
  *
88
136
  * Every append is checked and every overflow is counted, and they all degrade in one direction —
@@ -92,10 +140,11 @@ import { VGEO_CUT_WORKGROUP_SIZE } from "./VGEO_CUT_WORKGROUP_SIZE.js";
92
140
  *
93
141
  * ## Bindings
94
142
  *
95
- * Eight storage buffers against the engine's floor of ten (RUNTIME_PLAN §4.9), and the page table
96
- * is not one of them: a page's header is the first words of its own arena slot, so `vg_page_field`
97
- * is an offset into a buffer this kernel already binds rather than a second buffer to bind and keep
98
- * coherent.
143
+ * Nine storage buffers against the engine's floor of ten (RUNTIME_PLAN §4.9) — the tightest kernel
144
+ * of the cut, and `vgeo_cut_shaders.compile.spec.js` holds the exact count rather than the bound.
145
+ * The page table is not one of them: a page's header is the first words of its own arena slot, so
146
+ * `vg_page_field` is an offset into a buffer this kernel already binds rather than a second buffer
147
+ * to bind and keep coherent.
99
148
  *
100
149
  * @author Alex Goldring
101
150
  * @copyright Company Named Limited (c) 2026
@@ -122,9 +171,20 @@ resources.createGroup()
122
171
  // one bit per (instance, page ordinal); cleared per invocation because each expansion site
123
172
  // sees a different instance set
124
173
  .addStorageBuffer("claim", WebGPUArray.from(atomic_u32), true)
125
- // header, stats, touch and want stamps, then the two lists — one per frame, shared by every
126
- // view and bucket, which is what dedups a touch across them
174
+ /*
175
+ The tenth, and the last this kernel can have: `Renderer.js` requires ten storage
176
+ buffers per stage of an adapter and refuses one that offers fewer. It is spent on the
177
+ frontier because the alternative — deriving the page set afterwards from the draw
178
+ records — costs a pass over every drawn cluster to recover a fact the claim already
179
+ knew at the moment it was established.
180
+ */
181
+ .addStorageBuffer("frontier", WebGPUArray.from(atomic_u32), true)
182
+ // header, stats, one touch bit per page row, then the want records — one per frame, shared by
183
+ // every view and bucket, which is what dedups a touch across them
127
184
  .addStorageBuffer("feedback", WebGPUArray.from(atomic_u32), true)
185
+ // one stamp per lookup word, persistent across frames and tagged with the frame that wrote it,
186
+ // so a want is deduped without a lookup-sized clear every frame
187
+ .addStorageBuffer("vg_want_stamps", WebGPUArray.from(atomic_u32), true)
128
188
  // the same collection the static mesh expansion appends to, read downstream by the HZB meshlet
129
189
  // filter and the indirect draw with no knowledge that virtual geometry exists
130
190
  .addStorageBuffer("out_meshlets", MESHLET_COLLECTION_WRITE_STRUCT, true)
@@ -134,97 +194,55 @@ const body = CodeChunk.from(
134
194
  //language=WGSL
135
195
  `
136
196
  const VG_QUEUE_GROUP_BIT = ${VGEO_QUEUE_TARGET_GROUP_BIT}u;
197
+ const VG_QUEUE_PARENT_BIT = ${VGEO_QUEUE_TARGET_PARENT_BIT}u;
137
198
  const VG_PAGE_ROW_NONE = ${VGEO_PAGE_ROW_NONE}u;
138
199
  const VG_NO_CHILD_PAGE = ${VGEO_NO_CHILD_PAGE}u;
139
200
  const VG_GROUP_SELECTABLE = ${VGEO_GROUP_FLAG_SELECTABLE}u;
140
201
  const VG_WANT_WORDS = ${VGEO_WANT_WORDS}u;
141
202
 
142
- fn vg_stat(slot: u32, amount: u32) {
143
- atomicAdd(&feedback[${VGEO_FEEDBACK_OFFSET.STATS}u + slot], amount);
144
- }
203
+ const VG_EXPAND_NONE = ${VGEO_CUT_EXPAND.NONE}u;
204
+ const VG_EXPAND_NODES = ${VGEO_CUT_EXPAND.NODES}u;
205
+ const VG_EXPAND_CLUSTERS = ${VGEO_CUT_EXPAND.CLUSTERS}u;
206
+ const VG_EXPAND_PAGE = ${VGEO_CUT_EXPAND.PAGE}u;
207
+ const VG_EXPAND_PARENTS = ${VGEO_CUT_EXPAND.PARENTS}u;
145
208
 
146
209
  /**
147
- * Reserve count slots of the next wavefront.
210
+ * One unit of work: a node visit, or a single cluster of one group.
148
211
  *
149
- * The count is allowed to run past capacity — the prepare pass clamps it before it becomes a
150
- * dispatch — so every write against the returned base is bounds-checked here as well.
212
+ * The same four words the queue holds, and bit 31 of \`visit\` tells the two apart — a node
213
+ * index below it, a group index above it with \`aux\` naming the cluster inside that group.
151
214
  */
152
- fn vg_queue_reserve(count: u32) -> u32 {
153
- return atomicAdd(&queue_out.count, count);
154
- }
155
-
156
- fn vg_queue_write(slot: u32, instance_slot: u32, page_row: u32, visit: u32, first_cluster: u32) {
157
- if (slot >= params.queue_capacity) {
158
- vg_stat(${VGEO_CUT_STAT.QUEUE_DROPPED}u, 1u);
159
- return;
160
- }
161
-
162
- let base = slot * 4u;
163
-
164
- queue_out.elements[base] = instance_slot;
165
- queue_out.elements[base + 1u] = page_row;
166
- queue_out.elements[base + 2u] = visit;
167
- queue_out.elements[base + 3u] = first_cluster;
215
+ struct VGItem {
216
+ instance_slot: u32,
217
+ page_row: u32,
218
+ visit: u32,
219
+ aux: u32,
168
220
  }
169
221
 
170
222
  /**
171
- * Claim a page for one instance, and touch it if the claim was won.
223
+ * What an executed item wants done next, as a count and a rule for the k-th child rather than as
224
+ * children written somewhere.
172
225
  *
173
- * The claim is what makes "at most one draw record per (instance, cluster)" true, so an instance
174
- * whose slice of the bitmask does not fit is refused upstream rather than run unclaimed.
226
+ * This is the whole reason the descent can stay in registers: a producer states its production at
227
+ * constant cost and any lane of the wave can then build child \`k\` of it from five numbers.
175
228
  */
176
- fn vg_claim_page(instance_base: u32, page_row: u32) -> bool {
177
- let ordinal = vg_page_field(page_row, ${VGEO_PAGE_ROW_OFFSET.ORDINAL}u);
178
-
179
- if (ordinal >= instances[instance_base + ${VGEO_INSTANCE_OFFSET.ORDINAL_CAPACITY}u]) {
180
- return false;
181
- }
182
-
183
- let bit = instances[instance_base + ${VGEO_INSTANCE_OFFSET.CLAIM_BASE}u] + ordinal;
184
-
185
- let word = bit >> 5u;
186
-
187
- if (word >= params.claim_word_count) {
188
- return false;
189
- }
190
-
191
- let mask = 1u << (bit & 31u);
192
-
193
- if ((atomicOr(&claim[word], mask) & mask) != 0u) {
194
- return false;
195
- }
196
-
197
- vg_stat(${VGEO_CUT_STAT.PAGES}u, 1u);
198
-
199
- vg_touch_page(page_row);
200
-
201
- return true;
229
+ struct VGExpand {
230
+ kind: u32,
231
+ count: u32,
232
+ page_row: u32,
233
+ first: u32,
234
+ span: u32,
202
235
  }
203
236
 
204
- /**
205
- * Record that this page was selected from this frame — design §6's eviction key.
206
- *
207
- * LRU on last-*touched*, never on last-wanted: stamping at fetch time freezes the stamp of exactly
208
- * the pages that are on screen and evicts them, which is the classic fallback-chain bug the virtual
209
- * texture cache already keys on touch to avoid.
210
- *
211
- * Deduped across every view and bucket of the frame by one stamp word per page row, because the
212
- * feedback buffer is per frame rather than per invocation.
213
- */
214
- fn vg_touch_page(page_row: u32) {
215
- if (atomicExchange(&feedback[params.touch_stamp_base + page_row], 1u) != 0u) {
216
- return;
217
- }
218
-
219
- let slot = atomicAdd(&feedback[${VGEO_FEEDBACK_OFFSET.TOUCH_COUNT}u], 1u);
220
-
221
- if (slot >= params.touch_capacity) {
222
- return;
223
- }
224
-
225
- atomicStore(&feedback[params.touch_base + slot], page_row);
237
+ fn vg_expand_none() -> VGExpand {
238
+ return VGExpand(VG_EXPAND_NONE, 0u, 0u, 0u, 0u);
226
239
  }
227
240
 
241
+ /*
242
+ vg_stat, vg_claim_page and vg_touch_page are chunk_vgeo_cut_claim's — the seed and reseed
243
+ passes take the same three, which is why they are no longer written here.
244
+ */
245
+
228
246
  /**
229
247
  * Ask for the page a clamped cluster could not reach.
230
248
  *
@@ -241,9 +259,10 @@ fn vg_touch_page(page_row: u32) {
241
259
  * and VGeoResidencyManager maxes across frames.
242
260
  */
243
261
  fn vg_want(source_page_row: u32, child_ref: u32, child_page_index: u32, lookup_base: u32, projected: f32) {
244
- let stamp = params.want_stamp_base + lookup_base + child_page_index;
245
-
246
- if (atomicExchange(&feedback[stamp], 1u) != 0u) {
262
+ // persistent and frame-tagged rather than cleared: a stamp already holding this frame's tag
263
+ // was written by this frame, and anything else is a frame that has gone
264
+ if (atomicExchange(&vg_want_stamps[lookup_base + child_page_index], params.frame_tag)
265
+ == params.frame_tag) {
247
266
  return;
248
267
  }
249
268
 
@@ -282,18 +301,41 @@ fn vg_draw(page_row: u32, cluster: u32, mesh_index: u32) {
282
301
  out_meshlets.elements[slot] = ${MESHLET_DEFINITION_STRUCT.wgsl_ref}(meshlet, mesh_index);
283
302
 
284
303
  vg_stat(${VGEO_CUT_STAT.DRAWN}u, 1u);
304
+
305
+ /*
306
+ And remember the page for next frame. The frontier is where this cut *ended*, not
307
+ everywhere it went: recording at the claim instead would name every page the descent
308
+ passed through on its way down, and reseeding from that set is more work than the root
309
+ descent it replaces rather than less. A page that drew something is a page the cut is
310
+ standing on.
311
+
312
+ A bitmask, so the several clusters of a page that draw cost one bit and no dedup. Per view,
313
+ so two views keep their own answer.
314
+ */
315
+ let frontier_word = page_row >> 5u;
316
+
317
+ if (frontier_word < params.frontier_word_count) {
318
+ atomicOr(&frontier[frontier_word], 1u << (page_row & 31u));
319
+ }
285
320
  }
286
321
 
287
- fn vg_group_selectable(page_row: u32, group: u32) -> bool {
288
- let row = vg_group_base(page_row) + group * VG_GROUP_ROW_WORDS;
322
+ fn vg_group_row(page_row: u32, group: u32) -> u32 {
323
+ return vg_group_base(page_row) + group * VG_GROUP_ROW_WORDS;
324
+ }
289
325
 
290
- return (vg_arena[row + ${VGEO_GROUP_ROW_OFFSET.FLAGS}u] & VG_GROUP_SELECTABLE) != 0u;
326
+ fn vg_group_selectable(page_row: u32, group: u32) -> bool {
327
+ return (vg_arena[vg_group_row(page_row, group) + ${VGEO_GROUP_ROW_OFFSET.FLAGS}u]
328
+ & VG_GROUP_SELECTABLE) != 0u;
291
329
  }
292
330
 
293
331
  /**
294
- * A node: cull, then descend or hand off.
332
+ * A node: cull, then say what it expands into.
333
+ *
334
+ * Nothing is enqueued here. An interior node expands into its child nodes and a leaf into every
335
+ * cluster of its groups, and either is a base and a count — which is what lets the wave take the
336
+ * first \`subgroup_size\` of them into its own lanes and spill the rest.
295
337
  */
296
- fn vg_visit_node(instance_slot: u32, instance_base: u32, page_row: u32, node: u32) {
338
+ fn vg_visit_node(instance_base: u32, page_row: u32, node: u32) -> VGExpand {
297
339
  let row = vg_node_base(page_row) + node * VG_NODE_ROW_WORDS;
298
340
 
299
341
  let sphere = vg_sphere_at(row + ${VGEO_NODE_ROW_OFFSET.SPHERE}u);
@@ -301,7 +343,7 @@ fn vg_visit_node(instance_slot: u32, instance_base: u32, page_row: u32, node: u3
301
343
  if ((params.flags & ${VGEO_CUT_FLAG_CULL}u) != 0u
302
344
  && vg_sphere_outside_frustum(instance_base, sphere)) {
303
345
  vg_stat(${VGEO_CUT_STAT.CULLED_FRUSTUM}u, 1u);
304
- return;
346
+ return vg_expand_none();
305
347
  }
306
348
 
307
349
  let eye = vg_instance_eye(instance_base);
@@ -312,11 +354,26 @@ fn vg_visit_node(instance_slot: u32, instance_base: u32, page_row: u32, node: u3
312
354
  *largest* parent error under this node still projects to no more than tau, nothing under it can
313
355
  be drawn and the whole subtree goes. The node's sphere is the union of the covered clusters'
314
356
  parent spheres, so the projection is conservative in both terms.
315
- */
316
- if (vg_project(vg_arena_f32(row + ${VGEO_NODE_ROW_OFFSET.MAX_PARENT_ERROR}u), sphere, eye, k)
317
- <= params.tau) {
357
+
358
+ **It is a descent-only accelerator**, and the coarsen flag turns it off. Its premise is that
359
+ a subtree nothing can be drawn in is a subtree worth skipping, which is true of a traversal
360
+ that only ever looks for what to draw. A walk up is looking for something else: the groups
361
+ under this node are below the cut, and being below the cut is precisely what sends them to
362
+ their parents. Skipping them here is what made a cut seeded at the leaves draw nothing at all
363
+ -- the subtree carrying the whole walk was pruned at its first node, silently and by a rule
364
+ that is correct for the direction it was written for.
365
+
366
+ A node has no parent list of its own -- the upward edge is per group (format §6.3) -- so
367
+ there is nothing to coarsen *from* here, and entering the subtree is the only way to reach
368
+ the groups that can. That is the cost of walking up through a page: the pages on the way
369
+ are evaluated whole. It is why CUT_PLAN §1 keeps a frontier at all rather than re-deriving
370
+ one, and why the frontier is what makes this affordable rather than this pass.
371
+ */
372
+ if ((params.flags & ${VGEO_CUT_FLAG_COARSEN}u) == 0u
373
+ && vg_project(vg_arena_f32(row + ${VGEO_NODE_ROW_OFFSET.MAX_PARENT_ERROR}u), sphere, eye, k)
374
+ <= params.tau) {
318
375
  vg_stat(${VGEO_CUT_STAT.CULLED_LOD}u, 1u);
319
- return;
376
+ return vg_expand_none();
320
377
  }
321
378
 
322
379
  let child_first = vg_arena[row + ${VGEO_NODE_ROW_OFFSET.CHILD_FIRST}u];
@@ -325,204 +382,493 @@ fn vg_visit_node(instance_slot: u32, instance_base: u32, page_row: u32, node: u3
325
382
  let child_count = child_word & VG_NODE_CHILD_COUNT_MASK;
326
383
 
327
384
  if ((child_word & VG_NODE_LEAF_BIT) == 0u) {
328
- let slot = vg_queue_reserve(child_count);
329
-
330
- for (var i = 0u; i < child_count; i = i + 1u) {
331
- vg_queue_write(slot + i, instance_slot, page_row, child_first + i, 0u);
332
- }
333
-
334
- return;
385
+ return VGExpand(VG_EXPAND_NODES, child_count, page_row, child_first, 0u);
335
386
  }
336
387
 
337
- // A leaf's children are groups. Hand them off in bounded chunks rather than walking them here:
338
- // four groups of four thousand clusters is legal input and would be one lane's whole life.
339
- let group_base = vg_group_base(page_row);
340
-
388
+ // a leaf's children are groups, and its production is every cluster they hold
341
389
  var total = 0u;
342
390
 
343
391
  for (var i = 0u; i < child_count; i = i + 1u) {
344
- let members = vg_arena[
345
- group_base + (child_first + i) * VG_GROUP_ROW_WORDS
346
- + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT}u
392
+ total = total + vg_arena[
393
+ vg_group_row(page_row, child_first + i) + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT}u
347
394
  ];
348
-
349
- total = total + (members + params.cluster_chunk - 1u) / params.cluster_chunk;
350
395
  }
351
396
 
352
- var slot = vg_queue_reserve(total);
397
+ return VGExpand(VG_EXPAND_CLUSTERS, total, page_row, child_first, child_count);
398
+ }
353
399
 
354
- for (var i = 0u; i < child_count; i = i + 1u) {
355
- let group = child_first + i;
400
+ /**
401
+ * The k-th cluster under a leaf, as a group and an index inside it.
402
+ *
403
+ * Bounded by the leaf's own group count and branchless, which are both about the same hazard. Every
404
+ * lane of the wave runs this, including the lanes past the end of the wave's production, and those
405
+ * hold a \`k\` with no cluster behind it. A walk that stopped when it found the group would run off
406
+ * the end of the group rows for them, and a run of zero-member rows is an infinite loop rather than
407
+ * a wrong answer — a hung GPU, on data the builder is free to produce.
408
+ *
409
+ * Their result is nonsense and is never executed, because the lane is inactive. It only has to be
410
+ * in-range and finite.
411
+ *
412
+ * It is a loop with a non-uniform trip count and no subgroup operation inside, which WGSL permits
413
+ * and which taints nothing after it.
414
+ */
415
+ fn vg_leaf_cluster(page_row: u32, group_first: u32, group_count: u32, k: u32) -> vec2<u32> {
416
+ var remaining = k;
417
+ var group = 0u;
418
+ var found = false;
356
419
 
420
+ for (var i = 0u; i < group_count; i = i + 1u) {
357
421
  let members = vg_arena[
358
- group_base + group * VG_GROUP_ROW_WORDS + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT}u
422
+ vg_group_row(page_row, group_first + i) + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT}u
359
423
  ];
360
424
 
361
- var first = 0u;
425
+ /*
426
+ The latch is the whole of it. Walking on without one is not a slower answer but a wrong
427
+ one: the walk keeps testing \`remaining\` against later groups, and any later group smaller
428
+ than the remainder it already settled on takes the item off the group that owns it. Two
429
+ values of k then name one cluster, and one cluster drawn twice is the exact failure the
430
+ claim bitmask and the own-group term exist to prevent.
431
+ */
432
+ let here = !found && remaining < members;
362
433
 
363
- while (first < members) {
364
- vg_queue_write(slot, instance_slot, page_row, group | VG_QUEUE_GROUP_BIT, first);
434
+ group = select(group, i, here);
435
+ found = found || here;
365
436
 
366
- slot = slot + 1u;
367
- first = first + params.cluster_chunk;
368
- }
437
+ // consumed only while still searching, so this never underflows
438
+ remaining = select(remaining - members, remaining, found);
369
439
  }
440
+
441
+ return vec2<u32>(group_first + group, remaining);
370
442
  }
371
443
 
372
444
  /**
373
- * A run of one group's clusters: the rule, cluster by cluster.
445
+ * One cluster: the rule, once.
374
446
  */
375
- fn vg_visit_group(
376
- instance_slot: u32,
447
+ fn vg_visit_cluster(
377
448
  instance_base: u32,
378
449
  page_row: u32,
379
450
  group: u32,
380
- first_cluster: u32
381
- ) {
451
+ member: u32
452
+ ) -> VGExpand {
382
453
  /*
383
454
  The own-group term. A group that is resident but not selectable draws nothing at all — its
384
455
  coarser selectable ancestors are already covering that surface, and drawing both is the
385
456
  double-draw this whole runtime is arranged to avoid.
386
457
  */
387
458
  if (!vg_group_selectable(page_row, group)) {
388
- return;
459
+ return vg_expand_none();
389
460
  }
390
461
 
391
- let group_row = vg_group_base(page_row) + group * VG_GROUP_ROW_WORDS;
392
-
393
- let members = vg_arena[group_row + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_COUNT}u];
394
- let cluster_first = vg_arena[group_row + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_FIRST}u];
462
+ let group_row = vg_group_row(page_row, group);
395
463
 
396
- let last = min(first_cluster + params.cluster_chunk, members);
464
+ let cluster = vg_arena[group_row + ${VGEO_GROUP_ROW_OFFSET.CLUSTER_FIRST}u] + member;
397
465
 
398
- let cluster_base = vg_cluster_base(page_row);
466
+ let row = vg_cluster_base(page_row) + cluster * VG_CLUSTER_ROW_WORDS;
399
467
 
400
468
  let eye = vg_instance_eye(instance_base);
401
469
  let k = vg_instance_f32(instance_base, ${VGEO_INSTANCE_OFFSET.PROJECTION_K}u);
402
470
  let mesh_index = instances[instance_base + ${VGEO_INSTANCE_OFFSET.MESH_INDEX}u];
403
471
  let lookup_base = instances[instance_base + ${VGEO_INSTANCE_OFFSET.LOOKUP_WORD_OFFSET}u];
404
472
 
405
- for (var i = first_cluster; i < last; i = i + 1u) {
406
- let cluster = cluster_first + i;
473
+ vg_stat(${VGEO_CUT_STAT.CLUSTERS_TESTED}u, 1u);
407
474
 
408
- let row = cluster_base + cluster * VG_CLUSTER_ROW_WORDS;
475
+ /*
476
+ Before the cluster can refine, and the reason is residency rather than the draw: refining
477
+ claims the child page, and a claim is a touch — the LRU's whole input — so an off-screen
478
+ cluster that refined anyway would keep pages nobody can see hot and the tier full of them.
479
+ self_sphere contains the cluster's own vertices (FORMAT §7), so this rejects nothing on
480
+ screen; and every parent of a group shares one self_sphere, so a group's parents decide this
481
+ uniformly and the cut's proofs stand (RUNTIME_PLAN §4.4).
482
+ */
483
+ if ((params.flags & ${VGEO_CUT_FLAG_CULL}u) != 0u
484
+ && vg_sphere_outside_frustum(
485
+ instance_base, vg_sphere_at(row + ${VGEO_CLUSTER_ROW_OFFSET.SELF_SPHERE}u)
486
+ )) {
487
+ vg_stat(${VGEO_CUT_STAT.CULLED_CLUSTER}u, 1u);
409
488
 
410
- vg_stat(${VGEO_CUT_STAT.CLUSTERS_TESTED}u, 1u);
489
+ return vg_expand_none();
490
+ }
411
491
 
412
- let parent_projected = vg_project(
413
- vg_arena_f32(row + ${VGEO_CLUSTER_ROW_OFFSET.PARENT_ERROR}u),
414
- vg_sphere_at(row + ${VGEO_CLUSTER_ROW_OFFSET.PARENT_SPHERE}u),
415
- eye, k
416
- );
492
+ let parent_projected = vg_project(
493
+ vg_arena_f32(row + ${VGEO_CLUSTER_ROW_OFFSET.PARENT_ERROR}u),
494
+ vg_sphere_at(row + ${VGEO_CLUSTER_ROW_OFFSET.PARENT_SPHERE}u),
495
+ eye, k
496
+ );
417
497
 
418
- if (parent_projected <= params.tau) {
419
- // something coarser is covering this surface; this cluster is below the cut
420
- continue;
498
+ if (parent_projected <= params.tau) {
499
+ /*
500
+ Below the cut: something coarser covers this surface. A descent from the root never
501
+ arrives here -- it only descends through a step that projected above tau -- so this
502
+ is the branch a cut that was *kept* between frames lands in when the camera pulls
503
+ back, and stopping is what makes such a cut unable to coarsen.
504
+
505
+ parent_error and parent_sphere are the consuming step's, and every cluster of a group
506
+ shares that one step (FORMAT section 11.16), so this decision is group-uniform and
507
+ the group's first member can carry it alone. That is an election with nothing to
508
+ communicate: the other members reach the same answer and defer to the same lane.
509
+ */
510
+ let group_row_here = vg_group_row(page_row, group);
511
+
512
+ let parent_count = vg_arena[group_row_here + ${VGEO_GROUP_ROW_OFFSET.PARENT_COUNT}u];
513
+
514
+ if ((params.flags & ${VGEO_CUT_FLAG_COARSEN}u) == 0u || member != 0u
515
+ || parent_count == 0u) {
516
+ return vg_expand_none();
421
517
  }
422
518
 
423
- let self_projected = vg_project(
424
- vg_arena_f32(row + ${VGEO_CLUSTER_ROW_OFFSET.SELF_ERROR}u),
425
- vg_sphere_at(row + ${VGEO_CLUSTER_ROW_OFFSET.SELF_SPHERE}u),
426
- eye, k
519
+ return VGExpand(
520
+ VG_EXPAND_PARENTS,
521
+ parent_count,
522
+ page_row,
523
+ vg_arena[group_row_here + ${VGEO_GROUP_ROW_OFFSET.PARENT_FIRST}u],
524
+ 0u
427
525
  );
526
+ }
428
527
 
429
- let child_page_index = vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_PAGE_INDEX}u];
528
+ let self_projected = vg_project(
529
+ vg_arena_f32(row + ${VGEO_CLUSTER_ROW_OFFSET.SELF_ERROR}u),
530
+ vg_sphere_at(row + ${VGEO_CLUSTER_ROW_OFFSET.SELF_SPHERE}u),
531
+ eye, k
532
+ );
430
533
 
431
- if (self_projected <= params.tau || child_page_index == VG_NO_CHILD_PAGE) {
432
- vg_draw(page_row, cluster, mesh_index);
433
- continue;
434
- }
534
+ let child_page_index = vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_PAGE_INDEX}u];
535
+
536
+ if (self_projected <= params.tau || child_page_index == VG_NO_CHILD_PAGE) {
537
+ vg_draw(page_row, cluster, mesh_index);
538
+ return vg_expand_none();
539
+ }
435
540
 
436
- let child_page_row = vg_lookup[lookup_base + child_page_index];
541
+ let child_page_row = vg_lookup[lookup_base + child_page_index];
437
542
 
438
- if (child_page_row == VG_PAGE_ROW_NONE) {
439
- /*
440
- The clamp. Drawing this cluster is not a fallback for a missing one — it is the correct
441
- coarser surface over exactly the region the missing page would have refined, which is
442
- what makes an arriving cut watertight at every moment rather than only at the end.
443
- */
444
- vg_draw(page_row, cluster, mesh_index);
543
+ if (child_page_row == VG_PAGE_ROW_NONE) {
544
+ /*
545
+ The clamp. Drawing this cluster is not a fallback for a missing one — it is the correct
546
+ coarser surface over exactly the region the missing page would have refined, which is
547
+ what makes an arriving cut watertight at every moment rather than only at the end.
548
+ */
549
+ vg_draw(page_row, cluster, mesh_index);
445
550
 
446
- vg_stat(${VGEO_CUT_STAT.CLAMPED}u, 1u);
551
+ vg_stat(${VGEO_CUT_STAT.CLAMPED}u, 1u);
447
552
 
448
- vg_want(
449
- page_row,
450
- vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_REF}u],
451
- child_page_index,
452
- lookup_base,
453
- self_projected
454
- );
553
+ vg_want(
554
+ page_row,
555
+ vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_REF}u],
556
+ child_page_index,
557
+ lookup_base,
558
+ self_projected
559
+ );
455
560
 
456
- continue;
457
- }
561
+ return vg_expand_none();
562
+ }
458
563
 
459
- let child_group = vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_GROUP}u];
564
+ let child_group = vg_arena[row + ${VGEO_CLUSTER_ROW_OFFSET.CHILD_GROUP}u];
460
565
 
461
- if (!vg_group_selectable(child_page_row, child_group)) {
462
- /*
463
- The bytes are here and the group is still waiting on a page holding one of its *other*
464
- parents, so there is nothing to fetch from this cluster. Another cluster in this same
465
- cut is clamped directly against that page and wants it — monotonicity puts every
466
- ancestor of a cluster projecting above tau above tau as well.
467
- */
468
- vg_draw(page_row, cluster, mesh_index);
566
+ if (!vg_group_selectable(child_page_row, child_group)) {
567
+ /*
568
+ The bytes are here and the group is still waiting on a page holding one of its *other*
569
+ parents, so there is nothing to fetch from this cluster. Another cluster in this same
570
+ cut is clamped directly against that page and wants it — monotonicity puts every
571
+ ancestor of a cluster projecting above tau above tau as well.
572
+ */
573
+ vg_draw(page_row, cluster, mesh_index);
469
574
 
470
- vg_stat(${VGEO_CUT_STAT.CLAMPED}u, 1u);
471
- vg_stat(${VGEO_CUT_STAT.CLAMPED_ON_ACTIVATION}u, 1u);
575
+ vg_stat(${VGEO_CUT_STAT.CLAMPED}u, 1u);
576
+ vg_stat(${VGEO_CUT_STAT.CLAMPED_ON_ACTIVATION}u, 1u);
472
577
 
473
- continue;
474
- }
578
+ return vg_expand_none();
579
+ }
475
580
 
476
- if (vg_claim_page(instance_base, child_page_row)) {
477
- let slot = vg_queue_reserve(1u);
581
+ if (!vg_claim_page(instance_base, child_page_row)) {
582
+ // another lane, or another parent of this page, got there first
583
+ return vg_expand_none();
584
+ }
478
585
 
479
- vg_queue_write(slot, instance_slot, child_page_row, 0u, 0u);
480
- }
586
+ return VGExpand(VG_EXPAND_PAGE, 1u, child_page_row, 0u, 0u);
587
+ }
588
+
589
+ /**
590
+ * One entry of a page's parent rows: the page holding that parent, claimed and queued.
591
+ *
592
+ * The mirror of the descent's page crossing, and deliberately page-granular like it. The
593
+ * entry names a cluster as well, and this ignores it: a page reached from any direction is
594
+ * evaluated whole, the rule is a pure function of each row, and visiting a cluster the walk
595
+ * did not need costs only the test. What the cluster half of the entry is for is the
596
+ * refine-side election of CUT_PLAN_2026_09_14 section 3, which is section 7.6's work.
597
+ *
598
+ * A parent that is not resident stops the walk rather than clamping. There is nothing to
599
+ * clamp *to* -- the surface this lane is standing on is below the cut and is not drawn -- so
600
+ * the honest answer is that this instance cannot be served incrementally this frame, which
601
+ * is the fallback section 4 already defines and section 7.6 wires up.
602
+ */
603
+ fn vg_visit_parent(instance_base: u32, page_row: u32, entry: u32) -> VGExpand {
604
+ if (entry >= vg_page_field(page_row, ${VGEO_PAGE_ROW_OFFSET.PARENT_ENTRY_COUNT}u)) {
605
+ return vg_expand_none();
606
+ }
607
+
608
+ let row = vg_parent_base(page_row) + entry * VG_PARENT_ROW_WORDS;
609
+
610
+ let parent_page_index = vg_arena[row + ${VGEO_PARENT_ROW_OFFSET.PAGE_INDEX}u];
611
+
612
+ if (parent_page_index == VG_NO_CHILD_PAGE) {
613
+ return vg_expand_none();
481
614
  }
615
+
616
+ let lookup_base = instances[instance_base + ${VGEO_INSTANCE_OFFSET.LOOKUP_WORD_OFFSET}u];
617
+
618
+ let parent_page_row = vg_lookup[lookup_base + parent_page_index];
619
+
620
+ if (parent_page_row == VG_PAGE_ROW_NONE) {
621
+ return vg_expand_none();
622
+ }
623
+
624
+ if (!vg_claim_page(instance_base, parent_page_row)) {
625
+ // this page is already in this instance's cut, from above or from another parent
626
+ return vg_expand_none();
627
+ }
628
+
629
+ return VGExpand(VG_EXPAND_PAGE, 1u, parent_page_row, 0u, 0u);
482
630
  }
483
631
 
484
- @compute @workgroup_size(${VGEO_CUT_WORKGROUP_SIZE})
485
- fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
486
- let item = global_id.x;
632
+ /**
633
+ * Execute whatever this lane is holding.
634
+ */
635
+ fn vg_execute(item: VGItem, holding: bool) -> VGExpand {
636
+ if (!holding) {
637
+ return vg_expand_none();
638
+ }
487
639
 
488
- if (item == 0u && queue_in.count > 0u) {
489
- /*
490
- One per wavefront that actually ran. The descent is a fixed number of indirect dispatches
491
- of zero workgroups once the work runs out, so this is the only way to know how deep it
492
- went — and reaching the iteration budget is what turns RUNTIME_PLAN §4.7's measured depth
493
- of about 48 from a fact into an assumption.
494
- */
495
- vg_stat(${VGEO_CUT_STAT.WAVEFRONTS}u, 1u);
640
+ vg_stat(${VGEO_CUT_STAT.ITEMS}u, 1u);
641
+
642
+ let instance_base = item.instance_slot * VG_INSTANCE_WORDS;
643
+
644
+ if ((item.visit & VG_QUEUE_GROUP_BIT) != 0u) {
645
+ return vg_visit_cluster(
646
+ instance_base, item.page_row, item.visit & ~VG_QUEUE_GROUP_BIT, item.aux
647
+ );
648
+ }
649
+
650
+ if ((item.visit & VG_QUEUE_PARENT_BIT) != 0u) {
651
+ return vg_visit_parent(
652
+ instance_base, item.page_row, item.visit & ~VG_QUEUE_PARENT_BIT
653
+ );
654
+ }
655
+
656
+ return vg_visit_node(instance_base, item.page_row, item.visit);
657
+ }
658
+
659
+ /**
660
+ * Child \`k\` of an expansion, as an item.
661
+ *
662
+ * Pure, and that is what lets the same rule serve both consumers: the lane that keeps a child calls
663
+ * it with values shuffled out of the producer's registers, and the lane that spills one calls it
664
+ * with its own.
665
+ */
666
+ fn vg_child(instance_slot: u32, expand: VGExpand, k: u32) -> VGItem {
667
+ if (expand.kind == VG_EXPAND_NODES) {
668
+ return VGItem(instance_slot, expand.page_row, expand.first + k, 0u);
669
+ }
670
+
671
+ if (expand.kind == VG_EXPAND_CLUSTERS) {
672
+ let at = vg_leaf_cluster(expand.page_row, expand.first, expand.span, k);
673
+
674
+ return VGItem(instance_slot, expand.page_row, at.x | VG_QUEUE_GROUP_BIT, at.y);
675
+ }
676
+
677
+ if (expand.kind == VG_EXPAND_PARENTS) {
678
+ // entry first + k of the *producing* page's parent rows; the page it names is
679
+ // resolved and claimed when this item is executed, not here
680
+ return VGItem(
681
+ instance_slot, expand.page_row, (expand.first + k) | VG_QUEUE_PARENT_BIT, 0u
682
+ );
496
683
  }
497
684
 
498
- if (item >= queue_in.count) {
685
+ // VG_EXPAND_PAGE: the child page's node 0, which is how a descent crosses a page boundary
686
+ return VGItem(instance_slot, expand.page_row, 0u, 0u);
687
+ }
688
+
689
+ /**
690
+ * Which lane of the wave produced the item at \`index\` of its densely numbered production.
691
+ *
692
+ * The owner is the **largest** lane whose base is at or below the index — largest, because a lane
693
+ * that produced nothing repeats its neighbour's base, and a repeat sits at the start of the next
694
+ * producer's run rather than the end of the previous one. The bases are non-decreasing, so this is
695
+ * a binary search, and \`span\` starts at a builtin so the loop's trip count is uniform.
696
+ */
697
+ fn vg_child_owner(first: u32, index: u32, lane_count: u32) -> u32 {
698
+ var owner = 0u;
699
+ var span = lane_count;
700
+
701
+ while (span > 1u) {
702
+ let step_half = span >> 1u;
703
+ let mid = owner + step_half;
704
+
705
+ // branchless: a non-uniform if around the next iteration's shuffle would not compile
706
+ owner = select(owner, mid, subgroupShuffle(first, mid) <= index);
707
+
708
+ span = span - step_half;
709
+ }
710
+
711
+ return owner;
712
+ }
713
+
714
+ fn vg_queue_write(slot: u32, item: VGItem) {
715
+ if (slot >= params.queue_capacity) {
716
+ vg_stat(${VGEO_CUT_STAT.QUEUE_DROPPED}u, 1u);
499
717
  return;
500
718
  }
501
719
 
502
- let base = item * 4u;
720
+ let base = slot * 4u;
503
721
 
504
- let instance_slot = queue_in.elements[base];
505
- let page_row = queue_in.elements[base + 1u];
506
- let visit = queue_in.elements[base + 2u];
507
- let first_cluster = queue_in.elements[base + 3u];
722
+ queue_out.elements[base] = item.instance_slot;
723
+ queue_out.elements[base + 1u] = item.page_row;
724
+ queue_out.elements[base + 2u] = item.visit;
725
+ queue_out.elements[base + 3u] = item.aux;
726
+ }
508
727
 
509
- vg_stat(${VGEO_CUT_STAT.ITEMS}u, 1u);
728
+ @compute @workgroup_size(${VGEO_CUT_WORKGROUP_SIZE})
729
+ fn main(
730
+ @builtin(global_invocation_id) global_id: vec3<u32>,
731
+ @builtin(subgroup_size) lane_count: u32,
732
+ @builtin(subgroup_invocation_id) lane_index: u32,
733
+ ) {
734
+ let slot = global_id.x;
735
+
736
+ /*
737
+ The queue's count against what the queue holds. \`shader_vgeo_cut_queue_prepare\` runs
738
+ immediately before every dispatch of this kernel and stores exactly this clamp back, so the
739
+ bound is already there — but it was the *read* below that carried it here and not the predicate
740
+ that decides whether a lane executes, and those two have to agree. A lane held past capacity
741
+ reads the clamped index, which is the last item of the queue: it would execute a duplicate of
742
+ an item another lane is already holding, and a cluster visited twice from two lanes is two draw
743
+ records for one surface. That is the one failure the claim bitmask and the own-group term are
744
+ both arranged to prevent, so the predicate does not get to be the looser of the two.
745
+ */
746
+ let queue_count = min(queue_in.count, params.queue_capacity);
747
+
748
+ /*
749
+ One per dispatch that had work, which is one per spill generation rather than one per level of
750
+ the descent: the depth is inside this kernel. Reaching params.descent_steps with items still
751
+ held is what QUEUE_REMAINING reports, and nothing else can.
752
+ */
753
+ if (slot == 0u && queue_count > 0u) {
754
+ vg_stat(${VGEO_CUT_STAT.WAVEFRONTS}u, 1u);
755
+ }
510
756
 
511
- let instance_base = instance_slot * VG_INSTANCE_WORDS;
757
+ var holding = slot < queue_count;
512
758
 
513
- if ((visit & VG_QUEUE_GROUP_BIT) != 0u) {
514
- vg_visit_group(
515
- instance_slot, instance_base, page_row, visit & ~VG_QUEUE_GROUP_BIT, first_cluster
759
+ let base = slot * 4u;
760
+
761
+ /*
762
+ Read unconditionally and mask the result. A branch here would be a non-uniform if, which is
763
+ harmless on its own — but every lane must reach every subgroup operation below, and a lane that
764
+ returned early is a lane the shuffles are not allowed to read.
765
+ */
766
+ let in_bounds = min(base, params.queue_capacity * 4u - 4u);
767
+
768
+ var item = VGItem(
769
+ queue_in.elements[in_bounds],
770
+ queue_in.elements[in_bounds + 1u],
771
+ queue_in.elements[in_bounds + 2u],
772
+ queue_in.elements[in_bounds + 3u]
773
+ );
774
+
775
+ for (var step = 0u; step < params.descent_steps; step = step + 1u) {
776
+ let expand = vg_execute(item, holding);
777
+
778
+ let first = subgroupExclusiveAdd(expand.count);
779
+ let total = subgroupAdd(expand.count);
780
+
781
+ // ----- what this wave keeps: one child per lane, densely packed, whoever produced it -----
782
+
783
+ let owner = vg_child_owner(first, lane_index, lane_count);
784
+
785
+ let kept = vg_child(
786
+ subgroupShuffle(item.instance_slot, owner),
787
+ VGExpand(
788
+ subgroupShuffle(expand.kind, owner),
789
+ 0u,
790
+ subgroupShuffle(expand.page_row, owner),
791
+ subgroupShuffle(expand.first, owner),
792
+ subgroupShuffle(expand.span, owner)
793
+ ),
794
+ lane_index - subgroupShuffle(first, owner)
516
795
  );
517
- } else {
518
- vg_visit_node(instance_slot, instance_base, page_row, visit);
796
+
797
+ // ----- what it cannot hold: spilled, packed, under one reservation for the whole wave -----
798
+
799
+ let mine_first = max(first, lane_count);
800
+ let mine_last = max(first + expand.count, lane_count);
801
+
802
+ let spill_count = mine_last - mine_first;
803
+
804
+ let spill_offset = subgroupExclusiveAdd(spill_count);
805
+ let spill_total = subgroupAdd(spill_count);
806
+
807
+ /*
808
+ Elected *before* the branch, so the branch carries no subgroup operation of its own and is
809
+ free to be non-uniform — which lets a wave with nothing to spill skip the reservation
810
+ outright. An atomic on the queue head is a global round trip; a descent step that keeps
811
+ everything it produced is otherwise register work, and would spend more reserving zero
812
+ slots than on the step.
813
+ */
814
+ let spill_elected = subgroupElect();
815
+
816
+ var reserved = 0u;
817
+
818
+ if (spill_elected && spill_total > 0u) {
819
+ reserved = atomicAdd(&queue_out.count, spill_total);
820
+ }
821
+
822
+ // after the branch closes, which Tint permits: it models reconvergence
823
+ let spill_base = subgroupBroadcastFirst(reserved) + spill_offset;
824
+
825
+ for (var i = 0u; i < spill_count; i = i + 1u) {
826
+ vg_queue_write(spill_base + i, vg_child(
827
+ item.instance_slot, expand, mine_first - first + i
828
+ ));
829
+ }
830
+
831
+ item = kept;
832
+ holding = lane_index < total;
833
+ }
834
+
835
+ /*
836
+ Whatever is still held has not been executed, so it goes back to the queue rather than being
837
+ dropped. This is the descent handing its unfinished frontier to the next dispatch, and at
838
+ lane_count items per wave it is the common case rather than an overflow path.
839
+ */
840
+ let tail_count = select(0u, 1u, holding);
841
+
842
+ let tail_offset = subgroupExclusiveAdd(tail_count);
843
+ let tail_total = subgroupAdd(tail_count);
844
+
845
+ let tail_elected = subgroupElect();
846
+
847
+ var tail_reserved = 0u;
848
+
849
+ // as above: a wave holding nothing does not reserve
850
+ if (tail_elected && tail_total > 0u) {
851
+ tail_reserved = atomicAdd(&queue_out.count, tail_total);
852
+ }
853
+
854
+ let tail_base = subgroupBroadcastFirst(tail_reserved) + tail_offset;
855
+
856
+ if (holding) {
857
+ vg_queue_write(tail_base, item);
519
858
  }
520
859
  }
521
860
  `, [
522
861
  chunk_vgeo_cut_access,
862
+ chunk_vgeo_cut_claim,
523
863
  MESHLET_DEFINITION_STRUCT.declaration_chunk,
524
864
  ]);
525
865
 
866
+ /*
867
+ The descent is a subgroup cooperating with itself — every lane's item packed into every lane's
868
+ register at each step — so the device has to have that capability and the source has to say so.
869
+ */
870
+ body.addExtension(WebGPUExtensionType.Subgroups);
871
+
526
872
  export const shader_vgeo_cut_traverse = ComputeShader.from({
527
873
  label: "VGeo cut traverse",
528
874
  body,