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