@woosh/meep-engine 3.60.4 → 3.60.6
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/CHANGELOG.md +2621 -0
- package/CONTRIBUTING.md +7 -0
- package/MIGRATION_2_TO_3.md +136 -0
- package/SECURITY.md +4 -0
- package/editor/EditorCameraNavigation.d.ts +2 -1
- package/editor/EditorCameraNavigation.d.ts.map +1 -1
- package/editor/EditorCameraNavigation.js +51 -57
- package/package.json +6 -2
- package/src/engine/EngineHarness.d.ts +3 -9
- package/src/engine/EngineHarness.d.ts.map +1 -1
- package/src/engine/EngineHarness.js +6 -8
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
- package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
- package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
- package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
- package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
- package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
- package/src/engine/graphics/ecs/water/Water.js +8 -0
- package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/WaterSystem.js +731 -734
- package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
- package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
- package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
- package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
- package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
- package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
- package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
- package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
- package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
- package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
- package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
- package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/water_refraction/README.md +5 -6
- package/src/shade/playground/water_refraction/index.html +1 -1
- package/src/shade/renderer/Renderer.d.ts +11 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +50 -6
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
- package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
- package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
- package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
- package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
- package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +28 -45
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +111 -119
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +22 -3
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
- package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +84 -3
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts +19 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_proxy_triangle_limit.js +25 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts +15 -0
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VirtualGeometry.js +38 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts +28 -59
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGeoSDFProxies.js +84 -789
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +7 -6
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +12 -11
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.d.ts +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/brick/mesh/chunk_mesh_brick_instance.js +2 -2
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
- package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
- package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
- package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
- package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
- package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
- package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
- package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
- package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
- package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts +11 -1
- package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
- package/src/shade/renderer/material/GPUMaterialManager.js +16 -8
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
- package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
- package/src/shade/renderer/postprocess/gtao/GTAO.d.ts +7 -3
- package/src/shade/renderer/postprocess/gtao/GTAO.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/GTAO.js +26 -13
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts +14 -0
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/ao_reproject_shader.js +196 -68
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao.js +42 -28
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.d.ts.map +1 -1
- package/src/shade/renderer/postprocess/gtao/fragment_shader_gtao_with_irradiance.js +429 -414
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts +16 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.d.ts.map +1 -0
- package/src/shade/renderer/postprocess/gtao/shader_gtao_spatial_denoise.js +156 -0
- package/src/shade/renderer/scene/GPUSceneContext.d.ts +2 -2
- package/src/shade/renderer/scene/GPUSceneContext.js +5 -5
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
- package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
- package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
- package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
- package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
- package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
- package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water.js +22 -13
- package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
- package/src/shade/renderer/water/shader_water_avboit.js +7 -7
- package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
- package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
- package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
- package/editor/REWORK_PLAN_2026_08.md +0 -692
- package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
- package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
- package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
- package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
- package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
- package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
- package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
- package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
- package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
- package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
- package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
- package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
- package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
- package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
- package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
- package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
- package/src/REVIEW_2026_08_06/adv-core.md +0 -511
- package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
- package/src/REVIEW_2026_08_06/adv-view.md +0 -446
- package/src/REVIEW_2026_08_06/api-core.md +0 -296
- package/src/REVIEW_2026_08_06/api-engine.md +0 -195
- package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
- package/src/REVIEW_2026_08_06/core-binary.md +0 -348
- package/src/REVIEW_2026_08_06/core-collection.md +0 -376
- package/src/REVIEW_2026_08_06/core-math.md +0 -221
- package/src/REVIEW_2026_08_06/core-process.md +0 -266
- package/src/REVIEW_2026_08_06/core-science.md +0 -223
- package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
- package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
- package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
- package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
- package/src/REVIEW_2026_08_06/engine-input.md +0 -197
- package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
- package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
- package/src/REVIEW_2026_08_06/generation.md +0 -176
- package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
- package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
- package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
- package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
- package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
- package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
- package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
- package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
- package/src/REVIEW_2026_08_06/view.md +0 -667
- package/src/REVIEW_2026_08_06.md +0 -610
- package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
- package/src/core/bvh2/bvh3/NOTES.md +0 -2
- package/src/core/bvh8/NOTES.md +0 -63
- package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
- package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
- package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
- package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
- package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
- package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
- package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
- package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
- package/src/core/geom/3d/gjk/NOTES.md +0 -8
- package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
- package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
- package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
- package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
- package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
- package/src/engine/REVIEW_2026_07_03.md +0 -211
- package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
- package/src/engine/control/first-person/DESIGN.md +0 -658
- package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
- package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
- package/src/engine/control/first-person/TODO.md +0 -160
- package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
- package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
- package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
- package/src/engine/ecs/ik/PLAN.md +0 -179
- package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
- package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
- package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
- package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
- package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
- package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
- package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
- package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
- package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
- package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
- package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
- package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
- package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
- package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
- package/src/engine/network/CONGESTION_CONTROL.md +0 -87
- package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
- package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
- package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
- package/src/engine/physics/cloth/PLAN.md +0 -3056
- package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
- package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
- package/src/engine/physics/fluid/solver/NOTES.md +0 -46
- package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
- package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
- package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
- package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
- package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
- package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
- package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
- package/src/format/image/avif/DECISIONS.md +0 -545
- package/src/format/image/jpeg/NOTES.md +0 -3
- package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
- package/src/generation/COORDINATES.md +0 -54
- package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
- package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
- package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
- package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
- package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
- package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
- package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
- package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
- package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
- package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
- package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
- package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
- package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
- package/src/shade/RENDERER_CONTRACT.md +0 -697
- package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
- package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
- package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
- package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
- package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
- package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
- package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
- package/src/shade/renderer/NOTES.md +0 -78
- package/src/shade/renderer/PLAN.md +0 -8
- package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
- package/src/shade/renderer/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
- package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
- package/src/shade/renderer/camera/orbital/README.md +0 -81
- package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
- package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
- package/src/shade/renderer/camera/orbital/index.js +0 -9
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
- package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
- package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
- package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
- package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
- package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
- package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
- package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
- package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
- package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
- package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
- package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
- package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts +0 -47
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/VGEO_SDF.js +0 -80
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts +0 -16
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_collect.js +0 -67
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts +0 -17
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_decode.js +0 -145
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts +0 -12
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/sdf/shader_vgeo_sdf_traverse.js +0 -105
- package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
- package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
- package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
- package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
- package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
- package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
- package/src/shade/renderer/hiz/NOTES.md +0 -3
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
- package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
- package/src/shade/renderer/light/cluster/NOTES.md +0 -11
- package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
- package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
- package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
- package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
- package/src/shade/renderer/particles/DESIGN.md +0 -1011
- package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
- package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
- package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
- package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
- package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
- package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
- package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
- package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
- package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
- package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
- package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
- package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
- package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
- package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
- package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
- package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
- package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
- package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
- package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
- package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
- package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
- package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
- package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
- package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
- package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
- package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
- package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
- package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
- package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
- package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
- package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
- package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
- package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
- package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
- package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
- package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
- package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
- package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
- package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
- package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
- package/src/shade/wgsl/NOTES.md +0 -4
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
# Virtual Geometry — Design
|
|
2
|
-
|
|
3
|
-
Status: **builder, container, reader and GPU runtime implemented; the runtime is under review and
|
|
4
|
-
not shippable** — see [runtime/REVIEW_LEDGER.md](./runtime/REVIEW_LEDGER.md), which found seven
|
|
5
|
-
critical defects including one that breaks §4's no-double-draw guarantee on the ordinary streaming
|
|
6
|
-
path. §10's M1 is written below as closed; treat it as *built and not yet trustworthy*. This document pins only what
|
|
7
|
-
is certain; everything else is listed explicitly under *Open*. Runtime home is this directory
|
|
8
|
-
(`shade/renderer/geometry/virtual/`, mirroring `texture/virtual/`); builder lives with
|
|
9
|
-
`core/geom/3d/topology/struct/binary/` and `core/graph/metis/`.
|
|
10
|
-
|
|
11
|
-
What exists, in the order a newcomer meets it: `build/` turns a `Geometry` into a cluster DAG,
|
|
12
|
-
`format/` writes and validates the `.vgeo` container ([VGEO_FORMAT.md](./VGEO_FORMAT.md)),
|
|
13
|
-
`format/read/` reads one back a page at a time with residency and activation, `residency/` owns the
|
|
14
|
-
RAM tier's budget, and `runtime/` is everything §7 describes — the selection buffer, the compute cut
|
|
15
|
-
pass, the VRAM page pool, the feedback loop, and the seam through which VG instances enter the
|
|
16
|
-
meshlet pipeline ([runtime/RUNTIME_PLAN.md](./runtime/RUNTIME_PLAN.md)).
|
|
17
|
-
|
|
18
|
-
`shade/playground/vgeo_runtime/` puts an ordinary `Mesh` in an ordinary `Scene` and draws it
|
|
19
|
-
entirely from a **GPU** cut through the meshlet pipeline — it proves the engine seam, and
|
|
20
|
-
`shade/playground/vgeo_scene/` opens a whole converted set at once, where the claim about a *scene*
|
|
21
|
-
is measured. What proves the DAG, the format and the clamp is not a page at all: it is
|
|
22
|
-
`runtime/cut/__fixtures__/vgeo_select_cut.js`, a CPU traversal of the same rule that every cut
|
|
23
|
-
spec is held to. A third playground once drew that traversal on screen through Shade's direct
|
|
24
|
-
geometry path, with its own renderer and its own streaming loop; once the GPU page could show
|
|
25
|
-
everything it showed, it was a second runtime for no gain, and it was retired. §10 says which milestone that leaves us
|
|
26
|
-
in, and the order the work actually arrived in.
|
|
27
|
-
|
|
28
|
-
## 1. What and why
|
|
29
|
-
|
|
30
|
-
Cluster-DAG continuous LOD (Nanite-family): the builder turns a source mesh into a DAG of
|
|
31
|
-
fixed-size clusters at halving detail levels; the runtime selects a *cut* through that DAG
|
|
32
|
-
each frame so that geometric error stays below a pixel threshold. Rendering cost becomes a
|
|
33
|
-
function of resolution, not of source triangle count, and the asset streams — only a
|
|
34
|
-
fraction is ever resident in RAM, and a smaller fraction in VRAM.
|
|
35
|
-
|
|
36
|
-
VG is an **enabler and scaler**, not a replacement for good art practice. The stress cases
|
|
37
|
-
that motivate it: CAD / digital-twin data (inconsistent density, no authored LODs, e.g.
|
|
38
|
-
thousands of unique fasteners), large scans, and film-quality assets — up to billions of
|
|
39
|
-
source triangles per scene. Instant first pixel on cold load is a hard goal.
|
|
40
|
-
|
|
41
|
-
## 2. Fixed points
|
|
42
|
-
|
|
43
|
-
What exists today, with its real contract: **shader-visible record formats and the
|
|
44
|
-
algorithmic primitives are the fixed points; the managers and render-graph seams were
|
|
45
|
-
built for whole-mesh static residency and need a VG-specific layer on top** — which the
|
|
46
|
-
hands-off principle argues for anyway.
|
|
47
|
-
|
|
48
|
-
- **Mesh structure**: `BinaryTopology` (BMesh-style, binary pools, float32 coordinates),
|
|
49
|
-
with `bt_mesh_validate`, `bt_merge_vertices_by_distance` (float32 weld quantum floor;
|
|
50
|
-
its contract requires a short-edge kill/collapse pre-pass — §8), boundary queries.
|
|
51
|
-
- **Simplifier**: `bt_mesh_simplify(mesh, target_face_count, restricted_vertices,
|
|
52
|
-
protected_faces)` — greedy quadric edge collapse, optimal placement, pinned vertices neither
|
|
53
|
-
moved nor removed, protected faces never destroyed. Required extensions (all builder-side;
|
|
54
|
-
three of the four have since landed, and the list is kept because the reasoning is what makes
|
|
55
|
-
the fourth findable):
|
|
56
|
-
1. ~~report the max/accumulated quadric residual of a pass (§4)~~ **done**. It returns the
|
|
57
|
-
object-space distance the surface moved, measured *before* the cleanup pass — see item 2,
|
|
58
|
-
where that ordering turned out to be most of the fix;
|
|
59
|
-
2. ~~boundary constraint quadrics~~ **done**. Open and non-manifold edges now carry a
|
|
60
|
-
constraint plane through the edge perpendicular to its face, so a border can no
|
|
61
|
-
longer retreat at a recorded cost of zero — which it could, and which the round loop
|
|
62
|
-
leaned on, since converging a mesh of open shells means eroding them. Measured
|
|
63
|
-
before: 400 loose triangles built a root of 25 carrying `self_error = 0`, selected at
|
|
64
|
-
every distance and never refined. A second hole went with it: the error was taken
|
|
65
|
-
over *surviving* vertices, and geometry deleted outright leaves no survivor to carry
|
|
66
|
-
it, so `bt_mesh_simplify` now measures before its cleanup pass and returns an
|
|
67
|
-
object-space distance rather than a raw residual. A flat sheet still reports exactly
|
|
68
|
-
zero, which is the other half of the contract. The same mechanism later serves UV
|
|
69
|
-
seams (§9);
|
|
70
|
-
3. triangle-inversion (flip) rejection — **still open, and the only one of the four that
|
|
71
|
-
is**. Pin-forced placement along locked group perimeters is exactly where greedy QEM folds
|
|
72
|
-
over. Nothing in `bt_mesh_simplify` tests the sign of a face normal before or after a
|
|
73
|
-
collapse, and no build validation catches a fold, so this is a real gap rather than a
|
|
74
|
-
latent one;
|
|
75
|
-
4. ~~group-boundary **polyline** preservation~~ **done**. Pinning vertices does not by itself
|
|
76
|
-
protect the boundary *edges* between them: a collapse elsewhere in the fan can take away
|
|
77
|
-
the only face holding one, and the cleanup pass then removes it. `protected_faces` names
|
|
78
|
-
the faces carrying a group's boundary polyline, and `vgeo_build_levels` re-walks every
|
|
79
|
-
boundary edge after the call and throws if one is gone — a missing boundary edge is a crack
|
|
80
|
-
between two LODs that never closes, so the build stops rather than shipping it.
|
|
81
|
-
- **Partitioner**: the ported METIS — `metis_partition_kway` (CSR in, deterministic via
|
|
82
|
-
fixed seed, single balance constraint, **unit vertex weights, no contiguity
|
|
83
|
-
guarantee**) and `cluster_mesh_metis(result, mesh, patch_size)` (face clustering with a
|
|
84
|
-
hard size ceiling, minimized edge cut, dense renumbering). Disconnected parts are
|
|
85
|
-
possible and matter (§3).
|
|
86
|
-
- **Bounds**: `Miniball` (minimal bounding sphere, iteration-capped). That is the whole list —
|
|
87
|
-
**normal cones are not part of this design**. Backface cone culling is declined outright (§9,
|
|
88
|
-
plan §5), so `cone_reserved` was reclaimed by `format_version` 5 (format §7), and
|
|
89
|
-
`bt_face_cluster_compute_normal_bounding_cone` — the `BinaryTopology` port of the old
|
|
90
|
-
`TopoMesh` cone builder — has no consumer and no prospect of one.
|
|
91
|
-
- **Renderer formats (the real seam)**: meshlets capped at 128 triangles / 128 vertices
|
|
92
|
-
(8-bit local indices); `MESHLET_METADATA_STRUCT`; the draw record
|
|
93
|
-
(`MESHLET_DEFINITION_STRUCT` = meshlet index + mesh); bucket pipeline issuing one
|
|
94
|
-
`drawIndirect` per material with instanceCount = meshlet count (no multi-draw needed);
|
|
95
|
-
HZB two-pass filters keyed on projected bounds against the previous-frame HZB (no
|
|
96
|
-
stale-ID interaction with a per-frame-changing cut); attribute encoding for
|
|
97
|
-
normals/tangents/colors (oct/packed). **Positions and UV0 are raw float32 today** —
|
|
98
|
-
cluster-local position quantization is new work spanning encode, metadata struct, and
|
|
99
|
-
every decode shader, not a finished capability.
|
|
100
|
-
- **Renderer managers (need a VG layer, not reuse as-is)**: `GPUMeshletManager` pools
|
|
101
|
-
metadata+data via OffsetAllocator. **Growth no longer re-packs**: it appends, files the new
|
|
102
|
-
tail as free space through `OffsetAllocator#grow`, copies each old buffer into the larger one
|
|
103
|
-
at offset 0, and moves no record — so no address is patched and no `changed` fires.
|
|
104
|
-
**Compaction still is a CPU-side rebuild** — new buffer pair (transient ~2× VRAM),
|
|
105
|
-
CPU-retained metadata mirrors, full re-upload — and it is what closes the holes growth leaves
|
|
106
|
-
behind, which under VG's churn is the case that matters rather than the one that got cheaper.
|
|
107
|
-
Downstream, meshlet-address migration is still an acknowledged unhandled TODO in
|
|
108
|
-
`GPUGeometryManager` (it logs a warning and continues); it is now reachable only through
|
|
109
|
-
compaction, which narrows the hole without closing it.
|
|
110
|
-
Mesh→meshlet expansion is one shared FrameGraph helper — `graph_expand_meshes_to_meshlets`,
|
|
111
|
-
called from four rasterization paths — and it reads a geometry's `meshlets_address` and
|
|
112
|
-
`meshlets_count` as one contiguous static range. VG's scattered, dynamic residency does not
|
|
113
|
-
fit that (§6, §7 state what replaces them).
|
|
114
|
-
- **Readback**: `graph_read_buffer` (MAP_READ scratch from the general buffer pool,
|
|
115
|
-
`mapAsync`, ≥1 frame latency). The virtual-texture system is the shipped precedent for
|
|
116
|
-
the whole feedback loop and pins its contract: in-flight readbacks capped, feedback
|
|
117
|
-
skipped entirely when saturated, occlusion-aware by construction (§6).
|
|
118
|
-
|
|
119
|
-
## 3. Structure: the cluster DAG
|
|
120
|
-
|
|
121
|
-
Pinned:
|
|
122
|
-
|
|
123
|
-
- **Leaf clusters = meshlets** (≤ 128 triangles), from `cluster_mesh_metis` on the source
|
|
124
|
-
mesh, followed by a **connected-components split** (metis parts can be disconnected;
|
|
125
|
-
disconnected clusters/groups inflate spheres and force the exact LOD-lockstep
|
|
126
|
-
over-rendering this design criticizes fixed-32 grouping for).
|
|
127
|
-
- **The round loop** (not a level sweep): hold a *frontier* of every cluster nothing has
|
|
128
|
-
merged yet; select a **band** of it by error; partition the band's *cluster adjacency
|
|
129
|
-
graph* (`metis_partition_kway`; edge weight = shared boundary size;
|
|
130
|
-
connected-components post-pass) into **groups**; lock every vertex on a group's outer
|
|
131
|
-
boundary (`restricted_vertices`); `bt_mesh_simplify` the group interior to ~50% of its
|
|
132
|
-
triangles; re-split the simplified group into ~half as many new clusters; those are the
|
|
133
|
-
*parents* of every cluster in the group. Parents go back into the frontier; clusters the
|
|
134
|
-
round did not use stay in it, untouched. Repeat until the frontier is one cluster — the
|
|
135
|
-
root. It is a DAG, not a tree: a parent spans a whole group of children.
|
|
136
|
-
- **The band is an error octave**, `[e, e · growth]` from the frontier's minimum, widened
|
|
137
|
-
until it covers at least a fixed share of the frontier. Every cluster in a group shares
|
|
138
|
-
the group's error, so a sweep that merges whatever happens to be on a level writes off
|
|
139
|
-
nearly-exact geometry at its cruder neighbour's error, and hands the runtime page bands
|
|
140
|
-
spanning an order of magnitude of error when the cut it takes is a *threshold* on error.
|
|
141
|
-
The minimum share is not optional: on error spread thinly and continuously — a UV
|
|
142
|
-
sphere, poles against equator — an octave alone holds a handful of clusters, and the
|
|
143
|
-
round count then scales with the cluster count rather than its logarithm, which no
|
|
144
|
-
`level_count` can survive and which a build refuses outright past
|
|
145
|
-
`VGeoLevelOptions.max_levels`. Measured on a 98k-triangle sphere against a sweep: growth
|
|
146
|
-
4 cuts clusters-dragged-up by 38% for one extra level; growth 2 cuts it 60% for seven.
|
|
147
|
-
On geometry of uniform error the band is the whole frontier and this *is* the sweep.
|
|
148
|
-
- **A level is an ordinal, not a generation.** A group's band ordinal is what orders pages;
|
|
149
|
-
its clusters may come from any level at or below it, because a cluster no round could use
|
|
150
|
-
waits. What the ordinal guarantees is the only thing the format needs: every group sits
|
|
151
|
-
strictly below the group holding its parents, so page order by descending level is a
|
|
152
|
-
topological order and every child reference points forward in the file.
|
|
153
|
-
- **The group is the atomic LOD decision unit.** Locked boundaries plus the group-uniform
|
|
154
|
-
cut rule (§4) is what makes independent per-cluster decisions crack-free.
|
|
155
|
-
- **Clusters own their payloads.** No vertex sharing across levels or clusters; each
|
|
156
|
-
cluster is an independently uploadable, contiguous, quantized meshlet. Duplication
|
|
157
|
-
along cluster borders is an accepted cost (cache coherency, page independence).
|
|
158
|
-
- **Group size is adaptive.** Fixed 4 (Nanite) starves the simplifier on small groups
|
|
159
|
-
(locked perimeter dominates); fixed 32 (Virtuos) locks a large spatial footprint into
|
|
160
|
-
LOD lockstep and over-renders under a conservative group error. Proven approach from
|
|
161
|
-
the prior WebGL implementation: start small, grow the group when decimation quality or
|
|
162
|
-
re-clustering quality is poor.
|
|
163
|
-
- **Termination is convergence to a single root, not a stall.** A round is accepted only
|
|
164
|
-
if it *strictly* shrinks the frontier; failing that it widens the band, then grows the
|
|
165
|
-
group. A build that cannot reach one root **throws** — the runtime starts its cut at the
|
|
166
|
-
root, and several parentless clusters is not a shape it can start from. The validator
|
|
167
|
-
enforces it from the bytes: exactly one cluster with `parent_error = +∞`, in page 0.
|
|
168
|
-
- **A step aims at a cluster count, and a cluster is bounded by two numbers.** What the
|
|
169
|
-
simplifier is handed is a triangle target, but a cluster is capped at 128 triangles *and*
|
|
170
|
-
128 vertices, and on geometry that has come apart — seams, small separate parts, the
|
|
171
|
-
patches a collapse leaves behind once it has eaten the connective tissue — there are two
|
|
172
|
-
or three vertices per triangle rather than a half. Halving the triangles then does not
|
|
173
|
-
halve the vertices, the re-cut needs as many clusters as the group consumed, and the step
|
|
174
|
-
is worth nothing to the round. So the re-cut is *measured* and the target tightened on
|
|
175
|
-
whichever of the two ceilings the attempt broke, until the step strictly shrinks its
|
|
176
|
-
group — tightened by the overshoot rather than by the cluster ratio, because what it
|
|
177
|
-
costs is fidelity and the overshoot is the least it can pay. Measured on the Zorah
|
|
178
|
-
sample: 187 of its 3,163 primitives reached a frontier no grouping could shrink, every
|
|
179
|
-
one of them this, and a 188th ran out of `VGeoLevelOptions.max_levels` inching forward a
|
|
180
|
-
merge a round — the same refusal to merge, spent on depth rather than on a stall.
|
|
181
|
-
- **Islands merge by proximity, once adjacency has nothing left to offer.** A cluster alone
|
|
182
|
-
in its connected component can never be coarsened — halving its triangles leaves one
|
|
183
|
-
cluster where there was one — so a mesh of separate shells has no root at all under
|
|
184
|
-
adjacency alone. Such clusters are bundled along a Morton curve. It is strictly the
|
|
185
|
-
fallback: a cluster the partitioner shredded off is folded back into the adjacent piece
|
|
186
|
-
it shares the most boundary with, never into the proximity pool, or a lone shell sixty
|
|
187
|
-
metres away ends up merged with a fragment of something else.
|
|
188
|
-
- **Leaves are re-packed, never merged losslessly.** Where a group is all leaves and fits
|
|
189
|
-
inside one cluster, the union *replaces* them rather than becoming their parent. A step
|
|
190
|
-
of zero error is a step the cut rule can never take — `project((s, 0)) ≡ 0`, so the
|
|
191
|
-
parent never satisfies `project(parent) > τ` and the leaves under it would be stored,
|
|
192
|
-
paged, and never selected. This is what makes a scene of small separate parts tractable:
|
|
193
|
-
a thousand twelve-triangle bolts need cutting up differently, not simplifying. Above the
|
|
194
|
-
leaves the same union is refused, because a coarser cluster cannot simply be deleted —
|
|
195
|
-
a subtree hangs off it.
|
|
196
|
-
- **The leaf cut does not pack across components; a group's re-split does.** Packing asks
|
|
197
|
-
"what is near what", and over the whole mesh the answer from a space-filling curve can be
|
|
198
|
-
sixty metres away. A group has already been chosen for proximity, so packing inside one
|
|
199
|
-
cannot reach further than the group does.
|
|
200
|
-
- **Build validation must check**: group-boundary polylines survive their group's
|
|
201
|
-
simplification byte-exact; locked boundaries do not persist across consecutive rounds
|
|
202
|
-
(a band's seam with the rest of the frontier is locked for as long as its neighbours
|
|
203
|
-
stay out of the band — self-limiting, since the band's own error rises to meet them, but
|
|
204
|
-
unproven); the frontier converges to one root; no inverted triangles; error monotonicity
|
|
205
|
-
(§4); page-ancestry acyclicity (§5); sampled-Hausdorff spot checks against `r_e` (§4).
|
|
206
|
-
- **Content addressing**: the asset's identity is `hash(output bytes)` — that is all
|
|
207
|
-
immutability and CDN caching require. Bit-reproducible builds (same input ⇒ same
|
|
208
|
-
output) are a *goal* scoped to a pinned toolchain (node major, deterministic seeds,
|
|
209
|
-
single-threaded or order-independent reductions), not a correctness requirement:
|
|
210
|
-
transcendental libm drift and parallel float reduction order make an unconditional
|
|
211
|
-
cross-platform pin unsafe.
|
|
212
|
-
|
|
213
|
-
Open: exact group-size policy and its quality signals; simplification execution model
|
|
214
|
-
(shared level topology with per-group restricted sets vs extracted group submeshes — the
|
|
215
|
-
polyline guarantee above is the constraint either must satisfy); vertex-weighted
|
|
216
|
-
partition balance (triangle-count balance instead of cluster-count balance).
|
|
217
|
-
|
|
218
|
-
## 4. Refinement metric
|
|
219
|
-
|
|
220
|
-
Distilled from the pixel-error musings
|
|
221
|
-
([share.gemini.google/AfYiNvAB2kt0](https://share.gemini.google/AfYiNvAB2kt0)).
|
|
222
|
-
|
|
223
|
-
Pinned:
|
|
224
|
-
|
|
225
|
-
- **Error is an upper bound on the object-space distance from the *source* surface** — not an
|
|
226
|
-
estimate of it, and not a density heuristic. The threshold is a threshold, so wherever the
|
|
227
|
-
number reads low the cut draws a cluster it should have refined and nothing downstream can
|
|
228
|
-
tell it did.
|
|
229
|
-
|
|
230
|
-
**The quadric cannot be that number, and it is not a tuning question.** Evaluating a carried
|
|
231
|
-
quadric at a vertex's position gives the *mean* squared distance to the planes it stands for,
|
|
232
|
-
and a mean is never a bound: a vertex that destroyed one feature and stands for a thousand
|
|
233
|
-
planes it sits exactly on reports the feature divided by all of them. Measured against the
|
|
234
|
-
distance the surface had actually moved, sampled at the source vertices, the quadric reading
|
|
235
|
-
came back at `0.34` of it on a closed pyramid, `0.58` on an octahedron, `0.62` on a spike
|
|
236
|
-
standing off a plane, and `0.03`–`0.62` across a cone swept in height. Reading it at the
|
|
237
|
-
collapses rather than after the pass, and per endpoint rather than summed, narrows *when* it
|
|
238
|
-
is diluted and not *that* it is.
|
|
239
|
-
|
|
240
|
-
So the quadrics stay, and they decide **where** a collapse puts the vertex, which is what they
|
|
241
|
-
are good at. What it **cost** is a separate scalar carried per vertex, composed by the triangle
|
|
242
|
-
inequality at every collapse:
|
|
243
|
-
|
|
244
|
-
```
|
|
245
|
-
bound[survivor] = max( bound[v1], bound[v2] ) + how far this collapse moved the surface
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
Zero at the source, carried across levels beside the quadrics, combined across steps by
|
|
249
|
-
`max` — two steps sharing a pinned boundary vertex each produce a valid bound on what that
|
|
250
|
-
step folded into it, so it carries the larger. No gains-minus-results, which is a quadric
|
|
251
|
-
problem.
|
|
252
|
-
|
|
253
|
-
- **The increment is how far the *surface* moved, not how far the vertices went.** A vertex
|
|
254
|
-
sliding within its own plane moves and the surface does not, and flat geometry is mostly
|
|
255
|
-
that — charging movement makes a flat wall refine forever, which is the one thing this metric
|
|
256
|
-
is not allowed to do (the flat-wall test below, that density heuristics were rejected on).
|
|
257
|
-
Measured, charging movement reported `15.9` where the surface had moved `0.98`, and `41.6`
|
|
258
|
-
on the same shape with a larger plane around it: the old dilution's mirror image, growing
|
|
259
|
-
with surface that never moved. So the increment is the distance from each endpoint's former
|
|
260
|
-
position to the survivor's one-ring — the geometry now standing where that surface was —
|
|
261
|
-
which the sliding case leaves sitting exactly on. With no one-ring left the patch was
|
|
262
|
-
consumed whole, there is nothing to measure against, and the movement is the only evidence
|
|
263
|
-
there is.
|
|
264
|
-
|
|
265
|
-
**Exactly zero, where it is zero.** A point nearer a triangle than the `float32` coordinates
|
|
266
|
-
can express is on it, and is reported as `0` rather than as the `1.6e-14` the closest-point
|
|
267
|
-
solve returns. Not pedantry: `project((s, 0)) === 0` is an explicit case and the
|
|
268
|
-
camera-inside-sphere `+∞` rule is conditioned on `r_e > 0`, so a flat cluster carrying
|
|
269
|
-
`1.6e-14` becomes undrawable at close range.
|
|
270
|
-
|
|
271
|
-
Measured against the surface's own movement: `1.00` on a closed pyramid and an octahedron,
|
|
272
|
-
`1.08` and `1.10` on a spike standing off a 13- and a 25-vertex plane — conservative
|
|
273
|
-
everywhere and within ten per cent of tight, and **not growing with the plane around it**.
|
|
274
|
-
Still sampled at vertices rather than over triangle interiors, so the sampled-Hausdorff gate
|
|
275
|
-
in §3 still earns its place.
|
|
276
|
-
|
|
277
|
-
**It costs nothing at a fixed budget.** The numbers rise — the arch primitive's root goes from
|
|
278
|
-
`3.786e-2` to `2.031e-1`, which is the old one having been that optimistic — so a given τ
|
|
279
|
-
buys a finer cut, and a caller who wants the old cluster count asks for a larger τ. At *equal
|
|
280
|
-
drawn cluster count* the picture is the same or better: `-9.3%`, `-6.7%`, `-6.4%`, `-3.1%`
|
|
281
|
-
of mean luminance against the old metric over a 16x range of cut depth. The ordering the cut
|
|
282
|
-
descends is preserved; what changed is what the number on it means.
|
|
283
|
-
|
|
284
|
-
Superseded: "a calibrated measure rather than a proven Hausdorff bound", and the whole of the
|
|
285
|
-
carried-quadric reading as the reported error. The carrying was right and stays; reading the
|
|
286
|
-
error off it was the mistake.
|
|
287
|
-
- **Each pass reports what its own collapses cost**, and a caller carrying the bound across
|
|
288
|
-
passes carries it across them too (`vgeo_build_group_step` across its attempts, the frontier
|
|
289
|
-
across levels, the child-error floor below as a backstop). A vertex that arrives already
|
|
290
|
-
deviating needs no re-reading: it arrives carrying its own bound.
|
|
291
|
-
|
|
292
|
-
Superseded: sweeping the surviving vertices after the pass, "before faceless vertices are
|
|
293
|
-
cleaned away, so that a patch consumed whole still costs what it cost". The measurement point
|
|
294
|
-
was right about the faceless vertex — it is read at the collapse that consumed it now — and
|
|
295
|
-
the sweep threw away every sharper reading to get there.
|
|
296
|
-
|
|
297
|
-
**Open.** Whether τ converts *accurately* to conservative pixels is still not measured, only
|
|
298
|
-
the object-space half of it is. `container_diff` measures how different the picture looks,
|
|
299
|
-
which has no unit relationship to a pixel threshold; what would answer it is a readback of
|
|
300
|
-
world position at the cut and at LOD 0, projected, worst case in pixels against τ. Until that
|
|
301
|
-
exists, "conservative" here means the object-space bound above.
|
|
302
|
-
- **A plane weighs the surface behind it, as a multiple of the mesh's mean triangle.** The
|
|
303
|
-
mean the error is read as has to be an average over the *surface*; over a plane count it is
|
|
304
|
-
an average over how the surface was cut, and a finely tessellated patch outvotes a coarse
|
|
305
|
-
one many times its size. Cutting one face in two along its own plane is the clean statement
|
|
306
|
-
of the defect — it adds no surface, and the metric heard the same plane twice. Weights are
|
|
307
|
-
ratios rather than areas because everything they are stated against is a ratio (the boundary
|
|
308
|
-
constraint is three *faces*; the attribute quadric summed with these weighs one per
|
|
309
|
-
triangle), and a slot holding square metres would make all of it depend on whether the asset
|
|
310
|
-
was authored in millimetres or kilometres. The reported error is a mean, which divides the
|
|
311
|
-
weights out again, so normalizing costs nothing there; it buys back every calibration.
|
|
312
|
-
|
|
313
|
-
This changes the quadric **matrix**, so it changes the placement solve and not only the
|
|
314
|
-
number — a behavioural change to simplification, landed on its own so it can be attributed.
|
|
315
|
-
`VGEO_DEFAULT_ATTRIBUTE_WEIGHT_RATIOS` was calibrated against the diluted reading and is
|
|
316
|
-
**not** re-tuned here; it needs re-tuning against this one, separately.
|
|
317
|
-
|
|
318
|
-
- **Composition across levels does not happen**, and that is the point of carrying quadrics.
|
|
319
|
-
The measured value is already deviation from the source; a step takes `max(measured, max(e
|
|
320
|
-
of child groups))`, where the floor exists only to impose monotonicity — the accumulated
|
|
321
|
-
quadric is not forced to rise when the vertex carrying the previous maximum is the one
|
|
322
|
-
collapsed away, and a parent claiming to be closer to the source than its own child would
|
|
323
|
-
make the cut oscillate.
|
|
324
|
-
|
|
325
|
-
Superseded: composing additively (`e_step + max(e of children)`) is what this replaced. The
|
|
326
|
-
triangle inequality justifies it as an upper bound and nothing else does — it loosens every
|
|
327
|
-
round and scales with the round count rather than with the geometry. Measured on a unit
|
|
328
|
-
sphere, the same ninety-triangle root read `0.36` built from 3k source triangles and `13.3`
|
|
329
|
-
built from 24k; carrying quadrics it reads `0.0315` either way, which is the same
|
|
330
|
-
approximation reporting the same number.
|
|
331
|
-
- **Units guard**: if vertex placement ever moves to higher-dimensional quadrics
|
|
332
|
-
(position + UV + normal), errors are still evaluated by plugging the resulting
|
|
333
|
-
*position* into the pure 3-D geometric quadric ("dual bookkeeping": solve in high-D,
|
|
334
|
-
evaluate in 3-D), so `r_e` stays in meters. Never take a root of a mixed-unit residual.
|
|
335
|
-
- **Per-cluster record stores two (sphere, error) pairs**, from *group* steps so all
|
|
336
|
-
siblings compute identical values:
|
|
337
|
-
- `self`: the step that created this cluster. Leaves store (own cluster sphere, 0).
|
|
338
|
-
- `parent`: the step that consumed it (roots: +∞).
|
|
339
|
-
- **Monotonicity is enforced at build**: `parent.error ≥ self.error` and `parent.sphere ⊇
|
|
340
|
-
self.sphere`, transitively. Without it the cut oscillates and flat evaluation is invalid.
|
|
341
|
-
- **Cut rule with residency clamp** — the per-cluster form, which the traversal reaches the
|
|
342
|
-
same answer as by stopping its descent rather than by evaluating it everywhere: draw a
|
|
343
|
-
cluster iff
|
|
344
|
-
|
|
345
|
-
`own group selectable AND project(parent) > τ AND (project(self) ≤ τ OR child group
|
|
346
|
-
non-selectable)`
|
|
347
|
-
|
|
348
|
-
The first term is not decoration: a group that is resident but not yet selectable satisfies
|
|
349
|
-
the rest of the rule while the coarser stand-in still covers the same surface, and both are
|
|
350
|
-
drawn a few microns apart — which reads as corrupt geometry rather than as a missing page.
|
|
351
|
-
The clamp is what makes "no holes, ever" (§6) true: without it, when the τ-selected
|
|
352
|
-
level lies below the streaming frontier, *no* resident cluster on that path satisfies
|
|
353
|
-
the pure rule and the guaranteed stand-in is never selected. The child-group
|
|
354
|
-
*selectable* bit flips **group-atomically** (a group never spans pages — §5 — and
|
|
355
|
-
becomes selectable only through activation — §6 — so page install/evict flips whole
|
|
356
|
-
groups; sibling disagreement would crack the frontier).
|
|
357
|
-
- **Projection**: `err_px = r_e · screen_height_px / (2 · tan(fov_y / 2) · d)` with `d` =
|
|
358
|
-
distance from camera to the *closest point* of the pair's sphere (conservative).
|
|
359
|
-
`project((s, 0)) ≡ 0` unconditionally; the camera-inside-sphere ⇒ +∞ convention applies
|
|
360
|
-
only when `r_e > 0` (otherwise leaves become undrawable exactly at close range).
|
|
361
|
-
- **The cut is a traversal, and there is one per view.**
|
|
362
|
-
|
|
363
|
-
*Traversal.* Descent from the root, testing a node or cluster and stopping when it is fine
|
|
364
|
-
enough, rather than a flat pass evaluating every resident cluster. Flat evaluation is
|
|
365
|
-
data-parallel and simple, and it does not scale: its cost is the resident set, which at the
|
|
366
|
-
scale this design targets is millions of clusters every frame, most of them off-screen. A
|
|
367
|
-
traversal costs the cut. It also gets two things for free that the flat rule pays for
|
|
368
|
-
separately — a missing page simply stops the descent, which *is* the residency clamp, and
|
|
369
|
-
culling at an interior node discards a whole region rather than one cluster. WebGPU guarantees no
|
|
370
|
-
forward progress across workgroups, so the persistent-thread answer (nvpro, vk_lod_clusters
|
|
371
|
-
comparison §2.4) is unbounded waiting and is not available; a wave redistributes its own
|
|
372
|
-
production among its lanes instead, and the descent's depth runs inside one dispatch. The
|
|
373
|
-
hierarchy it descends exists at two granularities. Pages form a rooted, acyclic,
|
|
374
|
-
fully-reachable DAG (§11.3–11.4 of the format), measurably
|
|
375
|
-
many-parented on any asset large enough to have more than one page per band, and every
|
|
376
|
-
child-table entry and page header now carries a **traversal metric** so a page can be tested
|
|
377
|
-
*before* it is fetched. Below page granularity, each page carries a tree over its own groups
|
|
378
|
-
(format §6.1–6.2). Cost, measured: 0.3%–1.6% of asset size.
|
|
379
|
-
|
|
380
|
-
*What exists.* `runtime/cut/__fixtures__/vgeo_select_cut.js` is this rule on the CPU, against a
|
|
381
|
-
partially resident container: descend from the root's parentless groups, refine where
|
|
382
|
-
`project(parent) > τ`, take the cluster where it does not, and stop where the child group is
|
|
383
|
-
not selectable — recording the want that would unblock it. It is the reference the GPU pass
|
|
384
|
-
has to agree with, and it is where the clamp was found to need *selectable* rather than
|
|
385
|
-
*resident* (§6). What it does **not** do is use either accelerator this section just
|
|
386
|
-
described: it descends group by group, and reads neither the child-table traversal metric nor
|
|
387
|
-
the page node tree. Both are written by the builder and checked by the validator, and nothing
|
|
388
|
-
reads either — the largest piece of built-and-unused machinery in the system, and the cut pass
|
|
389
|
-
is what it was built for.
|
|
390
|
-
|
|
391
|
-
*Per view.* **Virtual geometry is view-dependent by construction.** A cut is selected at one
|
|
392
|
-
camera's distances and pixel budget; it is the wrong cut for a shadow view, whose light may
|
|
393
|
-
sit a hundred metres behind the camera, and wrong again for each cascade. What is shared
|
|
394
|
-
between views is the *setup* — the traversal kernel, the residency state, the resident page
|
|
395
|
-
working set — not the result. Where a view does not justify a cut at all, it should take a
|
|
396
|
-
proxy mesh instead (plan §3.2, §3.3).
|
|
397
|
-
|
|
398
|
-
Superseded: this section previously claimed the cut was "view-agnostic in *coverage*" and
|
|
399
|
-
that shadow views could reuse the camera's. They cannot, and the claim is withdrawn.
|
|
400
|
-
- **Threshold `τ` defaults to 1 px and is a runtime dial** — the single quality/perf
|
|
401
|
-
lever. Empirically (prior implementation), pops stay invisible well above 1 px:
|
|
402
|
-
headroom for weak GPUs and dynamic resolution integration. **Read that number with care**:
|
|
403
|
-
the prior implementation shipped `1 / face_count` as its group error, a density heuristic
|
|
404
|
-
this section rejects below, so the observation is about a threshold dial under a different
|
|
405
|
-
metric and is not calibration for `r_e`.
|
|
406
|
-
- **Rejected**: pixels-per-triangle density heuristics — they fail the flat-wall test
|
|
407
|
-
(flat *closed-interior* geometry is exact at any triangle size; `r_e = 0` encodes that;
|
|
408
|
-
the qualifier matters — at *open boundaries* zero-residual erosion is real, which is
|
|
409
|
-
why §2 requires boundary constraint quadrics). And geomorphing — double residency and
|
|
410
|
-
bandwidth for a transition a tight metric already makes invisible; Epic disabled
|
|
411
|
-
theirs too.
|
|
412
|
-
|
|
413
|
-
Open: separate UV/attribute error term (CAD rarely; textured game art maybe — §9); τ
|
|
414
|
-
semantics under dynamic resolution scaling (native vs render pixels).
|
|
415
|
-
|
|
416
|
-
## 5. Binary format
|
|
417
|
-
|
|
418
|
-
A new container, designed for streaming: `.vgeo`. Requirements are pinned here; the
|
|
419
|
-
concrete layout is specified in [VGEO_FORMAT.md](./VGEO_FORMAT.md).
|
|
420
|
-
|
|
421
|
-
Pinned:
|
|
422
|
-
|
|
423
|
-
- **Scale**: billions of source triangles per asset ⇒ 64-bit byte offsets everywhere.
|
|
424
|
-
Little-endian, versioned, magic-tagged header. All counts below are **lower bounds**
|
|
425
|
-
(cluster fill < 128 under the hard ceiling; per-level reduction < 50% happens): 1 G
|
|
426
|
-
tris ⇒ ≥ ~8 M leaves, ≥ ~16 M DAG clusters, tens of GB of payload at today's raw-f32
|
|
427
|
-
positions (~4 KB per 128/128 cluster). Budgets and page sizing are set from measured
|
|
428
|
-
fill in M2, never from these nominals.
|
|
429
|
-
- **Immutable and content-addressed** (`hash(output bytes)` in header and filename):
|
|
430
|
-
perfect HTTP caching, CDN-friendly, no invalidation protocol.
|
|
431
|
-
- **Fetch unit = size-bounded page** (64 KiB, `VGEO_DEFAULT_PAGE_SIZE` — **one** page size, against
|
|
432
|
-
Nanite's two, traded off between hiding fetch latency and both response time and VRAM per
|
|
433
|
-
resident page on a low-end device; this section's "order hundreds of KB" was a first-principles
|
|
434
|
-
guess and is superseded, see the constant's own note), fetched by
|
|
435
|
-
HTTP **single-range** requests and **independently decodable**: a group never spans
|
|
436
|
-
pages; a page carries whole groups with payloads in (near-)upload-ready encoded form.
|
|
437
|
-
- **Pages carry the DAG, fetchably**: each cluster record stores its two (sphere, error)
|
|
438
|
-
pairs (§4) and a child reference (leaves: sentinel), and the page carries a
|
|
439
|
-
deduplicated **child table** with each referenced child page's exact byte range.
|
|
440
|
-
Page-to-page traversal is self-sufficient: an installed page fetches everything one
|
|
441
|
-
level down without consulting any global index. Wants (§6) are page indices resolved
|
|
442
|
-
entirely from the want-emitting page.
|
|
443
|
-
- **The hierarchy itself is paged.** Per-cluster metadata alone reaches GBs at the stated
|
|
444
|
-
scale and cannot be unconditionally resident. Only the root page — the topmost
|
|
445
|
-
level(s), cluster records and payloads together, at minimum a complete drawable cut —
|
|
446
|
-
is always resident; deeper pages stream on demand.
|
|
447
|
-
- **Time-to-first-pixel is O(1) by construction**: the fixed-size header directly carries
|
|
448
|
-
`(offset, size)` of the root page — first pixel needs header + root page only. The page
|
|
449
|
-
directory is **auxiliary**: fully derivable from the pages themselves, range-fetched
|
|
450
|
-
only for tooling and special tasks, never a runtime prerequisite (at billions scale it
|
|
451
|
-
is MBs).
|
|
452
|
-
- **Page-ancestry is acyclic by construction**: child references point strictly forward
|
|
453
|
-
in the file (coarse-first order is topological); non-root pages hold clusters of a
|
|
454
|
-
single level band. Build-validated.
|
|
455
|
-
- **Compression must not break random access.** Transparent HTTP compression does not
|
|
456
|
-
compose with range requests, so compression is per-page *inside* the container. Three codecs
|
|
457
|
-
are defined and all three shipped — `raw`, a bare LZ4 block, and a bare DEFLATE stream through
|
|
458
|
-
the platform's own zlib — chosen **per frame** rather than per file, so a page that would not
|
|
459
|
-
shrink is stored verbatim and says so. Measured, LZ4 is worth 1.12x–1.18x on the Stanford
|
|
460
|
-
models and deflate about seven points more for four and a half times the decode; deflate is what
|
|
461
|
-
the builder writes, because a build happens once and a fetch happens forever. The reason neither
|
|
462
|
-
is worth more is the reason to stop here: the payload is float32 positions and packed attribute
|
|
463
|
-
words, and it does not compress. Quantization (§2) is the lever, not the codec.
|
|
464
|
-
- **Coarse-first layout**: pages ordered so a plain prefix read of the file yields a
|
|
465
|
-
complete coarse asset — progressive load without a feedback loop.
|
|
466
|
-
|
|
467
|
-
Open: a page codec that knows what it is compressing — meshopt-style over the payload rather
|
|
468
|
-
than an LZ matcher over its bytes — if quantization leaves one worth having;
|
|
469
|
-
per-attribute quantization widths (cluster-local grids); relationship to
|
|
470
|
-
`MeshletGeometrySerializationAdapter` (extend vs parallel).
|
|
471
|
-
|
|
472
|
-
## 6. Streaming and residency
|
|
473
|
-
|
|
474
|
-
Pinned:
|
|
475
|
-
|
|
476
|
-
- **Three tiers with independent budgets**: CDN/network (whole asset) ⊇ RAM (page cache)
|
|
477
|
-
⊇ VRAM (resident cluster pool). Residency is a small fraction of the asset in RAM and a
|
|
478
|
-
smaller one in VRAM. Instances of the same asset share pages and residency.
|
|
479
|
-
- **The VRAM pool's hard ceiling is the adapter's `maxStorageBufferBindingSize`**, not
|
|
480
|
-
free configuration — pools bind as single storage buffers (spec default 128 MiB; Shade
|
|
481
|
-
requests adapter max, but low-end adapters stay low). Growth must clamp against device
|
|
482
|
-
limits with a defined failure policy; the low-limit fallback is a smaller pool + higher τ.
|
|
483
|
-
Multi-buffer sharding is explicit future work.
|
|
484
|
-
`GPUMeshletManager.grow` clamps now, having not before — but against **`maxBufferSize`**,
|
|
485
|
-
which is the wrong limit for an arena that binds as one storage buffer. Spec defaults are
|
|
486
|
-
256 MiB and 128 MiB respectively, so a buffer between the two is creatable and not bindable.
|
|
487
|
-
`GPUDatabase` clamps against the binding limit; `GPUMeshletManager` and
|
|
488
|
-
`GPUGeometrySDFManager` clamp against `maxBufferSize`. A VG pool has to pick deliberately,
|
|
489
|
-
and the reason this is worth stating rather than assuming is that `createBuffer` answers an
|
|
490
|
-
over-limit size with an *invalid buffer* rather than an exception — the failure is silent at
|
|
491
|
-
the point it happens and loud several frames later.
|
|
492
|
-
- **The VRAM paging mechanism is new work**, not `GPUMeshletManager` as-is (§2): v1
|
|
493
|
-
intent is **fixed-size page-slot allocation** (no compaction, no CPU metadata mirror,
|
|
494
|
-
no transient 2× rebuild), plus propagation of address migration to every consumer —
|
|
495
|
-
the existing `GPUGeometryManager` TODO becomes load-bearing under churn.
|
|
496
|
-
|
|
497
|
-
What that rests on is the container, and it now holds: FORMAT §11.14 makes
|
|
498
|
-
`nominal_page_size` a bound no page exceeds, announced by a feature bit a pool can require.
|
|
499
|
-
Before that it was an advisory number the builder itself broke — 13 of 40 pages over it on a
|
|
500
|
-
measured build — and a fixed stride sized against it would have been a slot overrun. A pool
|
|
501
|
-
taking the guarantee is entitled to one slot size and nothing else: it refuses a container
|
|
502
|
-
built at a different page size rather than carrying a size-class table for it.
|
|
503
|
-
- **Activation invariant (closure without dependency metadata)**: a group is
|
|
504
|
-
*selectable* iff **all** of its parent-cluster references are live — each referencing
|
|
505
|
-
cluster's page installed and that cluster's own group selectable (per-group reference
|
|
506
|
-
counter against the format's `parent_cluster_count`; root-level groups have count 0
|
|
507
|
-
and activate at install; activation cascades and flips whole groups). A selectable
|
|
508
|
-
cluster's entire ancestor cone is therefore installed and selectable — the closure
|
|
509
|
-
the cut rule needs, carried by a counter instead of dependency lists. The runtime
|
|
510
|
-
keeps a reverse-reference index for not-yet-installed pages (the same structure that
|
|
511
|
-
carries wants); a want targeting an already-installed page resolves as an activation
|
|
512
|
-
recheck, never a fetch. With the §4 clamped rule, any miss (or resident-but-inactive
|
|
513
|
-
group) has a coarser stand-in that *is selected* — streaming latency degrades
|
|
514
|
-
quality, never correctness. No holes, no double-draw, in any arrival order.
|
|
515
|
-
**Implemented**, in `VGeoContainerReader`: a live-reference counter per resident group, a
|
|
516
|
-
reverse index for pages not yet installed, cascades on both install and evict, and
|
|
517
|
-
`is_selectable(page, group)`. The double-draw half of that guarantee is not theoretical —
|
|
518
|
-
clamping on residency instead drew two levels over the same surface on 355 of 576 camera
|
|
519
|
-
positions of a 19-page bunny, up to 10.7% of drawn triangles in a doubly-covered region, and
|
|
520
|
-
it reads as corrupt geometry rather than as a missing page, which is what makes it hard to
|
|
521
|
-
attribute. 21 of that asset's 362 non-root groups have parents in more than one page, which
|
|
522
|
-
is all it takes.
|
|
523
|
-
- **Feedback carries touches as well as wants.** Wants = pages whose refinement was
|
|
524
|
-
blocked; touches = pages actually selected this frame. Eviction is LRU on
|
|
525
|
-
**last-touched** with a protect window ≥ feedback latency — LRU on last-*wanted* would
|
|
526
|
-
freeze stamps at fetch time and evict exactly what is on screen (the classic
|
|
527
|
-
fallback-chain bug; the VT system already keys on touch for this reason).
|
|
528
|
-
**Implemented**, in `virtual/residency/`: `VGeoResidencyManager` holds the RAM tier's byte budget
|
|
529
|
-
across every registered reader, `touch(reader, page)` takes the stamp, `advance_frame()` moves the
|
|
530
|
-
protect window, and `evict_to_budget()` drops on the format §10 rule ordered by coldest touch.
|
|
531
|
-
It is a sibling of `format/` and not part of it, because a container cannot answer a question
|
|
532
|
-
about a scene. The reader stayed mechanics: it reports `resident_bytes` and enforces nothing, and
|
|
533
|
-
the manager is written entirely against its public surface.
|
|
534
|
-
The parenthetical above is load-bearing and should be followed rather than taken on trust:
|
|
535
|
-
`VTPhysicalCache` is the working implementation of this same rule, with a `#last_touch`
|
|
536
|
-
`Uint32Array` per slot, `touch`/`last_touch`, a lock flag for slots that may never be evicted,
|
|
537
|
-
and `find_eviction_candidate(current_frame, protect_window)` whose doc gives the window the
|
|
538
|
-
identical justification. The similarity is therefore convergent *by instruction* — this bullet
|
|
539
|
-
told the implementation to key on touch because VT does — and the only real difference is that
|
|
540
|
-
VT budgets a count of fixed-size slots, which is what lets it pick by linear scan at ≤ ~1k
|
|
541
|
-
slots. Budgeting **bytes** across **several** readers, and admitting a page against that budget
|
|
542
|
-
before the fetch out of the `decoded_size` its parent already carries, is the part with no
|
|
543
|
-
precedent in this tree; so are per-page retry-into-quarantine and per-fetch `AbortSignal`.
|
|
544
|
-
- **The fetch cap has to be shared, and per-reader caps do not compose.**
|
|
545
|
-
`VGeoReadOptions.max_concurrent_fetches` bounds one reader, which is all a reader can see: four
|
|
546
|
-
readers at a cap of four each were measured issuing sixteen concurrent range requests, eight at
|
|
547
|
-
the default six issuing forty-eight. `VGeoResidencyManager` holds one total across every reader
|
|
548
|
-
as one priority queue, so a want's priority — pixels of projected error, comparable across assets
|
|
549
|
-
by construction — decides globally rather than within whichever reader asked first.
|
|
550
|
-
A shared cap is not itself a new idea here: `AssetManager.load_concurrency` bounds concurrent
|
|
551
|
-
top-level loads for the same reason and orders its queue through a negated priority into a
|
|
552
|
-
`BinaryHeap`, and `VirtualTextureManager` bounds `max_concurrent_loads` at 32. Writing that
|
|
553
|
-
negation a second time is what moved the queue out of this directory: it is
|
|
554
|
-
`core/process/ConcurrencyGate.js` now — a free-standing "run at most N of these at a time,
|
|
555
|
-
highest priority first" — and the `VGeoFetchScheduler` that shipped first was folded into it.
|
|
556
|
-
The other callers kept their own queues, each recording why where its cap is declared. What none
|
|
557
|
-
of them can do is compare two containers' wants — VT sorts its queue coarse-mip first, a proxy
|
|
558
|
-
that only orders within one texture, and `AssetRequestScope.final_priority` is hierarchical
|
|
559
|
-
rather than a common scale. The cut hands this one, so the global order is the half that is
|
|
560
|
-
actually new.
|
|
561
|
-
- **Wants are occlusion-aware in v1** (HZB test in the want path, or previous-frame
|
|
562
|
-
feedback à la VT — which is occlusion-aware by construction). Frustum-only wants plus
|
|
563
|
-
error-priority would page in and pin entire occluded interiors, breaking both budgets.
|
|
564
|
-
- **The wants loop tolerates its transport**: multi-frame `mapAsync` latency, a small
|
|
565
|
-
in-flight readback cap, and frames where feedback is skipped entirely (saturation) —
|
|
566
|
-
the VT contract. Priority = projected error reduction; requests dedup across frames
|
|
567
|
-
and instances; scoring/hysteresis details open.
|
|
568
|
-
- **In-flight protection**: a fetch pins the page it was discovered through until the
|
|
569
|
-
child installs — known at queue time, purely local. Arrivals verify the
|
|
570
|
-
identity-seeded frame checksum; activation makes any arrival order safe beyond that.
|
|
571
|
-
- **Network**: batched = adjacent wanted pages coalesced into **contiguous single-range**
|
|
572
|
-
requests over H2/H3 multiplexing (multipart ranges fail on real CDNs — S3 returns 200
|
|
573
|
-
+ full body); responses are verified `206`, with immediate abort on `200`.
|
|
574
|
-
- **Disk tier is an explicit per-page store or nothing.** The implicit browser HTTP cache
|
|
575
|
-
cannot serve as one: the Cache API rejects 206 responses and browsers do not usefully
|
|
576
|
-
cache/serve partial ranges at this scale. v1 may ship CDN + RAM only; the page store
|
|
577
|
-
(OPFS vs IndexedDB) is the open disk-tier design.
|
|
578
|
-
- **Uploads are budgeted per frame** (bytes/frame cap) into the VG residency pool; the
|
|
579
|
-
root page is pinned; within each tier, a page is evictable only while none of its
|
|
580
|
-
child pages are resident in that tier (checked against its own child table —
|
|
581
|
-
leaves-first order emerges from the rule).
|
|
582
|
-
|
|
583
|
-
Open: page store API choice; prefetch heuristics (camera velocity); priority scoring and hysteresis
|
|
584
|
-
constants; the in-flight pin, which is now blocked on a `VGeoPageWant` that names the page it was
|
|
585
|
-
discovered *through* rather than on nobody owning the budget (format §10 — it is an efficiency item,
|
|
586
|
-
because activation is safe at every arrival order). RAM eviction is no longer open; the numbers in
|
|
587
|
-
`VGeoResidencyOptions` are defaults to be overridden by whoever knows the device, not measurements.
|
|
588
|
-
|
|
589
|
-
## 7. Runtime pipeline (per frame)
|
|
590
|
-
|
|
591
|
-
1. Instance-level culling — existing.
|
|
592
|
-
2. **VG cut pass (new, compute)**: a §4 traversal, one per view. It descends the page DAG
|
|
593
|
-
and, inside a page, that page's node tree, from a queue seeded with the root cluster of
|
|
594
|
-
each visible instance; a node or cluster fine enough for τ terminates the descent, and so
|
|
595
|
-
does one whose child group is not selectable — which emits a want. It emits meshlet draw
|
|
596
|
-
records (existing record type) and wants/touches. VG carries geometry only — materials are
|
|
597
|
-
orthogonal and engine-side: a VG instance binds one geometry to one material through its
|
|
598
|
-
mesh exactly like any other geometry (a multi-material source splits into one geometry per
|
|
599
|
-
material upstream of the builder), so the mesh-keyed material buckets downstream stay
|
|
600
|
-
unchanged.
|
|
601
|
-
|
|
602
|
-
What it reads is the **page arena** — one fixed-stride slot per resident page, holding that
|
|
603
|
-
page's header, node rows, group rows and cluster rows — and the per-geometry lookup that
|
|
604
|
-
resolves a child page index to a slot (RUNTIME_PLAN §3.1). There is no selection buffer and
|
|
605
|
-
no page table: a page's header is the first words of its own slot, and the two (sphere,
|
|
606
|
-
error) pairs are in its cluster rows.
|
|
607
|
-
|
|
608
|
-
Superseded: this step previously specified a **flat** evaluation of the rule over every
|
|
609
|
-
row of that buffer, at a cost of `O(Σ over visible instances of resident(geometry))`. §4
|
|
610
|
-
replaced it, and the reason is worth keeping: that cost is the resident set rather than the
|
|
611
|
-
cut, and it is the resident set that grows without bound at the scale this design targets.
|
|
612
|
-
The buffer survives the change; the sweep over it does not.
|
|
613
|
-
3. Existing pipeline: HZB meshlet filtering (two-pass, per view — this is where frustum
|
|
614
|
-
and occlusion happen), material bucket sort, indirect draws, G-buffer, deferred
|
|
615
|
-
shading. The *record formats* flow unchanged; the *injection point* is new work — the
|
|
616
|
-
current mesh→meshlet expansion (`graph_expand_meshes_to_meshlets`, one helper shared by
|
|
617
|
-
four rasterization paths) reads one contiguous static meshlet range per geometry, so VG
|
|
618
|
-
instances enter through their own expansion step. M0 was where that seam was to be fixed
|
|
619
|
-
and it was not (§10); it is the first thing M1 has to do.
|
|
620
|
-
|
|
621
|
-
The traversal's GPU shape is settled and built: one indirect dispatch per spill generation, the
|
|
622
|
-
descent's depth inside the kernel, work redistributed across a subgroup's lanes by prefix sum. A
|
|
623
|
-
step costs about 0.5 µs whatever the wave holds, so a light asset pays for the shape of its descent
|
|
624
|
-
rather than for its geometry, while a container with real work in the frontier pays mostly for the
|
|
625
|
-
work. How many dispatches a cut needs is a queue-drain quantity rather than depth divided by the
|
|
626
|
-
step count: a wave keeps `subgroup_size` children and spills the rest to the next dispatch.
|
|
627
|
-
CUT_PLAN_2026_09_14 proposes to stop re-walking from the root by keeping the cut between frames.
|
|
628
|
-
|
|
629
|
-
Open: shadow views (v1 takes the one cut per view of §4, sharing setup rather than results, and a
|
|
630
|
-
proxy where a view does not justify a cut); DRS τ semantics.
|
|
631
|
-
|
|
632
|
-
Settled since: **flat evaluation versus a hierarchy is no longer a measurement to take.** §4
|
|
633
|
-
pins the traversal, and the hierarchy it descends is already written into every built asset
|
|
634
|
-
(plan §2.3). The prior-implementation data that made flat evaluation look adequate — selection
|
|
635
|
-
cheap, triangle throughput and screen-size uniformity dominant — was taken at that
|
|
636
|
-
implementation's scale, and is not evidence about millions of resident clusters.
|
|
637
|
-
|
|
638
|
-
## 8. Builder pipeline (offline)
|
|
639
|
-
|
|
640
|
-
Pinned:
|
|
641
|
-
|
|
642
|
-
- Stages: load → short-edge pre-pass (`bt_mesh_kill_short_edges` on soups / edge collapse
|
|
643
|
-
on closed surfaces — required by the weld contract) → weld
|
|
644
|
-
(`bt_merge_vertices_by_distance`; float32 quantum floor constrains tolerance) →
|
|
645
|
-
`bt_mesh_validate` → level-0 clustering (+ CC split) → the §3 round loop → per-cluster
|
|
646
|
-
encode → page assignment (level-banded, DAG + spatial locality, acyclicity check) →
|
|
647
|
-
container emit. The encode is the miniball sphere, the AABB, and `SHADE_MESHLET_V1`
|
|
648
|
-
attributes at their shipped widths (format §8): there is no cone field because cone culling
|
|
649
|
-
is declined (§9), and positions are raw float32 because quantization is a `payload_encoding`
|
|
650
|
-
that does not exist yet.
|
|
651
|
-
- **The weld tolerance is scale-relative**, a fraction of each geometry's bounding-box
|
|
652
|
-
diagonal (`bt_mesh_relative_merge_distance`), resolved once off the source's bounds
|
|
653
|
-
before any pass has removed anything. A conversion is many geometries at many sizes and
|
|
654
|
-
no single distance is right for all of them: a fixed `1e-4` leaves an 18-unit dragon
|
|
655
|
-
intact and tears 1,061 faces out of the same dragon at 1/100th scale. The ratio's own
|
|
656
|
-
interval is narrow — floored by the float32 quantum (`~1e-7` of the diagonal), capped
|
|
657
|
-
well under the finest feature a quantized source can carry (`6.1e-5` at `gltfpack`'s
|
|
658
|
-
14-bit default) — so `1e-5` is bracketed rather than tuned, and is not a knob callers
|
|
659
|
-
are expected to move.
|
|
660
|
-
- **Portable core**: the builder core is environment-agnostic JS — typed arrays and
|
|
661
|
-
`BinaryTopology` only, no fs/network/process APIs inside it. I/O and parallelism enter
|
|
662
|
-
through injected seams, and two harnesses are first-class: **node** (CLI, file streams,
|
|
663
|
-
worker_threads) and **browser** (user-picked handles / OPFS, Web Workers — injected
|
|
664
|
-
worker spawn, per the existing WorkerSystem precedent). The bounded-memory invariant
|
|
665
|
-
below is what makes the browser harness viable: the RAM budget is a configuration
|
|
666
|
-
input, and a browser build simply runs with a tighter budget and smaller islands.
|
|
667
|
-
- **Bounded memory**: the builder never requires the full source mesh in RAM. The level
|
|
668
|
-
loop needs the current level plus one group neighborhood. Level 0 of a billions-scale
|
|
669
|
-
mesh does not fit one `BinaryTopology`, so the source is pre-split into spatial
|
|
670
|
-
islands. **Island seams are locked while islands are separate, so interiors halve but
|
|
671
|
-
seams do not** — islands therefore merge **hierarchically, pairwise**, dissolving the
|
|
672
|
-
shared seam at each merge (never "all at once at the top"), and the split heuristic
|
|
673
|
-
carries an explicit seam-size budget. Seams must be gone well before the root.
|
|
674
|
-
- Deterministic seeds; asset identity = output hash (§3).
|
|
675
|
-
|
|
676
|
-
**One source container, many assets.** A scene file is not a geometry: glTF holds meshes of
|
|
677
|
-
primitives, placed by nodes. The unit the builder takes is the *primitive* — the smallest thing the
|
|
678
|
-
container calls a geometry, and already one material's worth — so a container yields one `.vgeo` per
|
|
679
|
-
primitive, each in object space with the node transforms left to the instances that carry them. The
|
|
680
|
-
per-material split §7 asks for therefore costs nothing on this path.
|
|
681
|
-
|
|
682
|
-
Open: island split heuristic and seam-quality validation; parallelism (WorkerSystem) vs
|
|
683
|
-
the reproducibility goal; browser-harness plumbing (OPFS vs user-picked file handles;
|
|
684
|
-
per-allocation ArrayBuffer ceilings feeding island sizing); the per-material split's
|
|
685
|
-
boundary handling (adjacent per-material geometries coarsen independently, like any two
|
|
686
|
-
neighboring assets); instance deduplication beyond what content addressing gives for free
|
|
687
|
-
(identical geometry hashes to one file; *near*-identical fasteners still do not, and
|
|
688
|
-
canonicalizing them is a preprocessor's job).
|
|
689
|
-
|
|
690
|
-
## 9. Non-goals (v1)
|
|
691
|
-
|
|
692
|
-
- **Backface normal-cone culling — declined outright, not deferred.** The two-pass HZB already
|
|
693
|
-
rejects what a cone would, on evidence rather than on a bound; see plan §5 for the mechanism
|
|
694
|
-
and the reports behind it. `cone_reserved` was a zeroed reserved field until `format_version` 5
|
|
695
|
-
spent it on `subtree_aabb` (format §7). This is the one entry here that is not a "v1" qualifier.
|
|
696
|
-
- Skinned / morphing / WPO meshes through the VG path (static geometry only).
|
|
697
|
-
- **Non-opaque VG instances.** The cut runs beside the mesh→meshlet expansion of the *opaque*
|
|
698
|
-
rasterization paths, and the transparent and alpha-tested bucket path
|
|
699
|
-
(`prepare_meshlet_draw_commands_by_material`) has no seam: it is handed a scene and an optional
|
|
700
|
-
camera resource rather than a view context, where a cut needs the camera's uniform buffer to put
|
|
701
|
-
each instance into object space. A VG instance with a non-opaque material therefore expands to
|
|
702
|
-
zero meshlets and draws nothing at all. `GPUSceneRows.attach_mesh` says so out loud when one is
|
|
703
|
-
attached, because this bullet is the authority for it and did not exist while the code was citing
|
|
704
|
-
it.
|
|
705
|
-
- Compute/software rasterization — WebGPU lacks portable 64-bit atomics; the hardware
|
|
706
|
-
raster path through the existing pipeline is the only raster path. Deferred G-buffer
|
|
707
|
-
shading already bounds the overshading cost of small triangles.
|
|
708
|
-
- Ray tracing over the VG cut (`blas_from_meshlets` exists for static meshes; VG×RT is
|
|
709
|
-
future work).
|
|
710
|
-
- *Sharing* one cut between views. §4 pins a cut per view and a proxy mesh where a view does
|
|
711
|
-
not justify one (plan §3.2, §3.3); what v1 does not attempt is a single cut made to serve the
|
|
712
|
-
camera and a shadow cascade at once, which was this document's earlier position and is wrong.
|
|
713
|
-
- Attribute-quadric placement and UV-seam constraint planes (the boundary-plane-penalty
|
|
714
|
-
mechanism §2 requires for open boundaries is the same one UV seams will use; v1
|
|
715
|
-
targets position/normal-fidelity content).
|
|
716
|
-
- Multi-buffer VRAM sharding beyond the single-binding limit.
|
|
717
|
-
- Editor tooling beyond a playground prototype.
|
|
718
|
-
|
|
719
|
-
## 10. Milestones
|
|
720
|
-
|
|
721
|
-
These have not been taken in order. The format and the RAM-tier pager were built before the
|
|
722
|
-
GPU cut, because the container is what the builder had to produce anyway and a reader is what
|
|
723
|
-
proves it; the consequence is that M0's one *engine*-facing criterion — the expansion seam —
|
|
724
|
-
is the piece still missing, and M1 is where the work now starts.
|
|
725
|
-
|
|
726
|
-
- **M0 — DAG proof** — *the DAG half is done; the engine half is not*: builder (in-memory, no
|
|
727
|
-
format) → flat CPU-side cut → existing meshlet pipeline draws it in a playground; the VG
|
|
728
|
-
expansion seam is fixed here.
|
|
729
|
-
Acceptance: crack-free under τ sweep and flythrough (incl. group-boundary polyline and
|
|
730
|
-
flip validation); drawn-triangle count tracks resolution, not source size; §3 build
|
|
731
|
-
validations green.
|
|
732
|
-
**Done**: the builder; a CPU cut (`runtime/cut/__fixtures__/vgeo_select_cut.js`) that is a traversal
|
|
733
|
-
rather than the flat pass planned here, and that runs against a partially resident container,
|
|
734
|
-
which was M2's job; τ-sweep watertightness as a spec (`build/vgeo_cut.spec.js` counts edges
|
|
735
|
-
used by one triangle instead of two, at a spread of thresholds, and wants zero); the
|
|
736
|
-
group-boundary polyline guarantee, enforced by the builder rather than merely validated.
|
|
737
|
-
**Done since**: the expansion seam. `runtime/graph_vgeo_expand_instances_to_meshlets` runs beside
|
|
738
|
-
`graph_expand_meshes_to_meshlets` at each of the three opaque call sites and appends into the same
|
|
739
|
-
collection, so a VG instance enters through a VG-shaped door and everything past the expansion is
|
|
740
|
-
untouched. `shade/playground/vgeo_runtime/` is that, on screen.
|
|
741
|
-
**Still not done**: flip validation (§2, extension 3).
|
|
742
|
-
- **M1 — GPU cut** — **done**: selection buffer + compute cut pass; wants/touches readback wired.
|
|
743
|
-
`runtime/` holds all of it and [runtime/RUNTIME_PLAN.md](./runtime/RUNTIME_PLAN.md) is its design.
|
|
744
|
-
Acceptance was parity with M0 and selection cost measured, and both were taken:
|
|
745
|
-
|
|
746
|
-
*Parity* is `runtime/cut/vgeo_cut_agreement.spec.js` — the kernels run under the WGSL emulator
|
|
747
|
-
against a real container and their drawn cluster **multiset** is held to exactly what
|
|
748
|
-
`vgeo_select_cut` selects, over seven cameras and four thresholds, fully resident and with pages
|
|
749
|
-
withheld, one instance and two. Multiset rather than set because a duplicate is the failure that
|
|
750
|
-
reads as corrupt geometry rather than as a missing page.
|
|
751
|
-
|
|
752
|
-
*Measured*, on an nvidia/lovelace adapter, a 13-page torus opened holding only its root page and
|
|
753
|
-
streamed entirely by the cut's own wants: 50 drawn clusters at τ=0.5 down to 8 at τ=16, monotone,
|
|
754
|
-
visiting 9 pages down to 5, clamping to zero as pages arrive, dropping nothing. Half the frames
|
|
755
|
-
skip feedback entirely under a two-deep readback cap, which is the virtual-texture contract and
|
|
756
|
-
costs streaming latency rather than pixels.
|
|
757
|
-
|
|
758
|
-
The flat-vs-hierarchy decision data is *not* what M1 produced, and §7 already says why: §4 pinned
|
|
759
|
-
the traversal and the question stopped being a measurement to take. What the cut measures instead
|
|
760
|
-
is that the hierarchy's own accelerator is sound — the node metric's `max_parent_error` cull fires
|
|
761
|
-
across the whole agreement sweep and changes not one selected cluster.
|
|
762
|
-
- **M2 — format + streaming** — *the RAM tier landed early*: `.vgeo` container, pager,
|
|
763
|
-
residency tiers with the §6 invariants (page-slot pool, address-migration propagation,
|
|
764
|
-
occlusion-aware wants, touch-LRU, in-flight pinning), budgets, eviction. Position
|
|
765
|
-
quantization lands here (encode + struct + decode shaders). Acceptance: cold-load TTFP =
|
|
766
|
-
header + root page regardless of asset size; streaming never produces holes; page
|
|
767
|
-
size/codec chosen on measurement.
|
|
768
|
-
**Done ahead of M1**: the container and its validator; the RAM-tier pager with residency,
|
|
769
|
-
activation and eviction (`format/read/`); TTFP, which is header + level table + root page and
|
|
770
|
-
measured to hold on a 27M-triangle asset; the page codec, chosen on measurement (§5).
|
|
771
|
-
**Done with M1**: the VRAM page pool (`runtime/GPUVirtualGeometryPool`) with a byte budget and
|
|
772
|
-
touch-LRU under a protect window, and the want record now naming the page it was discovered
|
|
773
|
-
*through* — which is what in-flight pinning was blocked on, since the format stores parent counts
|
|
774
|
-
rather than parent references and nothing below the cut could name that page.
|
|
775
|
-
**Still M2's**: address-migration propagation, occlusion-aware wants, the in-flight pin itself,
|
|
776
|
-
and position quantization.
|
|
777
|
-
- **M3 — hardening**: prefetch, disk page store decision, budget defaults, low-limit
|
|
778
|
-
adapters (τ fallback), stress on CAD-class input; browser builder-harness proof
|
|
779
|
-
(build a mid-size asset in-tab).
|
|
780
|
-
|
|
781
|
-
## 11. References
|
|
782
|
-
|
|
783
|
-
- Karis, *Nanite — A Deep Dive*, SIGGRAPH 2021 (Advances in Real-Time Rendering).
|
|
784
|
-
- Michaelides & Vaisse, *Maximizing Graphics Performance with Flexible Virtualized
|
|
785
|
-
Geometry*, GDC 2024 (Virtuos "Hi-Res Vision").
|
|
786
|
-
- Refinement-metric musings: https://share.gemini.google/AfYiNvAB2kt0
|
|
787
|
-
- `../meshlet/README.md` — RE Engine meshlet compression notes, nv_cluster_builder.
|