@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,637 @@
|
|
|
1
|
+
# Virtual Geometry — the cut becomes state
|
|
2
|
+
|
|
3
|
+
**2026-09-14, revision 2** after an adversarial pass over revision 1 (ten findings; the ones that
|
|
4
|
+
changed the design are marked *(rev 2)*, and §6.1 — which rev 1 called the blocking question — is
|
|
5
|
+
answered against the builder and the container validator).
|
|
6
|
+
|
|
7
|
+
A design, not yet a commitment: nothing here is built. It supersedes nothing until it does, and what
|
|
8
|
+
it proposes to change in `RUNTIME_PLAN.md` is §4 — not the cut *rule*, whose proofs stand unchanged,
|
|
9
|
+
but the claim that the rule is evaluated from the root every frame.
|
|
10
|
+
|
|
11
|
+
It also asks for one **format** change (§2.1). That is deliberate and it is the reason this is
|
|
12
|
+
written before any code: `.vgeo` has no upward edge, and whether it gets one decides which of two
|
|
13
|
+
very different runtimes we build.
|
|
14
|
+
|
|
15
|
+
## 0. Why — measured, not argued
|
|
16
|
+
|
|
17
|
+
A `.sgpt` capture of `playground/vgeo_runtime/` (4090, torus 12,288 faces, τ=1, one instance) and a
|
|
18
|
+
sweep driven through CDP against the live page. **Read medians**: this machine throws ~400 µs stalls
|
|
19
|
+
that land on unrelated passes too — `mesh frustum cull` has median 3.07 µs and max 468.99 µs in the
|
|
20
|
+
same capture — so a mean over frames is mostly environment.
|
|
21
|
+
|
|
22
|
+
The frame runs **16 traverse dispatches** — two expansion sites, `max_iterations` 8 each. Their cost
|
|
23
|
+
is not spread across them:
|
|
24
|
+
|
|
25
|
+
| ordinal | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 |
|
|
26
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
27
|
+
| median, µs | **40.96** | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 | 2.05 |
|
|
28
|
+
|
|
29
|
+
One pass carries it and the rest are at the 1.02 µs timestamp tick. And that pass is **not the
|
|
30
|
+
geometry**. Holding the scene still and sweeping `descent_steps`, with `items` fixed at 78 and
|
|
31
|
+
`drawn` fixed at 31:
|
|
32
|
+
|
|
33
|
+
| `descent_steps` | 4 | 8 | 16 | 32 | 64 | 128 |
|
|
34
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
35
|
+
| first traverse, 1 instance | 11.3 | 18.4 | — | 36.9 | 52.2 | — |
|
|
36
|
+
| first traverse, 256 instances | 13.3 | 25.6 | 38.9 | 46.1 | 62.5 | 94.2 |
|
|
37
|
+
|
|
38
|
+
**A dispatch costs ≈ 9 µs + ~0.5 µs × `descent_steps` + work.** The slope is the same at one
|
|
39
|
+
instance and at 256, so it is neither memory latency nor an occupancy problem: it is the serial
|
|
40
|
+
dependent chain of ~16 subgroup operations every step executes whether or not the wave still holds
|
|
41
|
+
anything — two full scans to pack what the wave keeps, two more for the spill, the five-shuffle
|
|
42
|
+
owner search, six payload shuffles, elect and broadcast. A dispatch lasts one wave's step loop and
|
|
43
|
+
every wave runs that loop concurrently, so idle steps are paid at full price, once.
|
|
44
|
+
|
|
45
|
+
The other half of the measurement is the one that changes the target. At `descent_steps` 8, adding
|
|
46
|
+
instances of the same geometry:
|
|
47
|
+
|
|
48
|
+
| instances | 1 | 16 | 64 | 256 | 1024 |
|
|
49
|
+
|---|---:|---:|---:|---:|---:|
|
|
50
|
+
| items descended | 78 | 1,248 | 4,992 | 19,968 | 79,872 |
|
|
51
|
+
| clusters drawn | 31 | 496 | 1,984 | 7,936 | 31,744 |
|
|
52
|
+
| cut, µs/frame | 97.2 | 131.0 | 156.8 | 159.7 | **160.7** |
|
|
53
|
+
|
|
54
|
+
`QUEUE_REMAINING` 0 and nothing dropped throughout. **1024 instances cost what 64 do.** The cut is
|
|
55
|
+
not a per-instance cost and the 100s-to-1000s target is already met; what it is, is a *constant*,
|
|
56
|
+
and the constant is the depth of the DAG — which grows like log2(triangles) and is a property of
|
|
57
|
+
the asset rather than of the scene. Deeper asset, bigger constant: the same kernel on a Zorah
|
|
58
|
+
container runs 62.5 µs a dispatch where the torus runs 41.
|
|
59
|
+
|
|
60
|
+
**What that table does not say *(rev 2)*.** Every instance sat at the same transform, so all 1024
|
|
61
|
+
were at the same distance, selected the same level, and read the same rows — best-case L2 and a
|
|
62
|
+
single working set. The item counts are honest (79,872 items really were descended) but the memory
|
|
63
|
+
behaviour is not representative of 1024 instances spread through a scene at different depths.
|
|
64
|
+
|
|
65
|
+
**Redone with spread transforms *(§7.2)*.** Same torus, instances on a grid three radii apart, the
|
|
66
|
+
camera pulled back each time to frame the whole grid:
|
|
67
|
+
|
|
68
|
+
| instances | 1 | 16 | 64 | 256 | 1024 |
|
|
69
|
+
|---|---:|---:|---:|---:|---:|
|
|
70
|
+
| items, stacked | 65 | 1,040 | 4,160 | 16,640 | 66,560 |
|
|
71
|
+
| items, spread | 65 | 512 | 1,664 | 1,222 | 1,968 |
|
|
72
|
+
| pages visited, spread | 6 | 80 | 265 | 348 | 984 |
|
|
73
|
+
| cut µs/frame, spread | 116.6 | 162.7 | 158.7 | 146.3 | **100.2** |
|
|
74
|
+
|
|
75
|
+
The flatness survives, and it is not the L2 that saves it: spreading instances over a scene and
|
|
76
|
+
framing them all puts each one further away, so each selects a **coarser** level and the descent is
|
|
77
|
+
shorter. 1024 spread instances descend 1,968 items where 1024 stacked ones descend 66,560, and the
|
|
78
|
+
cut halves rather than growing. The two tables are therefore not the same workload — the stacked one
|
|
79
|
+
is the pathological case where every instance is near — and neither of them is a per-instance cost.
|
|
80
|
+
That is the claim §0 needed, and it now rests on both.
|
|
81
|
+
|
|
82
|
+
**The free part first — and it is a quarter, not a third.** `drawn` is bit-identical at every
|
|
83
|
+
`descent_steps` that drains, so the knob slices the descent and cannot change the selected set: the
|
|
84
|
+
default is a pure schedule choice. What bounds it is **not** `descent_steps × max_iterations`
|
|
85
|
+
exceeding the DAG depth. A wave keeps one child per lane and spills the rest, so a dispatch advances
|
|
86
|
+
what it held by `descent_steps` levels and hands everything else to the next dispatch, and the
|
|
87
|
+
dispatches a cut needs are
|
|
88
|
+
|
|
89
|
+
> `ceil(depth / descent_steps)` **plus its spill generations**
|
|
90
|
+
|
|
91
|
+
which is a breadth term the depth model has no room for. Measured over a full orbit of the deepest
|
|
92
|
+
converted Zorah container — 27.9M faces, 17,172 pages, the 39 levels — at τ=1, `max_iterations` 8:
|
|
93
|
+
|
|
94
|
+
| `descent_steps` | 32 | 24 | 20 | 16 | 12 | 10 | 8 |
|
|
95
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
96
|
+
| dispatches needed | 6 | 6 | 7 | 8 | 8 | >8 | >8 |
|
|
97
|
+
| worst `QUEUE_REMAINING` | 0 | 0 | 0 | 0 | 0 | 14 | 208 |
|
|
98
|
+
|
|
99
|
+
So **8 is not available at `max_iterations` 8** — it leaves the descent holding work, which §4.5
|
|
100
|
+
makes a hole rather than coarseness — and 24 is the most the default can be lowered for no dispatch
|
|
101
|
+
margin at all: the same six dispatches 32 needs, with a quarter fewer idle steps inside each. That
|
|
102
|
+
is what §7.1 took.
|
|
103
|
+
|
|
104
|
+
The ~3× was the torus, and the torus does no work: at 12,288 faces the cut *is* the per-step
|
|
105
|
+
constant. On the container the second traverse alone is 309 µs of real descent, so the eight steps
|
|
106
|
+
saved move the frame by **3.3%** — 591.8 → 572.4 µs over the close orbit, three repeats each and the
|
|
107
|
+
two sets do not overlap. At the far orbit the same pair does not separate at all: repeats of one
|
|
108
|
+
setting span 340–549 µs, because residency keeps growing across a session and a later run is simply
|
|
109
|
+
holding more pages. That is why the table above is dispatch counts. Dispatch counts are integers,
|
|
110
|
+
they are what the safety question is actually asked in, and they do not drift.
|
|
111
|
+
|
|
112
|
+
It does not fix the shape either: a still camera still re-derives the same answer from node 0 every
|
|
113
|
+
frame, 39 levels deep, once per active bucket per view (§3).
|
|
114
|
+
|
|
115
|
+
## 1. What is kept between frames
|
|
116
|
+
|
|
117
|
+
**The frontier**: per (view, instance), the set of **groups** the cut has descended to.
|
|
118
|
+
|
|
119
|
+
Groups, not clusters, because that is the granularity the rule itself has. A cluster is drawn iff
|
|
120
|
+
|
|
121
|
+
> `own group selectable AND project(parent) > tau AND (project(self) <= tau OR child group not selectable)`
|
|
122
|
+
|
|
123
|
+
and of those, the first two terms are **group-uniform** — every cluster of a group is consumed by
|
|
124
|
+
one step and carries that step's sphere and error (§2, verified) — while only the third varies per
|
|
125
|
+
cluster. So a group is uniformly *reached*, and within it each cluster independently draws or
|
|
126
|
+
refines. The frontier is the reached set; the per-cluster test over it is the cheap half, and the
|
|
127
|
+
profile says so: 78 items descended to find 31 drawn clusters cost 41 µs. **Finding the pages is the
|
|
128
|
+
entire cost. Testing the clusters is free.**
|
|
129
|
+
|
|
130
|
+
**It reseeds the descent; it does not replace it.** The seed pass today emits one item per instance
|
|
131
|
+
naming its root page's node 0. Seeded from the frontier instead, every other pass stays exactly as
|
|
132
|
+
it is — the subgroup descent, the spill, the claim, the draw, the wants. What changes is that
|
|
133
|
+
`descent_steps` then has to cover the *delta* between frames, one or two levels, rather than the
|
|
134
|
+
depth of the DAG. The measured law stops working against us: 0.5 µs × 2 instead of × 32.
|
|
135
|
+
|
|
136
|
+
**A stale frontier is never wrong.** Any consistent cut of the DAG is watertight — the same argument
|
|
137
|
+
the clamp rests on. Too coarse costs detail, too fine costs triangles; neither costs a hole, as long
|
|
138
|
+
as a group moves as a unit, which the group-uniform terms guarantee. Three consequences, and they
|
|
139
|
+
are what make this buildable:
|
|
140
|
+
|
|
141
|
+
- the per-frame update can be **budgeted** — N moves per view per frame, with no correctness cliff
|
|
142
|
+
when the budget binds;
|
|
143
|
+
- validation can be **conservative** — when in doubt, fall back, never repair;
|
|
144
|
+
- **hysteresis is free** (refine at τ, coarsen at ~0.85τ), which removes the flip-flop a per-frame
|
|
145
|
+
cut cannot avoid. Note that TAA jitter is *not* a source of that churn here: `vg_project` reads
|
|
146
|
+
the eye position and `projection_k`, and a sub-pixel clip-space offset changes neither. The churn
|
|
147
|
+
is camera motion, and it is two-directional — pan around a house and near walls refine while the
|
|
148
|
+
far side coarsens, in one geometry, in the same frame.
|
|
149
|
+
|
|
150
|
+
**Only refinement is budgeted *(rev 2)*.** Coarsening strictly shrinks the cut and the draw that
|
|
151
|
+
follows it, and its destinations are resident by §2's invariant, so refusing it is the expensive
|
|
152
|
+
direction: a camera pulling back against a bound coarsen budget would hold the finest level on
|
|
153
|
+
screen for as many frames as the budget takes to drain, which is a sustained frame-time spike rather
|
|
154
|
+
than a transient. Coarsen every entry that asks; budget only the descent.
|
|
155
|
+
|
|
156
|
+
### 1.1 Keying — what a frontier entry belongs to *(rev 2)*
|
|
157
|
+
|
|
158
|
+
`instance_slot` is **not** an identity. `shader_vgeo_cut_collect` assigns it with
|
|
159
|
+
`atomicAdd(&claim_widths.count, 1u)`, so it is a compaction index whose order is decided by GPU
|
|
160
|
+
scheduling: an identical scene can hand the same instance a different slot on the next frame. A
|
|
161
|
+
frontier stored against a slot would attach itself to a different instance, silently, and draw one
|
|
162
|
+
object's cut through another's transform.
|
|
163
|
+
|
|
164
|
+
The frontier is therefore keyed by the instance's **mesh row** — stable, already in the instance
|
|
165
|
+
record as `VGEO_INSTANCE_OFFSET.MESH_INDEX`, and the same "entity is the row" identity the rest of
|
|
166
|
+
the scene state uses. The per-frame instance list maps slot → mesh row in one read, so a reseeded
|
|
167
|
+
seed pass resolves its frontier without a second structure. The store itself is a dense table
|
|
168
|
+
indexed by mesh row, like every other GPU-managed component.
|
|
169
|
+
|
|
170
|
+
That also answers where the frontier *lives*: not in a frame-graph transient, which is recycled, but
|
|
171
|
+
in a runtime-owned buffer with the pool and the tables.
|
|
172
|
+
|
|
173
|
+
## 2. Walking up
|
|
174
|
+
|
|
175
|
+
Coarsening is half of every camera movement, and mixed with refinement inside a single instance.
|
|
176
|
+
A design that can only descend would have to answer every coarsen by re-descending from the root —
|
|
177
|
+
the cost this whole document is trying to remove, paid on the common case. **So either the runtime
|
|
178
|
+
can walk up, or keeping a cut is not worth doing.**
|
|
179
|
+
|
|
180
|
+
**How often it actually moves the frontier, measured *(§7.2)*.** Less than "half of every movement"
|
|
181
|
+
suggests, and the reason is worth knowing before the kernel is written. When the camera pulls back,
|
|
182
|
+
a frontier group's clusters stop being drawn and their parents' clusters must be drawn instead — but
|
|
183
|
+
**47% of frontier entries already draw some clusters and refine others** (measured below), so the
|
|
184
|
+
parent group is usually *already in the frontier* and the coarsen changes which of its clusters draw
|
|
185
|
+
rather than adding an entry. Only the coarsen whose destination is not already there needs the
|
|
186
|
+
upward edge, and that is 0.0–0.8 entries a frame at a walking camera.
|
|
187
|
+
|
|
188
|
+
So §2.1 stays: when the edge is needed there is no other way to find the destination, and a
|
|
189
|
+
re-descent from the root is the alternative. But the coarsen path is not a hot path, which is a
|
|
190
|
+
reason to write it for clarity rather than for throughput.
|
|
191
|
+
|
|
192
|
+
Today it cannot, and not merely when a page is missing. The cluster record is
|
|
193
|
+
`self_sphere, parent_sphere, self_error, parent_error, child_ref, child_group, …`: the parent fields
|
|
194
|
+
are *metrics of the consuming step*, not a reference to what consumes it. The group record carries
|
|
195
|
+
`parent_cluster_count` — a count, deliberately. Every edge in `.vgeo` points down.
|
|
196
|
+
|
|
197
|
+
Three things are already in place, and they are what make the addition small.
|
|
198
|
+
|
|
199
|
+
**One step per group, and the decision is local — verified *(rev 2)*.** `vgeo_build_levels` builds a
|
|
200
|
+
group from exactly one step (`group.clusters = step.child_clusters`), writes that step's error and
|
|
201
|
+
sphere into every child's `parent_error`/`parent_sphere`, and writes the *same* values into every
|
|
202
|
+
parent's `self_error`/`self_sphere` while pointing each parent's `child_group` at that group. The
|
|
203
|
+
container validator then enforces it per (parent, child) pair with exact equality — "one group step
|
|
204
|
+
writes one value on both sides" — and separately that `parent_error ≥ self_error` per cluster.
|
|
205
|
+
Monotonicity is imposed at construction rather than hoped for: the step's error is floored by
|
|
206
|
+
`max_child_error(child_clusters)`, and its sphere is built to contain both what it consumed and what
|
|
207
|
+
it produced.
|
|
208
|
+
|
|
209
|
+
So the coarsen test a cluster reads is **bit-identical** to its parents' own draw test — the same
|
|
210
|
+
floats, so the same IEEE result, with no tolerance argument and no vote. Every cluster of a group
|
|
211
|
+
agrees, every parent of that group agrees, and none of them has to communicate to find out. That is
|
|
212
|
+
the Nanite locality property, and here it is a validated invariant rather than an aspiration.
|
|
213
|
+
|
|
214
|
+
**Tested, not only argued *(§7.5)*.** A CPU oracle that starts at the pages nothing refines into and
|
|
215
|
+
walks up through §6.3's table selects the *same multiset of clusters* as `vgeo_select_cut` descending
|
|
216
|
+
from the root, over 7 camera positions × 4 thresholds on a 160×80 torus. It never compares two
|
|
217
|
+
floats for nearness: it reads `parent_error`/`parent_sphere` off the child and
|
|
218
|
+
`self_error`/`self_sphere` off the parent and requires the same IEEE answer. That is the whole design
|
|
219
|
+
resting on one property, and the property holds on real build output.
|
|
220
|
+
|
|
221
|
+
**The predicate for "may I walk up" exists and is maintained.** `VGEO_GROUP_FLAG_SELECTABLE` is
|
|
222
|
+
"every parent reference is live", kept incrementally by `VGeoRuntimeTables` — an install counts the
|
|
223
|
+
references a resident parent page's selectable groups make into the arriving page, an eviction takes
|
|
224
|
+
them back out (`RUNTIME_PLAN_2026_09_14` §3). A group in the cut is selectable by construction, so
|
|
225
|
+
**its parents are resident, and selectable in turn, all the way up**. Coarsening a drawn group is
|
|
226
|
+
therefore always legal; a cascade of several levels is legal. The kernel still checks — the check is
|
|
227
|
+
one lookup — but the check is not expected to fail, and when it does the answer is §4's fallback.
|
|
228
|
+
|
|
229
|
+
**The coarsen decision needs no new data.** Per the verified identity above, `project(parent) ≤ τ` on
|
|
230
|
+
a cluster of G *is* the parents' `project(self) ≤ τ`. The format change is one edge, not a new
|
|
231
|
+
metric.
|
|
232
|
+
|
|
233
|
+
### 2.1 The format change — **landed**, as `format_version` 4 (§7.3)
|
|
234
|
+
|
|
235
|
+
What follows is the proposal as written; the shipped shape matches it, with the offsets settled and
|
|
236
|
+
the width question answered below. `VGEO_FORMAT.md` §6.3, §6.4 and §11.15–16 are now the reference.
|
|
237
|
+
|
|
238
|
+
One thing the plan did not anticipate: the parent tables are not optional per page, so they are part
|
|
239
|
+
of what a page *costs*, and `vgeo_assemble_pages` lays pages out against `nominal_page_size` before
|
|
240
|
+
the writer ever sees them. The assembler therefore has to budget for a group's parent entries while
|
|
241
|
+
deciding which page the group lands in — bounded by one parent page per entry, since the exact
|
|
242
|
+
parent-page count depends on the placement being decided. Without that, a build simply refuses
|
|
243
|
+
itself: the first attempt put a 16 KB-bounded page at 16,396 bytes.
|
|
244
|
+
|
|
245
|
+
The other is that nothing has to be rebuilt. `parent_table_offset` reads zero on every container
|
|
246
|
+
written before this, which is exactly "no upward edge", so a version-4 reader takes old files and a
|
|
247
|
+
version-3 reader takes new ones. `min_reader_version` did not move. **The 2,975 converted Zorah
|
|
248
|
+
containers are all version 3**, so the coarsen path of §7.5 and the frontier of §7.6 meet assets
|
|
249
|
+
with no parent tables and have to fall back rather than assume — the same fallback §4 already
|
|
250
|
+
defines, reached for a different reason.
|
|
251
|
+
|
|
252
|
+
Mirror the child table, minus everything a parent does not need. A child reference must carry a
|
|
253
|
+
fetch range because the child may be absent; a parent reference never does, because a parent of a
|
|
254
|
+
drawn group is resident by the invariant above. So this is 4 bytes an entry, not 48.
|
|
255
|
+
|
|
256
|
+
**Group record (§6, 8 B — no growth).** The `reserved u16` at offset 6 becomes `parent_first`, an
|
|
257
|
+
index into the page's group-parent table. `parent_cluster_count` at offset 4 is already the length.
|
|
258
|
+
|
|
259
|
+
| offset | type | field | |
|
|
260
|
+
|---:|---|---|---|
|
|
261
|
+
| 0 | u16 | cluster_first | unchanged |
|
|
262
|
+
| 2 | u16 | cluster_count | unchanged |
|
|
263
|
+
| 4 | u16 | parent_cluster_count | unchanged — now also the length of the list below |
|
|
264
|
+
| 6 | u16 | **parent_first** | was `reserved` |
|
|
265
|
+
|
|
266
|
+
**Group-parent table (new, in the decoded page).** `parent_entry_count` × 4 B:
|
|
267
|
+
|
|
268
|
+
| offset | type | field | |
|
|
269
|
+
|---:|---|---|---|
|
|
270
|
+
| 0 | u16 | parent_ref | index into the page's parent-page table, or `SAME_PAGE` |
|
|
271
|
+
| 2 | u16 | parent_cluster | cluster index within that page |
|
|
272
|
+
|
|
273
|
+
**Parent-page table (new, in the decoded page).** `parent_page_count` × 4 B, deduplicated, one entry
|
|
274
|
+
per distinct page holding a parent: just `page_index u32`, resolved through `vg_lookup` exactly as
|
|
275
|
+
the cut already resolves a child. It lives in the *decoded* page rather than beside the plaintext
|
|
276
|
+
child table, because nothing needs it before decode — no fetch range is computed from it — and in
|
|
277
|
+
the blob it compresses with the other tables (§5: tables are 7% of page bytes at 1.6×). It is not
|
|
278
|
+
optional per page, so it also raises `decoded_size` and therefore the RAM tier's budget by the same
|
|
279
|
+
fraction *(rev 2)*.
|
|
280
|
+
|
|
281
|
+
**PageHeader (§6, 56 B — no growth).** Two of the eight reserved bytes at offset 24 become
|
|
282
|
+
`parent_table_offset u32`; two more `parent_entry_count u16` and `parent_page_count u16`.
|
|
283
|
+
|
|
284
|
+
**`parent_first` is capped, not widened — answered *(§7.3)*.** The concern was real:
|
|
285
|
+
`parent_entry_count` is Σ `parent_cluster_count` over a page's groups and `group_count` is itself a
|
|
286
|
+
u16, so the arithmetic does not forbid overflow the way `child_ref` does. The measurement says the
|
|
287
|
+
distance is not close. Across **8,899 pages of 30 converted Zorah containers**, from 1 MB to 120 MB:
|
|
288
|
+
|
|
289
|
+
| | mean | worst |
|
|
290
|
+
|---|---:|---:|
|
|
291
|
+
| parent entries per page | 17.4 | **26** |
|
|
292
|
+
| parent pages per page | 4.9 | 18 |
|
|
293
|
+
| parents of one group | — | 5 |
|
|
294
|
+
|
|
295
|
+
The u16 bound stands **2,521× above the worst real page**, and 40 of 8,899 pages carry no parent
|
|
296
|
+
entry at all. So the cap is `VGEO_MAX_PARENT_ENTRY_COUNT` and the group record keeps its 8 bytes.
|
|
297
|
+
The cost is also smaller than §2.1 estimated: 17.4 + 4.9 entries at 4 B is about 90 bytes against a
|
|
298
|
+
56–256 KB page, a tenth of a percent rather than "well under 1%".
|
|
299
|
+
|
|
300
|
+
**Cost.** Sum over groups of `parent_cluster_count` × 4 B, plus 4 B per distinct parent page. At the
|
|
301
|
+
converted assets' shape — a few parents per group — that is single-digit bytes per group against
|
|
302
|
+
56–256 KB pages: well under 1%, and compressible.
|
|
303
|
+
|
|
304
|
+
**Arena (runtime).** The page slot gains a fourth section:
|
|
305
|
+
`[header][node rows][group rows][cluster rows][parent rows]`, addressed by a `vg_parent_base` that
|
|
306
|
+
follows the existing pattern, with `PARENT_ENTRY_COUNT` in the page header row and `PARENT_FIRST` in
|
|
307
|
+
the group row. The slot stride is already a uniform (`page_slot_words`), so nothing else moves.
|
|
308
|
+
|
|
309
|
+
**Build invariants to add (§11).** Every parent reference resolves to a cluster whose `child_group`
|
|
310
|
+
is this group; `parent_cluster_count` equals the list length; the list is deduplicated and sorted by
|
|
311
|
+
(page_index, cluster), so the election in §3 is deterministic on the GPU.
|
|
312
|
+
|
|
313
|
+
## 3. The frame
|
|
314
|
+
|
|
315
|
+
Per view, one dispatch over the frontier, replacing the seed:
|
|
316
|
+
|
|
317
|
+
1. **Validate** (§4). An entry that fails takes its instance out of the incremental path for this
|
|
318
|
+
frame.
|
|
319
|
+
2. **Test.** For each cluster of each frontier group, the existing rule, unchanged — draw, clamp,
|
|
320
|
+
want.
|
|
321
|
+
3. **Move.** `project(self) > τ` and the child group selectable → the child group enters the
|
|
322
|
+
frontier, under the refine budget. `project(parent) ≤ τ` → its parents' groups enter,
|
|
323
|
+
unbudgeted (§1). Both are the existing descent code, seeded differently.
|
|
324
|
+
|
|
325
|
+
**G leaves only when every one of its clusters has moved *(§7.2)*.** Rev 2 wrote "and G leaves
|
|
326
|
+
it", which is wrong: `project(self)` is per cluster, not group-uniform — §1's own list says so —
|
|
327
|
+
and a group whose clusters disagree has to stay in the frontier for the ones still drawing while
|
|
328
|
+
its child groups enter for the ones refining. That is not an edge case. Measured on a converted
|
|
329
|
+
container at a walking camera, **47% of frontier entries draw some clusters and refine others**
|
|
330
|
+
(34.6% on a smaller one), so removing G on the first cluster that refines would drop about half
|
|
331
|
+
the frontier's drawn surface every frame. An entry's departure is therefore a count reaching
|
|
332
|
+
zero, not a flag being set, and that is one more word per entry rather than a different design.
|
|
333
|
+
|
|
334
|
+
The budget is **one bound per view per frame**, streaming: entries are taken in frontier order
|
|
335
|
+
from a persistent cursor, so a frame that runs out resumes where it stopped rather than starving
|
|
336
|
+
whatever sorts last. One frontier serves **every expansion site of a view** — it describes where
|
|
337
|
+
the camera is, not what the depth prepass concluded — and no site can move an entry another
|
|
338
|
+
already moved, because their instance sets are disjoint. See §6.3 for what "disjoint" was
|
|
339
|
+
verified to mean, and for the two things rev 2 got wrong about those sites.
|
|
340
|
+
4. **Draw** is unchanged, and so is everything downstream of `out_meshlets`.
|
|
341
|
+
|
|
342
|
+
**Dedup is a refine-side problem only *(rev 2)*.** A cluster names exactly one `child_group`, so
|
|
343
|
+
distinct groups have disjoint parent sets and coarsening can never append the same group twice — no
|
|
344
|
+
claim, no election. Refinement can: `parent_cluster_count > 1` is precisely several clusters naming
|
|
345
|
+
one child group. Because §2 makes their decisions bit-identical, the append can be elected rather
|
|
346
|
+
than deduplicated — a refining cluster appends the child group only if it is the **first entry of
|
|
347
|
+
that group's parent list**, which §2.1's sorted, deduplicated table makes a single comparison. No
|
|
348
|
+
bitmask, and nothing sized by instances × groups.
|
|
349
|
+
|
|
350
|
+
A camera cut is the one case where a bounded number of refine steps is not enough. It is also the
|
|
351
|
+
case the view already detects — `indicate_view_change` exists, and per-view previous-camera state is
|
|
352
|
+
already kept — so the answer is to drop the frontier and take today's root descent for that frame,
|
|
353
|
+
at today's cost, which is exactly what §4's fallback already does for a different reason. That
|
|
354
|
+
signal is a TAA hint rather than a statement about the cut, so the durable form is a counter: an
|
|
355
|
+
instance whose frontier wants to move more than K entries in one frame is cheaper to re-descend
|
|
356
|
+
than to walk *(rev 2)*.
|
|
357
|
+
|
|
358
|
+
## 4. Validation, without coupling pages to cuts
|
|
359
|
+
|
|
360
|
+
Do not maintain page → cut back-references. The entry names a `page_row`; let the row say whether it
|
|
361
|
+
still holds the occupant the entry was written against.
|
|
362
|
+
|
|
363
|
+
**Use the generation, not `visible_from` *(rev 2)*.** Rev 1 proposed mirroring
|
|
364
|
+
`GPUVirtualGeometryPool#visible_from[page_row]` — "the frame whose flush publishes the row's current
|
|
365
|
+
occupant" — and comparing `visible_from[r] ≤ F`. That works for the *feedback* path, where F is the
|
|
366
|
+
frame the cut ran and the record is read on the CPU, but on the GPU it makes every entry carry a
|
|
367
|
+
frame number and every check an ordering argument, including the case of a row installed twice
|
|
368
|
+
within one frame. `VGeoRuntimeTables#generations[page_row]` is a plain counter bumped on install:
|
|
369
|
+
the entry stores the generation it saw, validity is one equality, and there is no clock. Mirror that
|
|
370
|
+
word into the arena page header at install — one `u32`, written where every other page field is.
|
|
371
|
+
|
|
372
|
+
Together with `VGEO_GROUP_FLAG_SELECTABLE` for the parent side, it covers both failure modes: the
|
|
373
|
+
row was refilled, or the way up was evicted.
|
|
374
|
+
|
|
375
|
+
**The fallback is not repair, and it cannot be deferred.** An invalid entry disqualifies its
|
|
376
|
+
*instance* for the frame, which then seeds from the root — today's path, today's cost, already
|
|
377
|
+
correct. It must happen in the **same frame**: a dropped entry's surface is not covered by anything
|
|
378
|
+
else, because the stale cut had already descended past its ancestors, so postponing the re-descent
|
|
379
|
+
is a hole rather than a coarse stand-in *(rev 2)*. Steady state is incremental; residency churn
|
|
380
|
+
degrades to what we have now, which is also exactly when the cut is changing anyway.
|
|
381
|
+
|
|
382
|
+
**What that costs, stated honestly *(rev 2)*.** Because the fallback is same-frame, the graph must
|
|
383
|
+
keep encoding a ladder deep enough for a worst-case root descent every frame, even on frames where
|
|
384
|
+
no instance uses it. The frontier removes the ~41 µs first traverse; it does **not** remove the ~50
|
|
385
|
+
µs of empty prepare/traverse pairs that the ladder costs when every dispatch is empty. So the
|
|
386
|
+
expected win is roughly half of today's cut, not all of it, and the next lever after this is the
|
|
387
|
+
ladder itself — fusing prepare into traverse, or finding a bound that lets the encode shrink. Rev 1
|
|
388
|
+
claimed the ladder could collapse to a single pair; it cannot, and §6.5 is struck.
|
|
389
|
+
|
|
390
|
+
## 5. Per view, and CSM
|
|
391
|
+
|
|
392
|
+
Frontier entries are groups, and the measured shape is **4.0 clusters per group, 3.2 of them
|
|
393
|
+
selected** (§7.2, converted container) rather than the ~8 rev 2 assumed — so a scene drawing 31,744
|
|
394
|
+
clusters is ~10,000 entries, not ~4,000. An entry is `(page_row, group, generation)` plus the mesh
|
|
395
|
+
row it belongs to and the departure count §3 now needs — 16 B packed, or 12 B if the table is
|
|
396
|
+
segmented per mesh row rather than carrying it — so that scene is **120–160 KB per view**, and a
|
|
397
|
+
scene drawing a million clusters is ~5 MB per view. Five views (main plus four cascades) is tens of
|
|
398
|
+
MB rather than single-digit. Still a fraction of what storing the cut per *cluster* would cost, and
|
|
399
|
+
still affordable, but it is the number to size the buffer from.
|
|
400
|
+
|
|
401
|
+
Note that "per view" is the frontier's granularity and **not** the granularity the runtime can
|
|
402
|
+
express today: §6.3 found the params buffer is frame-global, collapsed across sites with a `Math.min`,
|
|
403
|
+
and `projection_k` is set once in `begin_frame` from one view's screen height. Per-view anything is
|
|
404
|
+
work §7.6 has to do first, and it is not small.
|
|
405
|
+
|
|
406
|
+
Cascades are the good case: a snapped cascade that did not move needs no update at all, and its
|
|
407
|
+
frontier is stable for many frames. But a cascade that *does* snap changes its projection wholesale,
|
|
408
|
+
which flips many decisions at once — that is the worst case for the budget, and it is exactly when
|
|
409
|
+
the counter in §3 should give up and re-descend rather than walk *(rev 2)*.
|
|
410
|
+
|
|
411
|
+
Per-view state is not novel here — the HZB and the previous camera are already per view — but the
|
|
412
|
+
closer precedent is `VGeoRuntimeTables`: selectability is a derived structure over the page DAG that
|
|
413
|
+
is *kept and patched on install and evict* rather than recomputed. This is that pattern applied to
|
|
414
|
+
the view-dependent half, with the same eviction hooks.
|
|
415
|
+
|
|
416
|
+
## 6. Open — the things still to settle
|
|
417
|
+
|
|
418
|
+
1. ~~Do all parents of a group come from one consuming step?~~ **Answered *(rev 2)*: yes.** One step
|
|
419
|
+
builds one group; both sides of the step carry bit-identical error and sphere; the validator
|
|
420
|
+
enforces it. Parents may still live in several *pages* — that is what
|
|
421
|
+
`parent_cluster_count`'s "`SAME_PAGE` included" means — which is why §2.1's table carries page
|
|
422
|
+
references rather than cluster indices alone.
|
|
423
|
+
2. ~~Frontier dedup.~~ **Answered *(rev 2)*: election over the sorted parent list, refine side
|
|
424
|
+
only** (§3). No bitmask.
|
|
425
|
+
3. ~~Is the frontier per view or per (view, bucket)?~~ **Answered: per view, and the partition is
|
|
426
|
+
verified — with two corrections *(§7.2)*.**
|
|
427
|
+
|
|
428
|
+
The load-bearing half holds. A view's expansion sites receive **disjoint** instance sets, and it
|
|
429
|
+
is disjoint by construction at every step: `shader_instances_rasterization_bucket_sort` derives
|
|
430
|
+
`bucket_index` from the mesh's one material and does a single `atomicAdd` and a single write, so
|
|
431
|
+
a mesh row lands in exactly one bucket; `shader_hzb_mesh_filter_2way` is one `if`/`else` on one
|
|
432
|
+
predicate with one append in each arm, so within a bucket positive and maybe cannot both hold a
|
|
433
|
+
row; and phase two consumes only `instances_maybe` through a one-way filter that appends a
|
|
434
|
+
subset. No instance is cut twice, so one frontier per view is safe and nothing can move an entry
|
|
435
|
+
another site already moved.
|
|
436
|
+
|
|
437
|
+
**There are not two sites — and that is a defect, not a shape to design around.** The
|
|
438
|
+
phase-one seam call sits *inside* the `ShadeDrawMode × ShadeDrawSide` loop of
|
|
439
|
+
`graph_rasterize_partial_opaque`, so a standard view records one cut per **active opaque bucket**
|
|
440
|
+
plus one for phase two. The engine already says so where rev 2 did not read it —
|
|
441
|
+
`GPUVirtualGeometryRuntime`: "once per bucket per HZB pass per view".
|
|
442
|
+
|
|
443
|
+
**Phase two already does it the right way**, which is what makes this a fix rather than a design
|
|
444
|
+
question. `graph_rasterize_remaining_opaque` filters its instances once, expands statics once,
|
|
445
|
+
cuts once, filters meshlets once, and only then calls `graph_sort_meshlets_by_bucket_id` and
|
|
446
|
+
loops the buckets to rasterize each slice with its own primitive state. Phase one is the outlier,
|
|
447
|
+
and the asymmetry is visible in its own buffers: `instances_maybe` is a single shared collection
|
|
448
|
+
every bucket iteration appends into, while `instances_positive` is created fresh per bucket.
|
|
449
|
+
|
|
450
|
+
What per-bucket costs: a cut is a whole ladder, and an idle prepare/traverse pair measures
|
|
451
|
+
**3.07 µs** on this machine (§0), so `max_iterations` 8 is ~25 µs of ladder per site before any
|
|
452
|
+
work, plus collect, seed and remainder. Nine active opaque buckets is nine of those where one
|
|
453
|
+
would do — and nine static expansions of the same instance set as well. **Decided: one cut per
|
|
454
|
+
view, bucket the meshlets afterwards**, which is the phase-two shape applied to phase one.
|
|
455
|
+
|
|
456
|
+
**The sets are disjoint but do not cover.** An instance reaches zero sites when it is
|
|
457
|
+
non-opaque, when its bucket is inactive, when phase two's re-test finds it occluded, or when the
|
|
458
|
+
2-way filter overflows. That is harmless here for the reason §1 gives — an instance not cut this
|
|
459
|
+
frame simply does not move its entries, and a stale frontier is never wrong — but "partition" was
|
|
460
|
+
the wrong word and the design should not lean on coverage anywhere else.
|
|
461
|
+
4. ~~Budget shape.~~ **Answered: per view, and streaming.** One bound on entries moved per view per
|
|
462
|
+
frame, held across instances rather than divided among them, with a persistent cursor into the
|
|
463
|
+
frontier so that an instance the bound cut off is where the next frame starts. Coarsening is
|
|
464
|
+
unbudgeted (§1), so the bound is on the descent alone.
|
|
465
|
+
5. ~~What `descent_steps` becomes.~~ Struck *(rev 2)*: §4 shows the ladder has to stay encoded for
|
|
466
|
+
the fallback, so the knob is a tuning question for the delta path (2–4) and a separate one for
|
|
467
|
+
the fallback path (8), not a collapse.
|
|
468
|
+
6. ~~**`parent_first` width** — cap or widen.~~ **Answered: cap *(§7.3)*.** The histogram is in
|
|
469
|
+
§2.1 — worst page 26 entries against a 65,535 bound, so the group record keeps its 8 bytes.
|
|
470
|
+
7. ~~What the probe measures next.~~ **Answered *(§7.2)*.** Spread transforms are in §0. The churn
|
|
471
|
+
number, measured by running the CPU reference's descent at consecutive camera poses and diffing
|
|
472
|
+
the reached group sets — a frontier entry being a group with at least one selected cluster — on a
|
|
473
|
+
converted container of 1.2M faces and 694 pages:
|
|
474
|
+
|
|
475
|
+
| motion | radii/frame | frontier | entered refine | entered coarsen | left | moves/frame |
|
|
476
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
477
|
+
| orbit | 0.002 | 168 | 0.1 | 0.0 | 0.1 | 0.08% |
|
|
478
|
+
| orbit | 0.01 | 176 | 0.7 | 0.0 | 0.4 | 0.44% |
|
|
479
|
+
| orbit | 0.05 | 196 | 2.6 | 0.8 | 3.1 | 1.71% |
|
|
480
|
+
| dolly | 0.005 | 26 | 0.2 | 0.0 | 0.1 | 0.78% |
|
|
481
|
+
| dolly | 0.02 | 37 | 1.1 | 0.0 | 0.4 | 2.93% |
|
|
482
|
+
| dolly | 0.05 | 136 | 13.5 | 0.1 | 6.6 | 10.02% |
|
|
483
|
+
|
|
484
|
+
**The budget is small.** Under 2% of the frontier moves per frame at any orbit speed worth
|
|
485
|
+
rendering, and a dolly fast enough to cross the object in twenty frames reaches 10%. A bound of
|
|
486
|
+
~16 entries a view a frame covers everything but the fastest dolly, and §3's counter is what
|
|
487
|
+
catches that one. Refinement dominates entries by better than 10:1 for the reason §2 now gives.
|
|
488
|
+
|
|
489
|
+
Two caveats on these numbers. The container did not fit the RAM tier — 232 of 694 pages — so a
|
|
490
|
+
frontier here is smaller than a fully resident one would be; and every entering group was
|
|
491
|
+
classified as arriving from a neighbour it had in the previous frontier (the "other" column was
|
|
492
|
+
0.0 throughout), which says the churn measured is the camera and not the stream.
|
|
493
|
+
|
|
494
|
+
## 7. Order, and what each step leaves green
|
|
495
|
+
|
|
496
|
+
1. ~~`descent_steps` default 32 → 8~~ — **done as 32 → 24**, because the confirmation failed:
|
|
497
|
+
8 leaves `QUEUE_REMAINING` at 55–208 on the deepest converted Zorah container, and the model
|
|
498
|
+
that predicted otherwise is corrected in §0. 24 needs the same six dispatches 32 does at every
|
|
499
|
+
camera tried, so it costs no margin; the selected set is bit-identical, which is what makes it
|
|
500
|
+
no behaviour change. Independent of the rest, as planned.
|
|
501
|
+
2. ~~The probe additions in §6.7~~ — **done.** Spread transforms fold into §0, the churn table
|
|
502
|
+
into §6.7, and the partition verification into §6.3. Three of them changed the design rather
|
|
503
|
+
than confirming it: mixed groups mean a frontier entry leaves on a count reaching zero and not
|
|
504
|
+
on one cluster refining (§3); a view has one site per active opaque bucket plus one, not two
|
|
505
|
+
(§6.3); and "per view" is not a granularity the runtime can express yet (§5).
|
|
506
|
+
3. ~~Format: the parent tables~~ — **done**, as `format_version` 4. `VGEO_FORMAT.md` §6.3, §6.4,
|
|
507
|
+
§11.15 and §11.16; the cap decision in §2.1; the writer builds the edge, the assembler budgets
|
|
508
|
+
for it, the reader parses it and the validator holds it to the cluster records. Round-trip
|
|
509
|
+
coverage both ways: the reader's tables must equal the relation rebuilt from `child_group`, and
|
|
510
|
+
three corruptions — a parent entry naming the wrong cluster, a run that does not tile, a
|
|
511
|
+
parent-page table that is not ascending — are each caught. Nothing consumes the tables yet, and
|
|
512
|
+
the runtime is green at 301.
|
|
513
|
+
4. ~~Arena: the fourth section, `PARENT_FIRST`, and the generation mirror~~ — **done.** The slot
|
|
514
|
+
is `[header][node rows][group rows][cluster rows][parent rows]`, addressed by `vg_parent_base`;
|
|
515
|
+
a parent row resolves to a global page index at install exactly as a cluster row does, so a walk
|
|
516
|
+
up reads the shape the descent already reads. The group row took a fifth word rather than
|
|
517
|
+
padding to eight, and the page row took `PARENT_ENTRY_COUNT` — which is what a coarsening kernel
|
|
518
|
+
checks instead of asking an asset its version — and `GENERATION`. `slot_range` learned the
|
|
519
|
+
section, or the parent rows would never have been uploaded. Still nothing consumes them.
|
|
520
|
+
|
|
521
|
+
**A verification surface went away mid-session, and §7.5 needs it.** The `vgeo_runtime`
|
|
522
|
+
playground draws nothing in headless Chrome: `begin_frame` runs, the feedback resource is open,
|
|
523
|
+
the adapter is an NVIDIA Lovelace with subgroups, the container builds and its root page installs
|
|
524
|
+
with a selectable group — and `VGEO_CUT_STAT.PAGES` stays 0, so the seed never reaches its first
|
|
525
|
+
line and nothing reports an error.
|
|
526
|
+
|
|
527
|
+
It is **not** any of steps 1–4. It reproduces in a detached worktree at 5289ade4f, a commit whose
|
|
528
|
+
only change is Markdown, and the probes in §0 and §6.7 above were taken through this same surface
|
|
529
|
+
earlier the same day. What changed in between is the browser: Chrome stable installed
|
|
530
|
+
**153.0.8010.37 at 21:28**, between the last probe that drew (21:07) and the first that did not.
|
|
531
|
+
Dawn in 153 passes indirect-dispatch validation through immediate data and overwrites the
|
|
532
|
+
application's, so every `var<immediate>` read by a shader launched with
|
|
533
|
+
`dispatchWorkgroupsIndirect` reads 65,535 — and `shader_hzb_mesh_filter_2way` reads
|
|
534
|
+
`bucket_index` that way, emits no survivors, and hands the seam an empty instance list. That is a
|
|
535
|
+
browser regression with its own investigation; the one thing that does not fit is that Canary
|
|
536
|
+
155.0.8057.0 reproduced it here too, which is worth handing back to whoever owns that.
|
|
537
|
+
|
|
538
|
+
So §7.5's gate is the agreement spec, which is what this plan asked for anyway: a coarsen path
|
|
539
|
+
can be validated and run against the CPU reference under the emulator, and cannot be compiled by
|
|
540
|
+
Tint until a browser on this machine draws again.
|
|
541
|
+
5. ~~A `coarsen` path in the traversal kernel~~ — **done**, behind `VGEO_CUT_FLAG_COARSEN`, and
|
|
542
|
+
the gate holds: a cut seeded at the leaf pages and walked up draws exactly what the CPU oracle
|
|
543
|
+
walks up to, which is exactly what `vgeo_select_cut` descends to. The same run with the flag off
|
|
544
|
+
draws **nothing**, so the walk is doing the work and not the seed.
|
|
545
|
+
|
|
546
|
+
The path is small because the kernel already had the branch. `vg_visit_cluster` identified
|
|
547
|
+
"below the cut" and returned nothing; now, under the flag, the group's first member expands into
|
|
548
|
+
its parent list instead. The election is free — `project(parent)` is group-uniform because every
|
|
549
|
+
cluster of a group shares one consuming step, so member 0 reaches the same answer as the rest and
|
|
550
|
+
the rest defer to it with nothing communicated. Each child is one entry of the parent rows, and
|
|
551
|
+
it claims the page it resolves to when it is *executed* rather than when produced: a group with
|
|
552
|
+
several parents would otherwise make its producer claim each, count only the ones it won, and
|
|
553
|
+
hand any lane a rule for building the k-th won page out of five numbers. One extra queue hop
|
|
554
|
+
costs less, on a path §6.7 measures at under one entry a frame.
|
|
555
|
+
|
|
556
|
+
**What it cost, and §7.6 needs to know.** The node metric's `max_parent_error` cull is a
|
|
557
|
+
**descent-only** accelerator and the flag turns it off. Its premise — a subtree nothing can be
|
|
558
|
+
drawn in is a subtree worth skipping — is exactly wrong for a walk up, where being below the cut
|
|
559
|
+
is what sends a group to its parents. With the cull on, a cut seeded at the leaves drew nothing
|
|
560
|
+
at all: the subtree carrying the whole walk was pruned at its first node, silently. A node has no
|
|
561
|
+
parent list of its own (the upward edge is per group), so there is nothing to coarsen from at
|
|
562
|
+
node granularity and entering the subtree is the only way to reach the groups that can.
|
|
563
|
+
|
|
564
|
+
So a page walked *up* through is evaluated whole, with no LOD pruning. That is affordable exactly
|
|
565
|
+
because §1's frontier means the walk is one or two levels rather than the depth of the DAG — it
|
|
566
|
+
is an argument for the frontier, not against the coarsen path, but it does mean the fallback in
|
|
567
|
+
§4 must stay a *descent* and never a walk up from below.
|
|
568
|
+
6. The frontier: storage keyed by mesh row (§1.1), the reseeded seed pass, validation, the budget.
|
|
569
|
+
Behind a flag, with the root descent as the fallback path it already is.
|
|
570
|
+
|
|
571
|
+
**6a — per-view state. Done.** A view owns `VGeoViewCut` beside its HZB and its previous camera,
|
|
572
|
+
and that is where a frontier will live. It carries this view's params buffer, written at the seam
|
|
573
|
+
where the view is known. One buffer per view rather than offsets into a shared one: views are
|
|
574
|
+
few, a uniform is tens of bytes, and packing them buys nothing against giving every per-view
|
|
575
|
+
value a second index to be wrong in. `begin_frame` takes one destructured argument and no
|
|
576
|
+
projection, because a frame does not have one.
|
|
577
|
+
|
|
578
|
+
That fixed a live defect on its own. `projection_k` is screen height over field of view, and one
|
|
579
|
+
buffer on the runtime made it a property of the frame, written by whichever view opened it — so a
|
|
580
|
+
shadow cascade selected a level of detail for a screen it is not drawn to. Pinned: two views
|
|
581
|
+
cutting in one frame must disagree about it, must each get what their own height and lens imply,
|
|
582
|
+
and must not be reading the same bytes.
|
|
583
|
+
|
|
584
|
+
**6b — one cut per view. Not done, and it is the next thing.** §6.3 has the shape and the
|
|
585
|
+
precedent: phase one restructures into the phase-two order — a shared positive collection, one
|
|
586
|
+
static expansion, one cut, one meshlet filter, `graph_sort_meshlets_by_bucket_id`, then the
|
|
587
|
+
bucket loop for rasterization alone. It is a change to the main opaque path for *all* geometry
|
|
588
|
+
rather than to anything under `virtual/`, and it cannot be looked at: §7.4's note explains why no
|
|
589
|
+
browser on this machine currently draws. That combination is why it is written down here rather
|
|
590
|
+
than taken.
|
|
591
|
+
|
|
592
|
+
**6c — the frontier. Done, in its simplest form, and that form is not the one §1 describes.**
|
|
593
|
+
|
|
594
|
+
A cut records the **page rows it drew from** into a per-view bitmask, and
|
|
595
|
+
`shader_vgeo_cut_reseed` puts those rows back on the queue the next frame instead of node 0
|
|
596
|
+
of the root. Measured on the agreement spec's torus with the camera unmoved: **19 wavefront
|
|
597
|
+
generations become 4**, `DRAWN` identical at 161. The ladder is what a cut is paid in, so
|
|
598
|
+
that is the whole of why a cut is kept.
|
|
599
|
+
|
|
600
|
+
**Pages, not groups.** §1 asks for a set of groups; this is a set of page rows. A page is the
|
|
601
|
+
unit the claim is built on and the unit a visit evaluates whole, so a reseeded page needs no
|
|
602
|
+
new item kind, no entry format, no allocator and no dedup beyond the claim the descent
|
|
603
|
+
already takes — and a 4,096-row tier is 128 words a view against the 120–160 KB §5 sizes a
|
|
604
|
+
group-level frontier at. Every group of a reseeded page is re-tested by the pure rule, and
|
|
605
|
+
testing one the cut did not need costs the test.
|
|
606
|
+
|
|
607
|
+
**Recorded at the draw, not at the claim.** Recording at the claim names every page the
|
|
608
|
+
descent passed *through*, and reseeding from that set is more work than the root descent it
|
|
609
|
+
replaces. That was the first version, and the "fewer pages" test is what caught it. A page
|
|
610
|
+
that drew something is a page the cut is standing on.
|
|
611
|
+
|
|
612
|
+
**Items is not the meter.** The coarsen flag turns off the node metric's LOD cull (§7.5), so
|
|
613
|
+
a reseeded page is evaluated whole and tests slightly *more* clusters — 463 against 463 → 476
|
|
614
|
+
— while visiting far fewer pages. Pages are what a fetch and a claim cost; wavefronts are what
|
|
615
|
+
the ladder costs.
|
|
616
|
+
|
|
617
|
+
**It spends the traversal's last storage binding.** The engine requires ten per stage of an
|
|
618
|
+
adapter and refuses one that offers fewer; the compile spec now pins traverse at exactly ten
|
|
619
|
+
and says there is no eleventh. Anything that kernel needs next must displace something or be
|
|
620
|
+
folded into a buffer it already binds.
|
|
621
|
+
|
|
622
|
+
What it gives up, and what is left:
|
|
623
|
+
|
|
624
|
+
- **Per-instance keying (§1.1) is not there.** Two instances of one geometry share a page
|
|
625
|
+
set — a superset of each one's own, so correct — and the reseed offers every page to every
|
|
626
|
+
instance, one lane per (instance, page row). Cheap at a handful of instances; not what a
|
|
627
|
+
thousand should do. Keying by mesh row is the change that lifts it, and it is where a group
|
|
628
|
+
frontier and §5's sizing come back.
|
|
629
|
+
- **The seam captures the frontier and does not yet read it.** Choosing reseed over seed per
|
|
630
|
+
view, and dropping a frontier whose tier has evicted since it was written — the pool's
|
|
631
|
+
eviction count against the one it was captured at, which is a host-side integer compare and
|
|
632
|
+
needs none of §4's per-entry generation — is the next step.
|
|
633
|
+
- **No budget.** §3's bound on entries moved per frame is not implemented, and §6.7 is why it
|
|
634
|
+
can wait: under 2% of a frontier moves per frame at any orbit speed worth rendering, and
|
|
635
|
+
the failure mode of no budget is a frame that does more work, never a wrong cut.
|
|
636
|
+
7. Delete nothing until a capture on the Zorah container shows the incremental path beating the root
|
|
637
|
+
descent on the same scene.
|