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