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