@woosh/meep-engine 3.60.4 → 3.60.5

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.
Files changed (379) hide show
  1. package/CHANGELOG.md +2602 -0
  2. package/CONTRIBUTING.md +7 -0
  3. package/MIGRATION_2_TO_3.md +136 -0
  4. package/SECURITY.md +4 -0
  5. package/editor/EditorCameraNavigation.d.ts +2 -1
  6. package/editor/EditorCameraNavigation.d.ts.map +1 -1
  7. package/editor/EditorCameraNavigation.js +51 -57
  8. package/package.json +6 -2
  9. package/src/engine/EngineHarness.d.ts +3 -9
  10. package/src/engine/EngineHarness.d.ts.map +1 -1
  11. package/src/engine/EngineHarness.js +6 -8
  12. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts +9 -6
  13. package/src/engine/graphics/camera/makeOrbitalCameraController.d.ts.map +1 -1
  14. package/src/engine/graphics/camera/makeOrbitalCameraController.js +44 -71
  15. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts +64 -0
  16. package/src/engine/graphics/camera/orbital/MomentumAxis.d.ts.map +1 -0
  17. package/src/engine/graphics/camera/orbital/MomentumAxis.js +187 -0
  18. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts +86 -0
  19. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.d.ts.map +1 -0
  20. package/src/engine/graphics/camera/orbital/OrbitalCameraRig.js +319 -0
  21. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts +34 -10
  22. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.d.ts.map +1 -1
  23. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js +116 -30
  24. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts +1 -7
  25. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
  26. package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +1 -37
  27. package/src/engine/graphics/ecs/water/Water.d.ts +8 -0
  28. package/src/engine/graphics/ecs/water/Water.d.ts.map +1 -1
  29. package/src/engine/graphics/ecs/water/Water.js +8 -0
  30. package/src/engine/graphics3/WaterSystem.d.ts.map +1 -1
  31. package/src/engine/graphics3/WaterSystem.js +731 -734
  32. package/src/engine/graphics3/decal/DecalTileShader.d.ts +61 -0
  33. package/src/engine/graphics3/decal/DecalTileShader.d.ts.map +1 -0
  34. package/src/engine/graphics3/decal/DecalTileShader.js +138 -0
  35. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts +10 -4
  36. package/src/engine/graphics3/decal/GPUDecalRenderer.d.ts.map +1 -1
  37. package/src/engine/graphics3/decal/GPUDecalRenderer.js +149 -44
  38. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts +25 -0
  39. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.d.ts.map +1 -0
  40. package/src/engine/graphics3/decal/chunk_decal_tile_vertex.js +55 -0
  41. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts +22 -0
  42. package/src/engine/graphics3/decal/graph_gather_decal_tiles.d.ts.map +1 -0
  43. package/src/engine/graphics3/decal/graph_gather_decal_tiles.js +70 -0
  44. package/src/engine/graphics3/decal/shader_decal_composite.d.ts +7 -5
  45. package/src/engine/graphics3/decal/shader_decal_composite.d.ts.map +1 -1
  46. package/src/engine/graphics3/decal/shader_decal_composite.js +48 -44
  47. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts +12 -0
  48. package/src/engine/graphics3/decal/shader_decal_tile_copy.d.ts.map +1 -0
  49. package/src/engine/graphics3/decal/shader_decal_tile_copy.js +78 -0
  50. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts +11 -0
  51. package/src/engine/graphics3/decal/shader_gather_decal_tiles.d.ts.map +1 -0
  52. package/src/engine/graphics3/decal/shader_gather_decal_tiles.js +80 -0
  53. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts +13 -0
  54. package/src/engine/input/devices/mouse/MouseEventButtons.d.ts.map +1 -0
  55. package/src/engine/input/devices/mouse/MouseEventButtons.js +23 -0
  56. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts +4 -0
  57. package/src/engine/input/ecs/controllers/KeyboardCameraController.d.ts.map +1 -1
  58. package/src/engine/input/ecs/controllers/KeyboardCameraController.js +9 -45
  59. package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
  60. package/src/shade/playground/water_refraction/README.md +5 -6
  61. package/src/shade/playground/water_refraction/index.html +1 -1
  62. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  63. package/src/shade/renderer/Renderer.js +7 -5
  64. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts +40 -80
  65. package/src/shade/renderer/camera/orbital/OrbitalCameraController.d.ts.map +1 -1
  66. package/src/shade/renderer/camera/orbital/OrbitalCameraController.js +197 -147
  67. package/src/shade/renderer/fluid/GPUFluidCascade.d.ts +3 -3
  68. package/src/shade/renderer/fluid/GPUFluidCascadeStack.d.ts +1 -1
  69. package/src/shade/renderer/fluid/forcing/GPUFluidForcing.d.ts +6 -6
  70. package/src/shade/renderer/fluid/pressure/GPUFluidPressureSolve.d.ts +1 -1
  71. package/src/shade/renderer/fluid/query/GPUFluidQueries.d.ts +1 -1
  72. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.d.ts +2 -2
  73. package/src/shade/renderer/geometry/sdf/GPUGeometrySDFManager.js +2 -2
  74. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts +2 -1
  75. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.d.ts.map +1 -1
  76. package/src/shade/renderer/geometry/sdf/cards/GPUMeshCardSets.js +18 -1
  77. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts +31 -0
  78. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.d.ts.map +1 -0
  79. package/src/shade/renderer/geometry/virtual/proxy/vgeo_to_proxy_mesh.js +343 -0
  80. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts +17 -0
  81. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.d.ts.map +1 -0
  82. package/src/shade/renderer/geometry/virtual/runtime/vgeo_meshlet_metadata_write.js +29 -0
  83. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.d.ts.map +1 -1
  84. package/src/shade/renderer/geometry/virtual/runtime/vgeo_runtime_meshlet_batch.js +7 -13
  85. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.d.ts.map +1 -1
  86. package/src/shade/renderer/global_illumination/probe_clipmap/GPUProbeClipmapGI.js +1 -8
  87. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts +5 -5
  88. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.d.ts.map +1 -1
  89. package/src/shade/renderer/global_illumination/probe_clipmap/ProbeClipmapSceneHolds.js +10 -10
  90. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts +1 -4
  91. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.d.ts.map +1 -1
  92. package/src/shade/renderer/global_illumination/probe_clipmap/probe/GPUProbeClipmap.js +1 -10
  93. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts +5 -15
  94. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.d.ts.map +1 -1
  95. package/src/shade/renderer/global_illumination/probe_clipmap/probe/chunk_probe_clipmap_hit_surface.js +6 -34
  96. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.d.ts.map +1 -1
  97. package/src/shade/renderer/global_illumination/probe_clipmap/probe/shader_probe_clipmap_shade.js +1 -7
  98. package/src/shade/renderer/hiz/HZBFootprint.d.ts +14 -0
  99. package/src/shade/renderer/hiz/HZBFootprint.d.ts.map +1 -0
  100. package/src/shade/renderer/hiz/HZBFootprint.js +16 -0
  101. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts +10 -0
  102. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.d.ts.map +1 -0
  103. package/src/shade/renderer/hiz/HZB_FOOTPRINT_X.js +11 -0
  104. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts +10 -0
  105. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.d.ts.map +1 -0
  106. package/src/shade/renderer/hiz/HZB_FOOTPRINT_Y.js +11 -0
  107. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts +0 -7
  108. package/src/shade/renderer/hiz/HierarchicalZBuffer.d.ts.map +1 -1
  109. package/src/shade/renderer/hiz/HierarchicalZBuffer.js +405 -350
  110. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts +12 -0
  111. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.d.ts.map +1 -0
  112. package/src/shade/renderer/hiz/chunk_hzb_footprint_x.js +27 -0
  113. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts +12 -0
  114. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.d.ts.map +1 -0
  115. package/src/shade/renderer/hiz/chunk_hzb_footprint_y.js +27 -0
  116. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts +14 -0
  117. package/src/shade/renderer/hiz/hzb_footprint_texels.d.ts.map +1 -0
  118. package/src/shade/renderer/hiz/hzb_footprint_texels.js +34 -0
  119. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts +0 -5
  120. package/src/shade/renderer/hiz/shader_hzb_reduce_any.d.ts.map +1 -1
  121. package/src/shade/renderer/hiz/shader_hzb_reduce_any.js +37 -57
  122. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts +11 -0
  123. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0.d.ts.map +1 -0
  124. package/src/shade/renderer/hiz/{shader_hzb_reduce_mip0_clip.js → shader_hzb_reduce_mip0.js} +120 -111
  125. package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
  126. package/src/shade/renderer/material/GPUMaterialManager.d.ts +7 -0
  127. package/src/shade/renderer/material/GPUMaterialManager.d.ts.map +1 -1
  128. package/src/shade/renderer/material/GPUMaterialManager.js +11 -0
  129. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts +12 -12
  130. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.d.ts.map +1 -1
  131. package/src/shade/renderer/material/resident/GPUResidentMaterialContext.js +17 -118
  132. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.d.ts.map +1 -1
  133. package/src/shade/renderer/shadow/map/GPUSceneShadowmapContext.js +7 -2
  134. package/src/shade/renderer/water/GPUWaterRenderer.d.ts +0 -6
  135. package/src/shade/renderer/water/GPUWaterRenderer.d.ts.map +1 -1
  136. package/src/shade/renderer/water/GPUWaterRenderer.js +12 -17
  137. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts +35 -0
  138. package/src/shade/renderer/water/medium/graph_water_meshlets.d.ts.map +1 -0
  139. package/src/shade/renderer/water/medium/graph_water_meshlets.js +89 -0
  140. package/src/shade/renderer/water/query/GPUWaterQueries.d.ts +1 -1
  141. package/src/shade/renderer/water/shader_water.d.ts.map +1 -1
  142. package/src/shade/renderer/water/shader_water.js +22 -13
  143. package/src/shade/renderer/water/shader_water_avboit.d.ts.map +1 -1
  144. package/src/shade/renderer/water/shader_water_avboit.js +7 -7
  145. package/src/shade/renderer/water/surface/graph_water_tessellation.d.ts +1 -1
  146. package/src/shade/renderer/water/surface/graph_water_tessellation.js +1 -1
  147. package/src/shade/wgsl/emulator/WGSLJavaScriptCompiler.js +9 -1
  148. package/editor/REWORK_PLAN_2026_08.md +0 -692
  149. package/src/CODEC_LAYOUT_VERIFICATION.md +0 -419
  150. package/src/CORRECTNESS_HUNT_2026_07_24.md +0 -142
  151. package/src/DUPLICATION_AUDIT_2026_08_07/00_core_reuse_surface.md +0 -738
  152. package/src/DUPLICATION_AUDIT_2026_08_07/A_core_math_geom.md +0 -79
  153. package/src/DUPLICATION_AUDIT_2026_08_07/B_core_data.md +0 -93
  154. package/src/DUPLICATION_AUDIT_2026_08_07/C_gfx_render.md +0 -80
  155. package/src/DUPLICATION_AUDIT_2026_08_07/D_gfx_features.md +0 -164
  156. package/src/DUPLICATION_AUDIT_2026_08_07/E_physics.md +0 -69
  157. package/src/DUPLICATION_AUDIT_2026_08_07/F_sound.md +0 -110
  158. package/src/DUPLICATION_AUDIT_2026_08_07/G_net_save.md +0 -129
  159. package/src/DUPLICATION_AUDIT_2026_08_07/H_engine_misc.md +0 -72
  160. package/src/DUPLICATION_AUDIT_2026_08_07/I_generation_view.md +0 -74
  161. package/src/DUPLICATION_AUDIT_2026_08_07/K_crosscutting.md +0 -169
  162. package/src/DUPLICATION_AUDIT_2026_08_07.md +0 -254
  163. package/src/DUPLICATION_AUDIT_REMEDIATION.md +0 -250
  164. package/src/DUPLICATION_REVIEW_2026_10_24.md +0 -1072
  165. package/src/REVIEW_2026_08_06/adv-core.md +0 -511
  166. package/src/REVIEW_2026_08_06/adv-gfx.md +0 -480
  167. package/src/REVIEW_2026_08_06/adv-view.md +0 -446
  168. package/src/REVIEW_2026_08_06/api-core.md +0 -296
  169. package/src/REVIEW_2026_08_06/api-engine.md +0 -195
  170. package/src/REVIEW_2026_08_06/bvh-phys-sound-net.md +0 -507
  171. package/src/REVIEW_2026_08_06/core-binary.md +0 -348
  172. package/src/REVIEW_2026_08_06/core-collection.md +0 -376
  173. package/src/REVIEW_2026_08_06/core-math.md +0 -221
  174. package/src/REVIEW_2026_08_06/core-process.md +0 -266
  175. package/src/REVIEW_2026_08_06/core-science.md +0 -223
  176. package/src/REVIEW_2026_08_06/engine-ai-nav.md +0 -279
  177. package/src/REVIEW_2026_08_06/engine-animation.md +0 -167
  178. package/src/REVIEW_2026_08_06/engine-asset.md +0 -166
  179. package/src/REVIEW_2026_08_06/engine-ecs.md +0 -920
  180. package/src/REVIEW_2026_08_06/engine-input.md +0 -197
  181. package/src/REVIEW_2026_08_06/engine-misc.md +0 -248
  182. package/src/REVIEW_2026_08_06/engine-terrain.md +0 -52
  183. package/src/REVIEW_2026_08_06/generation.md +0 -176
  184. package/src/REVIEW_2026_08_06/geom-2d.md +0 -434
  185. package/src/REVIEW_2026_08_06/geom-3d.md +0 -117
  186. package/src/REVIEW_2026_08_06/gfx-gi.md +0 -143
  187. package/src/REVIEW_2026_08_06/gfx-render.md +0 -740
  188. package/src/REVIEW_2026_08_06/gfx-texture.md +0 -690
  189. package/src/REVIEW_2026_08_06/prior-criticals.md +0 -49
  190. package/src/REVIEW_2026_08_06/prior-highs.md +0 -138
  191. package/src/REVIEW_2026_08_06/test-gaps.md +0 -364
  192. package/src/REVIEW_2026_08_06/view.md +0 -667
  193. package/src/REVIEW_2026_08_06.md +0 -610
  194. package/src/core/binary/lz4/LZ4_HIGH_COMPRESSION_PLAN_2026_09_14.md +0 -542
  195. package/src/core/bvh2/bvh3/NOTES.md +0 -2
  196. package/src/core/bvh8/NOTES.md +0 -63
  197. package/src/core/cache/wtinylfu/benchmark/CacheWTinylfu.bench.md +0 -243
  198. package/src/core/color/COLOR_FIX_PLAN_2026_08_28.md +0 -417
  199. package/src/core/color/COLOR_REVIEW_2026_08_28.md +0 -779
  200. package/src/core/geom/2d/polygon/TRIANGULATION_DESIGN.md +0 -433
  201. package/src/core/geom/3d/atlas/ATLAS_PORT_PLAN.md +0 -486
  202. package/src/core/geom/3d/atlas/BFF_PLAN.md +0 -174
  203. package/src/core/geom/3d/atlas/REVIEW_02.md +0 -392
  204. package/src/core/geom/3d/gjk/GJK_REVIEW_NOTES.md +0 -146
  205. package/src/core/geom/3d/gjk/NOTES.md +0 -8
  206. package/src/core/geom/3d/topology/TOPO_MESH_RETIREMENT_PLAN.md +0 -135
  207. package/src/core/geom/GEOMETRY_REVIEW_002_API_ISSUES.md +0 -381
  208. package/src/core/geom/GEOMETRY_REVIEW_002_NAMING.md +0 -684
  209. package/src/core/geom/MANIFOLD_COMPARISON_2026_08_01.md +0 -554
  210. package/src/core/model/reactive/evaluation/PREDICATE_APPLICABILITY_PROPOSAL.md +0 -446
  211. package/src/engine/REVIEW_2026_07_03.md +0 -211
  212. package/src/engine/asset/loaders/image/IMAGE_CODEC_REVIEW_2026_08_28.md +0 -815
  213. package/src/engine/control/first-person/DESIGN.md +0 -658
  214. package/src/engine/control/first-person/DESIGN_COLLISION.md +0 -382
  215. package/src/engine/control/first-person/DESIGN_EXTENSIONS.md +0 -563
  216. package/src/engine/control/first-person/TODO.md +0 -160
  217. package/src/engine/ecs/ASYNC_SYSTEMS_PROPOSAL.md +0 -531
  218. package/src/engine/ecs/HIERARCHY_REVIEW.md +0 -717
  219. package/src/engine/ecs/async/WORKER_SYSTEM_PLAN.md +0 -262
  220. package/src/engine/ecs/ik/PLAN.md +0 -179
  221. package/src/engine/ecs/storage/binary/SERIALIZATION_COVERAGE.md +0 -272
  222. package/src/engine/ecs/storage/binary/object-v2/DESIGN.md +0 -199
  223. package/src/engine/ecs/transform/TRANSFORM64_MIGRATION_PLAN_2026_09_04.md +0 -521
  224. package/src/engine/graphics/CONTEXT_LOSS_RECOVERY_PLAN.md +0 -463
  225. package/src/engine/graphics/ecs/camera/pp/NOTES.md +0 -48
  226. package/src/engine/graphics/render/frame_graph/CORRECTNESS_FIXES_2026_09_04.md +0 -659
  227. package/src/engine/graphics/render/frame_graph/FRAME_GRAPH_RECOMMENDATIONS.md +0 -235
  228. package/src/engine/graphics/sh3/path_tracer/REVIEW_001_ACTION_PLAN.md +0 -490
  229. package/src/engine/graphics/texture/sampler/compare/FLIP_IMPLEMENTATION_PLAN.md +0 -466
  230. package/src/engine/input/GAMEPAD_API_PROPOSAL.md +0 -717
  231. package/src/engine/input/ecs/ism/ISM_DESIGN.md +0 -1523
  232. package/src/engine/input/ecs/ism/ISM_IMPLEMENTATION_PLAN.md +0 -627
  233. package/src/engine/navigation/mesh/OVERLAPPING_SURFACES_2026_08_25.md +0 -661
  234. package/src/engine/navigation/mesh/PATHFINDING_PLAN.md +0 -229
  235. package/src/engine/network/CONGESTION_CONTROL.md +0 -87
  236. package/src/engine/network/MESSAGE_TRANSPORT.md +0 -364
  237. package/src/engine/network/SMOOTHNESS_DESIGN.md +0 -400
  238. package/src/engine/physics/cloth/MEASUREMENTS.md +0 -5304
  239. package/src/engine/physics/cloth/PLAN.md +0 -3056
  240. package/src/engine/physics/fluid/GPU_FLUID_CASCADES_PROPOSAL_2026_10_16.md +0 -1382
  241. package/src/engine/physics/fluid/REVIEW_02_PLAN.md +0 -243
  242. package/src/engine/physics/fluid/solver/NOTES.md +0 -46
  243. package/src/engine/sound/SOUND_EMITTER_RETIREMENT_PLAN.md +0 -536
  244. package/src/engine/sound/simulation/IMPLEMENTATION_PLAN.md +0 -432
  245. package/src/engine/sound/simulation/PROBE_PLACEMENT.md +0 -250
  246. package/src/engine/sound/simulation/PROBE_TRANSFER_RESEARCH.md +0 -322
  247. package/src/engine/sound/simulation/REFLECTION_IR_PLAN.md +0 -261
  248. package/src/engine/sound/simulation/VISIBILITY_GRAPH_PLAN.md +0 -121
  249. package/src/engine/sound/sopra/IMPLEMENTATION_PLAN.md +0 -999
  250. package/src/format/image/avif/DECISIONS.md +0 -545
  251. package/src/format/image/jpeg/NOTES.md +0 -3
  252. package/src/format/scene/gltf/MESHOPT_COMPRESSION_PLAN.md +0 -814
  253. package/src/generation/COORDINATES.md +0 -54
  254. package/src/generation/REVIEW_01_ACTION_PLAN.md +0 -628
  255. package/src/generation/foliage/ANNUAL_SHOOTS_BRIEF_2026_10_25.md +0 -220
  256. package/src/generation/foliage/CROWN_FILLING_BRIEF_2026_10_26.md +0 -172
  257. package/src/generation/foliage/CROWN_FULLNESS_BRIEF_2026_10_26.md +0 -86
  258. package/src/generation/foliage/FOLIAGE_DESIGN.md +0 -547
  259. package/src/generation/foliage/FOLIAGE_PLAN.md +0 -198
  260. package/src/generation/foliage/LEAF_ARCHITECTURE_BRIEF_2026_10_26.md +0 -158
  261. package/src/generation/foliage/LOD_REWORK_BRIEF_2026_10_27.md +0 -521
  262. package/src/generation/foliage/REAL_TIME_TREES_BRIEF_2026_10_27.md +0 -86
  263. package/src/generation/foliage/SELF_ORGANIZING_FRAMEWORK_BRIEF_2026_10_26.md +0 -347
  264. package/src/generation/foliage/SHOOT_SYSTEMS_ALL_SPECIES_BRIEF_2026_10_26.md +0 -248
  265. package/src/generation/foliage/SPEEDTREE_COMPARISON_2026_10_24.md +0 -694
  266. package/src/generation/foliage/TWO_FRONT_ENDS_BRIEF_2026_10_28.md +0 -552
  267. package/src/shade/RENDERER_CONTRACT.md +0 -697
  268. package/src/shade/SHADER_F16_PROPOSAL_2026_10_24.md +0 -263
  269. package/src/shade/device/IMMEDIATE_DATA_PLAN_2026_08_28.md +0 -569
  270. package/src/shade/device/PASS_ENCODER_PROPOSAL_2026_08_31.md +0 -755
  271. package/src/shade/device/mock/EXTENSION_PROPOSAL_2026_08_22.md +0 -458
  272. package/src/shade/device/mock/IMPLEMENTATION_PLAN.md +0 -255
  273. package/src/shade/device/mock/REVIEW_2026_08_22.md +0 -527
  274. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +0 -894
  275. package/src/shade/device/timing/profile/SGPT_FORMAT.md +0 -345
  276. package/src/shade/renderer/NOTES.md +0 -78
  277. package/src/shade/renderer/PLAN.md +0 -8
  278. package/src/shade/renderer/animation/ADDITIVE_BLENDING_PLAN.md +0 -145
  279. package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +0 -357
  280. package/src/shade/renderer/atmosphere/NOTES.md +0 -6
  281. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts +0 -76
  282. package/src/shade/renderer/camera/orbital/MomentumAxis.d.ts.map +0 -1
  283. package/src/shade/renderer/camera/orbital/MomentumAxis.js +0 -164
  284. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts +0 -45
  285. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.d.ts.map +0 -1
  286. package/src/shade/renderer/camera/orbital/OrbitalCameraConfig.js +0 -66
  287. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts +0 -98
  288. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.d.ts.map +0 -1
  289. package/src/shade/renderer/camera/orbital/OrbitalCameraInput.js +0 -184
  290. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts +0 -175
  291. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.d.ts.map +0 -1
  292. package/src/shade/renderer/camera/orbital/OrbitalCameraRig.js +0 -411
  293. package/src/shade/renderer/camera/orbital/README.md +0 -81
  294. package/src/shade/renderer/camera/orbital/index.d.ts +0 -6
  295. package/src/shade/renderer/camera/orbital/index.d.ts.map +0 -1
  296. package/src/shade/renderer/camera/orbital/index.js +0 -9
  297. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +0 -964
  298. package/src/shade/renderer/fluid/GPU_FLUID_OCCUPANCY_AND_SOTA_REVIEW_2026_10_26.md +0 -361
  299. package/src/shade/renderer/geometry/bvh/BLAS_REFIT_BATCH_PLAN.md +0 -619
  300. package/src/shade/renderer/geometry/meshlet/COMPACTION_PLAN_2026_09_16.md +0 -674
  301. package/src/shade/renderer/geometry/sdf/NOTES.md +0 -6
  302. package/src/shade/renderer/geometry/virtual/ATTRIBUTE_PLAN_2026_09_18.md +0 -540
  303. package/src/shade/renderer/geometry/virtual/MICRON_COMPARISON_2026_08_20.md +0 -305
  304. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +0 -913
  305. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +0 -787
  306. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +0 -481
  307. package/src/shade/renderer/geometry/virtual/VK_LOD_CLUSTERS_COMPARISON_2026_08_20.md +0 -301
  308. package/src/shade/renderer/geometry/virtual/WEDGE_PLAN_2026_09_18.md +0 -448
  309. package/src/shade/renderer/geometry/virtual/runtime/CULL_PLAN_2026_09_16.md +0 -385
  310. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +0 -656
  311. package/src/shade/renderer/geometry/virtual/runtime/REVIEW_LEDGER.md +0 -308
  312. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +0 -657
  313. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN_2026_09_14.md +0 -551
  314. package/src/shade/renderer/geometry/virtual/runtime/SHADOW_PLAN_2026_09_28.md +0 -529
  315. package/src/shade/renderer/global_illumination/lpv/NOTES.md +0 -3
  316. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_CLIPMAP_PROPOSAL_2026_10_27.md +0 -4306
  317. package/src/shade/renderer/global_illumination/probe_clipmap/PROBE_RAYS_VARIABLE_AND_SORTED_SCOPE_2026_10_30.md +0 -410
  318. package/src/shade/renderer/global_illumination/sharc/NOTES.md +0 -8
  319. package/src/shade/renderer/gpu_primitive/bvh/GPU_BVH_BUILD_PLAN.md +0 -1369
  320. package/src/shade/renderer/gpu_primitive/bvh/PHASE1_HANDOVER.md +0 -202
  321. package/src/shade/renderer/gpu_primitive/indirect/NOTES.md +0 -94
  322. package/src/shade/renderer/hiz/NOTES.md +0 -3
  323. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts +0 -8
  324. package/src/shade/renderer/hiz/shader_hzb_reduce_mip0_clip.d.ts.map +0 -1
  325. package/src/shade/renderer/light/cluster/NOTES.md +0 -11
  326. package/src/shade/renderer/light/cluster/PROPOSAL_ALGORITHM_2026_10_16.md +0 -524
  327. package/src/shade/renderer/light/cluster/PROPOSAL_HIERARCHICAL_2026_10_16.md +0 -1101
  328. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts +0 -24
  329. package/src/shade/renderer/material/resident/shader_resident_texture_means.d.ts.map +0 -1
  330. package/src/shade/renderer/material/resident/shader_resident_texture_means.js +0 -120
  331. package/src/shade/renderer/particles/DESIGN.md +0 -1011
  332. package/src/shade/renderer/particles/EMITTER_GLOBAL_STATE_PLAN_2026_10_29.md +0 -280
  333. package/src/shade/renderer/particles/ORIENTATION_PLAN_2026_10_15.md +0 -453
  334. package/src/shade/renderer/path_tracer/PATH_TRACED_MEDIA_PLAN_2026_10_02.md +0 -520
  335. package/src/shade/renderer/postprocess/bloom/NOTES.md +0 -6
  336. package/src/shade/renderer/postprocess/fsr/NOTES.md +0 -4
  337. package/src/shade/renderer/postprocess/gtao/NOTES.md +0 -6
  338. package/src/shade/renderer/postprocess/nss/NOTES.md +0 -332
  339. package/src/shade/renderer/postprocess/perspective/NOTES.md +0 -3
  340. package/src/shade/renderer/postprocess/sscs/NOTES.md +0 -5
  341. package/src/shade/renderer/postprocess/ssr/NOTES.md +0 -14
  342. package/src/shade/renderer/postprocess/ssr/REPROJECTION_PROPOSAL.md +0 -286
  343. package/src/shade/renderer/postprocess/ssr/prefilter/NOTES.md +0 -39
  344. package/src/shade/renderer/postprocess/taa/NOTES.md +0 -11
  345. package/src/shade/renderer/postprocess/upscale-dither/NOTES.md +0 -78
  346. package/src/shade/renderer/postprocess/vrs/NOTES.md +0 -4
  347. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_LEDGER.md +0 -1218
  348. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +0 -600
  349. package/src/shade/renderer/restir/reservoir/NOTES.md +0 -6
  350. package/src/shade/renderer/shader/chunk/atmosphere/NOTES.md +0 -6
  351. package/src/shade/renderer/shader/chunk/brdf/FRESNEL_BRIEF_2026_10_16.md +0 -391
  352. package/src/shade/renderer/shader/chunk/color/tonemap/ACES_TONE_MAPPING_BRIEF_2026_10_17.md +0 -153
  353. package/src/shade/renderer/shader/chunk/color/tonemap/HDR_TONE_MAPPING_FORMS_BRIEF_2026_10_16.md +0 -322
  354. package/src/shade/renderer/shader/chunk/color/tonemap/SDR_TONE_MAPPING_BRIEF_2026_10_16.md +0 -247
  355. package/src/shade/renderer/shader/chunk/fsr/NOTES.md +0 -3
  356. package/src/shade/renderer/shader/chunk/fsr/spd/NOTES.md +0 -4
  357. package/src/shade/renderer/shader/chunk/geometry/ray/NOTES.md +0 -4
  358. package/src/shade/renderer/shader/chunk/material/MULTISCATTER.md +0 -159
  359. package/src/shade/renderer/shader/chunk/mboit/NOTES.md +0 -38
  360. package/src/shade/renderer/shader/chunk/spherical_harmonics/l1i/NOTES.md +0 -3
  361. package/src/shade/renderer/shader/chunk/spherical_harmonics/sampling/NOTES.md +0 -8
  362. package/src/shade/renderer/shader/compiler/SHADER_SWITCH_PROPOSAL_2026_10_16.md +0 -927
  363. package/src/shade/renderer/shadow/contact/FINDINGS.md +0 -413
  364. package/src/shade/renderer/shadow/map/csm/NOTES.md +0 -5
  365. package/src/shade/renderer/shadow/ray/NOTES.md +0 -31
  366. package/src/shade/renderer/shadow/sdf/NOTES.md +0 -9
  367. package/src/shade/renderer/shadow/sdf/brixelizer/NOTES.md +0 -5
  368. package/src/shade/renderer/shadow/sdf/cascade/NOTES.md +0 -9
  369. package/src/shade/renderer/shadow/vsm/NOTES.md +0 -12
  370. package/src/shade/renderer/shadow/vsm/PLAN.md +0 -442
  371. package/src/shade/renderer/shadow/vsm/REVIEW_LEDGER.md +0 -43
  372. package/src/shade/renderer/texture/COMPRESSED_TEXTURE_PLAN_2026_09_01.md +0 -2444
  373. package/src/shade/renderer/texture/mipmaps/NOTES.md +0 -3
  374. package/src/shade/renderer/volumetrics/NOTES.md +0 -1329
  375. package/src/shade/renderer/volumetrics/VOLUMETRIC_NOISE_PLAN_2026_10_15.md +0 -336
  376. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts +0 -23
  377. package/src/shade/renderer/water/medium/GPUWaterBodies.d.ts.map +0 -1
  378. package/src/shade/renderer/water/medium/GPUWaterBodies.js +0 -75
  379. package/src/shade/wgsl/NOTES.md +0 -4
@@ -1,913 +0,0 @@
1
- # `.vgeo` — Virtual Geometry Container Format
2
-
3
- Status: **writer, validator and reader implemented; no GPU runtime.** Companion to
4
- [VIRTUAL_GEOMETRY_DESIGN.md](./VIRTUAL_GEOMETRY_DESIGN.md); every requirement pinned in
5
- its §5/§6 is realized here as a concrete field or invariant. Pinned structure below;
6
- open details are listed at the end. One `.vgeo` file = one unique geometry's complete
7
- cluster DAG; instances share the file.
8
-
9
- The implementation lives beside this document: `format/` is the container — what a file is,
10
- how one is written, and `vgeo_validate_container.js`, which audits every §11 invariant in
11
- one; `format/read/` reads one a page at a time; and `build/` is the pipeline that produces
12
- it from a `Geometry`, glTF front end included. The node command line over the writer and the
13
- validator is at `meep-engine/tools/geometry/virtual/`. One glTF yields **one file per
14
- primitive**, in object space, with the scene's placements written to a sidecar manifest
15
- rather than baked in.
16
-
17
- **Where the reader stops**, because it is the first thing a runtime needs to know. `format/read/`
18
- implements the *mechanics* of §10 — cold start, one fetch per page from the byte range its parent
19
- wrote down, want dedup, activation, eviction, and the mandatory reject rules of §4 and §5. It
20
- implements none of the *policy*: no fetch scheduler, no budgets, no touch-LRU, no quarantine, and
21
- the pinning rules are left to whoever manages residency.
22
-
23
- It decodes the **node table** (§6.1) — the GPU cut pass is the consumer that was predicted here and
24
- it arrived; the node metric's `max_parent_error` is what lets a whole subtree be skipped without
25
- touching a cluster. It decodes the **child-table traversal metric** (§5, §6.2) as well, because a
26
- reader that decodes two thirds of a structure is worse than one that decodes all of it — but no
27
- consumer reads it, and `runtime/RUNTIME_PLAN.md` §4.8 says why: at the moment a want is emitted, the
28
- per-cluster test the cut has already passed is strictly tighter than any page-level test, so the
29
- page-level one can never reject anything. Its consumer is prefetch, which is a later campaign.
30
-
31
- `shade/playground/vgeo_runtime/` and `shade/playground/vgeo_scene/` drive the reader end to end
32
- against a device, and `runtime/cut/__fixtures__/vgeo_select_cut.js` is a CPU reference traversal of the
33
- cut rule this format was shaped around — the oracle the GPU cut is held to, and the only traversal
34
- of it that is not a kernel.
35
-
36
- ## 1. Design constraints (inherited, binding)
37
-
38
- - **Geometry only.** `.vgeo` carries geometry and its vertex attributes — nothing else.
39
- Materials are orthogonal, engine-side concerns; a multi-material source splits into
40
- one geometry per material before it reaches the DAG builder — for glTF that split is
41
- free, since a primitive already carries exactly one material, and the converter's
42
- front-end emits one file per primitive. The format has no material fields.
43
- - Billions of source triangles per asset: 64-bit file offsets, 32-bit page indices.
44
- - Immutable, content-addressed; perfect HTTP/CDN caching; fetched by single-range
45
- requests.
46
- - Fetch unit = size-bounded **page**, independently decodable; a group never spans pages.
47
- - No separate global hierarchy: cluster metadata travels **in** pages, so the hierarchy
48
- streams with the geometry it describes.
49
- - **Traversal-first**: a page carries everything needed to fetch the pages it refines
50
- into. Standard runtime operation is header → root page → page-to-page traversal; the
51
- directory is auxiliary (tooling, validation, arbitrary-page access), never a hot-path
52
- dependency.
53
- - Time-to-first-pixel = header + root page, O(1) in asset size.
54
- - Prefix-readable: a plain byte-prefix of the file is a complete coarse asset.
55
- - Per-page compression inside the container only; transparent HTTP compression is
56
- assumed absent (it does not compose with range requests).
57
- - The page DAG is acyclic by construction: child references point strictly forward in
58
- the file.
59
-
60
- ## 2. Conventions
61
-
62
- - Little-endian throughout. Types: `u8 u16 u32 u64 f32`; `[T; N]` = N-element array.
63
- - All file offsets are absolute byte offsets from file start. All in-page offsets are
64
- byte offsets from the start of the **decoded** page.
65
- - **Frames** (stored pages) start at 16-byte-aligned file offsets; `directory_offset`
66
- is 16-aligned; alignment padding is real bytes in the file. Inside a decoded page,
67
- all tables and payload sections are 4-byte aligned (Shade's pools are u32-word
68
- addressed).
69
- - Levels are numbered `0 = leaves`, increasing toward the root. "Coarser" = higher
70
- level. A page's **band** is a contiguous level range `min_level..max_level`.
71
- A level is a **group** ordinal, not a per-cluster depth: it says which band a group
72
- sits in, and a group's clusters may sit at several different depths in the DAG,
73
- because a cluster the builder had no use for is carried forward untouched rather than
74
- coarsened for the sake of symmetry. Cluster records carry no level, and nothing needs
75
- one. What the ordinal guarantees is the property file order depends on: **every group
76
- is in a strictly lower band than the group holding its parents**, so pages laid out by
77
- descending band make every child reference point forward.
78
- - Sentinels for cluster `child_ref`: `NO_CHILD = 0xFFFF` (leaves),
79
- `SAME_PAGE = 0xFFFE` (child group lives in this page — legal only in multi-level
80
- pages, the root page included); valid child-table indices are `≤ 0xFFFD`. Cluster
81
- `child_group`: `NO_GROUP = 0xFFFF`, exactly for leaves. All `f32` fields are NaN-free.
82
- Error values are finite `≥ 0`, except the root's parent error which is `+∞` (f32
83
- infinity).
84
- - Reserved fields: writers MUST write 0; readers MUST ignore their contents.
85
- - Hashing is xxHash3; a `hash_algo` id is reserved for future change. **Integrity
86
- model**: identity-seeded per-frame checksums are the *operative client-side*
87
- mechanism — and because all DAG edges (child tables, cluster records) live inside
88
- frames, the traversal path is covered by them end to end; the header and the
89
- directory carry their own checksums; `asset_id` is whole-file and
90
- *publisher/pipeline/cache-side* — a range-fetching client never reads the whole file
91
- and can never verify it. None of these are a security boundary — they defend against
92
- truncation, corruption, version skew, and cache mixups, not attackers.
93
-
94
- ## 3. Top-level layout
95
-
96
- ```
97
- [ Header (256 B) ]
98
- [ Level table ] ← level_count × 32 B, §4.1
99
- [ Root page frame ] ← always-resident set = header + table + this
100
- [ Page frames, coarse-first band order ] ← prefix of the file = coarse asset
101
- [ Page directory ] ← flat 32 B records, range-fetchable
102
- [ Directory block checksums ] ← u64 per 128 records (4 KB block)
103
- ```
104
-
105
- The frame region is **exactly** `[root_page_offset, directory_offset)` — that is the
106
- normative end-of-frames rule (see §5). Its start is a header field rather than a constant
107
- because the level table (§4.1) precedes it and its size depends on the build. The directory is **derived data**: every field in it is
108
- reconstructible by scanning frames and decoding page headers. It exists for special
109
- tasks — tooling, validation, RAM budgeting before fetch, resuming from a disk cache,
110
- jumping to an arbitrary page without walking the DAG — and standard runtime traversal
111
- never touches it. Random-access readers fetch directory slices on demand: record `i`
112
- lives at exactly `directory_offset + 32·i`.
113
-
114
- ## 4. Header (256 bytes, fixed)
115
-
116
- All u64/u128 fields are naturally 8-byte aligned.
117
-
118
- | offset | type | field | notes |
119
- |-------:|-----------|--------------------------|-------|
120
- | 0 | u32 | magic | `'VGEO'` = 0x4F454756 |
121
- | 4 | u16 | format_version | 6 today; breaking layout changes only |
122
- | 6 | u16 | min_reader_version | 6 today; reader MUST reject if its version is lower |
123
- | 8 | u16 | header_size | = 256 |
124
- | 10 | u8 | hash_algo | 0 = xxHash3; readers MUST reject unknown values |
125
- | 11 | u8 | reserved | |
126
- | 12 | u32 | feature_flags_required | readers MUST reject any unknown set bit |
127
- | 16 | u32 | feature_flags_ignorable | readers MUST ignore unknown set bits; bit 0 = every page is within `nominal_page_size` (§11.14) |
128
- | 20 | u32 | payload_encoding | 1 = `SHADE_MESHLET_V1` (§8); readers MUST reject unknown values |
129
- | 24 | [u8; 16] | asset_id | xxh3-128 of the whole file with every **derived checksum field** zeroed (see below); stored low64 LE at bytes 0–7, high64 LE at bytes 8–15 |
130
- | 40 | u32 | vertex_attribute_mask | advisory attribute presence (§8 bit table) |
131
- | 44 | u32 | total_pages | = directory record count |
132
- | 48 | u64 | total_clusters | ≥ 1 |
133
- | 56 | u64 | total_groups | ≥ 1 |
134
- | 64 | u64 | root_page_offset | = `align16(256 + 32 · level_count)` — the level table (§4.1) sits between |
135
- | 72 | u32 | root_page_fetch_size | same definition as child-table/directory `fetch_size`, for page 0 |
136
- | 76 | u32 | root_page_decoded_size | RAM admission for the always-resident set, known from the header alone |
137
- | 80 | u64 | directory_offset | also the exact end of the frame region |
138
- | 88 | u64 | dir_block_checksums_offset | `ceil(total_pages / 128)` × u64 |
139
- | 96 | u32 | nominal_page_size | bytes a decoded page may come to; a **hard bound** when bit 0 of `feature_flags_ignorable` is set (§11.14), advisory otherwise |
140
- | 100 | u16 | reserved | |
141
- | 102 | u16 | level_count | levels `0..level_count-1`; ≤ 65,535. A `u8` through version 5, and the byte above it was reserved and written as zero — so little-endian makes this field read identically through either width for any container of 255 levels or fewer |
142
- | 104 | u64 | source_vertex_count | vertices of the normalized source mesh (post-weld) the leaves represent |
143
- | 112 | u64 | source_face_count | triangles of the normalized source mesh |
144
- | 120 | [f32; 4] | object_sphere | object-space minimal bounding sphere (center xyz, radius) of the whole geometry |
145
- | 136 | [f32; 6] | object_bounds | object-space AABB (min xyz, max xyz); instances transform at runtime |
146
- | 160 | [u8; 88] | reserved | |
147
- | 248 | u64 | header_checksum | xxh3-64 of bytes 0–255 with this field zeroed (`asset_id` already final) |
148
-
149
- **`format_version` 4 moved and `min_reader_version` did not**, which is the pair working as
150
- intended rather than an oversight. 4 added the parent tables (§6.3, §6.4): they occupy bytes that
151
- were reserved and written as zero, and they sit between the cluster records and the payload, so
152
- every offset a version-3 reader computes lands exactly where it did and such a reader reads a
153
- version-4 file correctly by ignoring them. In the other direction a version-4 reader meets
154
- `parent_table_offset = 0` on an older file and takes it as "this container has no upward edge",
155
- which is true — so neither direction needs a version test, and nothing has to be rebuilt to stay
156
- readable.
157
-
158
- **5 moved both**, which is the pair working the other way. It put `subtree_aabb` in the cluster
159
- record (§7) over the bytes `bounds_sphere` and `cone_reserved` held, so every field from offset 40
160
- on shifted: a version-4 reader would take a version-5 `child_ref` out of the middle of a bounding
161
- box and descend into whatever group that named. There is no ignoring it and no partial reading of
162
- it, so `min_reader_version` moved with the version, which is what that field is for. A version-5
163
- reader still reads version-4 records — it branches on the version it is holding — so nothing has to
164
- be rebuilt to stay readable, only to be culled tightly.
165
-
166
- **6 widened the level ordinal to `u16`** in all three places it is written down: `level_count`
167
- here, the page band in the page header (§6.1), and the page band in the directory record (§9). 255
168
- was a ceiling a real asset reached — the 2026-09-14 Zorah conversion refused a three-million-triangle
169
- bush at exactly 255 levels with 39 clusters still unmerged, while the other 2,974 geometries of that
170
- scene, one of them 27 million triangles, converged by 149 — and a ceiling an asset can touch is a
171
- ceiling in the wrong place. Two of the three cost nothing: this header and the directory record both
172
- had reserved bytes lying beside the field. The page header had none, and rather than grow the page
173
- this spends `node_table_offset`, which only ever restated `56 + 8·group_count`. So **a page header
174
- is still 56 bytes and every other field of every page ever written is still at its old offset** —
175
- but bytes 4..8 and 20..24 of it now mean different things, and there is no partial reading of that,
176
- so `min_reader_version` moved. A version-6 reader keeps the older page layout as it keeps the older
177
- cluster layout, so again nothing has to be rebuilt to stay readable.
178
-
179
- ### 4.1 Level table (`level_count` × 32 B, immediately after the header)
180
-
181
- Statistics per level, at `256 + 32·i`, ending where the root page frame begins. Small —
182
- a hundred levels is 3.2 KB, and the deepest container yet converted is 149 — so a single
183
- fetch of the file's head brings the header, the whole table, and the always-resident root
184
- page together, and the table is available before any page has been decoded. That is a
185
- statement about what builds produce and not a bound: `level_count` is a `u16`, and a
186
- reader sizing a head fetch must take the number it finds rather than assume this one.
187
-
188
- | offset | type | field | notes |
189
- |-------:|------|-------------------|-------|
190
- | 0 | f32 | min_error | smallest object-space `r_e` any cluster of this level carries |
191
- | 4 | f32 | max_error | largest |
192
- | 8 | f32 | max_sphere_radius | largest LOD-metric sphere radius at this level |
193
- | 12 | u32 | cluster_count | ≥ 1 |
194
- | 16 | u32 | group_count | groups sitting in this band; ≥ 1 |
195
- | 20 | u32 | triangle_count | |
196
- | 24 | u64 | reserved | |
197
-
198
- What it is for: an instance's level range can be **bracketed** from the errors and radii
199
- alone — the coarsest level that still passes the threshold at the instance's near point,
200
- the finest that could be wanted at its far one — without reading a cluster record or
201
- fetching a page. That is what lets a scene of ten thousand identical parts resolve to a
202
- handful of distinct cuts instead of ten thousand of them, and what lets a prefetch name
203
- exactly the bands a bracket spans.
204
-
205
- These are facts about the build, not a policy. In particular **`max_error` is not monotonic
206
- in the level**: a round merges an error band, and a later round can produce a cluster finer
207
- than an earlier one did, by finally reaching geometry that had been waiting for a partner.
208
- A reader that wants a monotonic bound takes a running maximum from the coarsest end; that
209
- is the reader's choice and not the file's.
210
-
211
- `cluster_count` and `group_count` sum to the header's `total_clusters` and `total_groups`,
212
- which is the cross-check that keeps the table from drifting from the pages.
213
-
214
- **What `asset_id` covers.** The whole file, with these fields treated as zero: `asset_id` itself,
215
- `header_checksum`, every frame's `checksum` (§5), and the entire `dir_block_checksums` region (§9).
216
- Excluding the last two is what breaks the circle — the frame checksums are seeded from
217
- `asset_id_low64` and the directory block checksums from `asset_id_high64`, so covering them would
218
- make the identity a function of itself. Nothing is lost by the exclusion: every one of those fields
219
- is a function of content the hash still covers, so any change to the asset still moves `asset_id`.
220
-
221
- Build order, and the order a verifier repeats: zero those fields → hash → write `asset_id` →
222
- compute and write the frame and directory-block checksums it seeds → compute and write
223
- `header_checksum` over bytes 0–255 with only that field zeroed.
224
-
225
- The asset filename MUST be derived from `asset_id` in streamed deployments (content
226
- addressing is what makes same-URL version skew impossible; every client-side integrity
227
- property leans on it). The field is authoritative, the name is the enforcement.
228
-
229
- A new `payload_encoding` (e.g. quantized positions) is a **per-asset capability gate,
230
- not a format break**: old readers cleanly reject the asset instead of misreading it;
231
- `format_version` moves only when the container layout itself changes.
232
-
233
- ## 5. Page frame (stored page)
234
-
235
- Every page — root included — is stored as a self-delimiting frame:
236
-
237
- ```
238
- [ FrameHeader (24 B) ] [ Child table (plaintext) ] [ blob (codec-encoded) ]
239
- ```
240
-
241
- | offset | type | field | notes |
242
- |-------:|------|------------------|-------|
243
- | 0 | u32 | magic | `'VGPG'` = 0x47504756 |
244
- | 4 | u8 | codec | 0 = raw, 1 = one LZ4 block, 2 = one raw DEFLATE stream; applies to the blob only; readers MUST reject unknown values |
245
- | 5 | u8 | flags | 0 |
246
- | 6 | u16 | child_page_count | entries in the child table; 0 for all-leaf pages |
247
- | 8 | u32 | stored_size | bytes of blob (after the child table) |
248
- | 12 | u32 | decoded_size | decoded-page bytes (== stored_size when raw); MUST equal the referencing child-table entry's (and the directory record's), else reject |
249
- | 16 | u64 | checksum | xxh3-64, **seed = asset_id_low64 XOR page_index**, over frame bytes `[0, 16)` followed by `[24, 24 + 48·child_page_count + stored_size)` — everything but this field |
250
-
251
- **Child table** — `child_page_count` × 48 B, plaintext (never compressed), deduplicated:
252
- one entry per distinct child page this page's clusters refine into. This is what makes
253
- traversal self-sufficient — an installed page needs nothing but itself to fetch every
254
- page one level down — and it is plaintext for two load-bearing reasons: fetch ranges
255
- are readable before (or without) blob decode, and stored sizes stay independent of
256
- offset values, which keeps the build's layout computation non-circular (§10).
257
-
258
- | offset | type | field | notes |
259
- |-------:|------|---------------|-------|
260
- | 0 | u64 | frame_offset | absolute; strictly greater than this page's own frame offset (§11.3) |
261
- | 8 | u32 | page_index | the child page's global index (frame ordinal) — checksum seed, want dedup |
262
- | 12 | u32 | fetch_size | `24 + 48·child.child_page_count + child.stored_size`, rounded up to 16 — the exact range to fetch |
263
- | 16 | u32 | decoded_size | RAM budgeting before fetch; MUST equal the child frame's |
264
- | 20 | u32 | reserved | |
265
- | 24 | metric | metric | the child page's own metric (§6.2), copied — this is what makes a page testable *before* it is fetched; MUST equal that page's header metric (§11.11) |
266
-
267
- The identity-seeded checksum binds the frame to *this page of this asset* and covers
268
- the frame header fields, the child table, and the blob — a well-formed frame of the
269
- wrong page, wrong asset, or wrong version fails verification instead of installing
270
- silently. It also guards the traversal path itself: a corrupt child-table entry fetches
271
- a range whose frame cannot checksum as the expected page index.
272
-
273
- The next frame starts at `round_up(frame_offset + 24 + 48·child_page_count +
274
- stored_size, 16)`. **Prefix scanning**: page indices are frame ordinals (file order ==
275
- index order); scan sequentially and stop when the next aligned offset + 24 exceeds
276
- `min(bytes_available, directory_offset)` — never rely on magic mismatch as a
277
- terminator.
278
-
279
- **Codecs.** `codec` is a property of one frame, not of the file: a writer picks per page
280
- and an asset may carry a mixture, so a reader that handles the defined codecs handles
281
- any file without asking what it was written with. That is what makes "compress unless it
282
- would not shrink" expressible — the LZ4 worst case is about one byte in 255 plus a
283
- token, deflate's is a stored block that is never smaller than its input, and paying
284
- either to store bytes verbatim is a straight loss, so a page that does not compress is
285
- written raw and says so.
286
-
287
- A new codec id is a **per-asset capability gate, not a format break**, exactly as a new
288
- `payload_encoding` is: an older reader meets a codec it has no decoder for and refuses
289
- the asset instead of misreading it. `format_version` moves only when the container
290
- layout itself changes, and adding a codec does not change it — `2` went in without
291
- touching `format_version` or `min_reader_version`.
292
-
293
- `1` is a **bare LZ4 block**, not an LZ4 frame, and `2` is a **bare DEFLATE stream**
294
- (RFC 1951), not a zlib or gzip member. Both wrappers carry a magic, a checksum and a
295
- content size that this frame already carries better, and `decoded_size` is what the blob
296
- is decoded against. Both codecs are independent per blob — no match reaches outside the
297
- page — so a page decodes from its own bytes with no other page involved, which is the
298
- same property residency already needs.
299
-
300
- `decoded_size` is untrusted input, and readers allocate against it before anything can
301
- contradict it. It MUST therefore be checked against what `stored_size` bytes could
302
- physically produce under `codec` — exactly `stored_size` for raw, at most
303
- `255·stored_size + 19` for LZ4, at most `1032·stored_size` for deflate — *before* the
304
- allocation. A blob that then decodes to anything other than `decoded_size` MUST be
305
- rejected: a page truncated to a plausible prefix is the one corruption that reads as a
306
- valid page with geometry missing.
307
-
308
- Those ceilings are what the *format* permits, and neither compressing one is anything
309
- like what a page looks like, so a length check alone is a weak guard — deflate's 1032:1 admits a 256 KB blob claiming 264
310
- MB. A decoder MUST therefore bound its own output as it produces it rather than
311
- comparing lengths once it is done: the LZ4 decoder is given the end of the page as its
312
- write limit, and the deflate decoder stops at the first chunk that would pass it. A blob
313
- that expands further than its header admits then costs one chunk rather than the whole
314
- expansion.
315
-
316
- **Which codec, and why deflate is the default.** Measured over the 44 pages of a
317
- 240×120 torus: LZ4 stores them at **1.295x** and deflate at **1.369x**, and decoding
318
- every page costs **3.8 ms** under LZ4 against **16.9 ms** under deflate — 562 MB/s of
319
- decoded output against 126. Deflate buys about seven points of ratio for four and a half
320
- times the decode.
321
-
322
- **The builder and the converter write deflate**, because the two sides of that trade are
323
- not paid the same number of times: an asset is built once and fetched forever, and the
324
- bytes deflate saves are saved on every fetch of every page by every client. LZ4 is the
325
- deliberate setting for a container whose pages arrive while someone is looking at it —
326
- there the decode is a page's *arrival* cost on the residency path, and 126 MB/s is what
327
- that costs. Neither is a property of the file format: `codec` is a frame field and a
328
- reader that handles the defined codecs reads either without being told.
329
-
330
- A *default* codec is a statement about readers rather than about bytes. Under the gate
331
- above, a container written with deflate opens only in a reader that carries the deflate
332
- decoder, so the default is what decides which builds of this engine can open a freshly
333
- converted asset. `format_version` still does not move, and an older reader still refuses
334
- rather than misreads.
335
-
336
- Deflate is also the *hosted* codec: `CompressionStream`/`DecompressionStream` are the
337
- platform's own zlib, so it costs no bytes of library — and the platform offers it only
338
- as a stream, which is why everything in this reader from the blob decode upwards is
339
- asynchronous. Nothing in the format requires that; a synchronous inflate would read the
340
- same files.
341
-
342
- **What it is worth, measured.** LZ4 over the pages of the Stanford models comes to
343
- **1.12x on the bunny, 1.18x on the dragon and 1.14x on the 27M-triangle lucy** — pages
344
- are >99% of a file, so that is very nearly the file ratio. That is modest, and the reason
345
- is worth writing down because it says where the next win is rather than here:
346
-
347
- | section | share of page bytes | lz4 |
348
- |---|---:|---:|
349
- | page header, group, node and cluster tables | 7% | 1.6x |
350
- | payload | 93% | 1.09–1.15x |
351
-
352
- The tables compress, and there are not enough of them to matter. The payload does not,
353
- because under `SHADE_MESHLET_V1` it is float32 positions and packed attribute words —
354
- high-entropy at the byte level, with no repetition for an LZ matcher to find. For
355
- reference, on the same bytes `gzip -9` reaches 1.22x — codec `2`'s algorithm, at a level
356
- the platform API does not expose — and `xz -9` 1.63x, so this is the data resisting
357
- rather than the codec failing; an entropy coder buys a little more for a lot more time. **The lever is `payload_encoding`, not
358
- `codec`** — quantized positions (§12) shrink those bytes directly and leave the residue
359
- far more compressible.
360
-
361
- What the reader pays is one block decode per page installed, into the buffer the page was
362
- going to be copied into anyway: ~350–500 MB/s of decoded output from the JS decoder, so
363
- about 0.6 ms for a 256 KB page.
364
-
365
- ## 6. Decoded page
366
-
367
- ```
368
- [ PageHeader (56 B) ] [ Group table ] [ Node table ] [ Cluster records ]
369
- [ Parent-page table ] [ Group-parent table ] [ Payload ]
370
- ```
371
-
372
- The two parent tables sit between the cluster records and the payload, and that placement is the
373
- reason `min_reader_version` did not move when version 4 added them (§4): the node table's `= 56 +
374
- 8·group_count` stays true, every earlier offset is where it was, and both tables hold 4-byte entries
375
- over sections that are already multiples of four, so §11.8's word-aligned payload survives.
376
-
377
- PageHeader:
378
-
379
- | offset | type | field | notes |
380
- |-------:|------|-----------------------|-------|
381
- | 0 | u16 | group_count | ≥ 1; group table immediately follows the header |
382
- | 2 | u16 | cluster_count | ≥ 1 |
383
- | 4 | u16 | min_level | page band; non-root pages span one contiguous band. A `u8` at 4 and another at 5 through version 5 |
384
- | 6 | u16 | max_level | |
385
- | 8 | u32 | cluster_table_offset | |
386
- | 12 | u32 | payload_offset | |
387
- | 16 | u32 | payload_size | |
388
- | 20 | u16 | node_count | ≥ 1; ≤ 2·`group_count` (§6.1). At 6 through version 5 |
389
- | 22 | u16 | reserved | what version 5 spent on `node_table_offset`, which said only `56 + 8·group_count` — the group table follows the header and the node table follows the group table, so a reader derives it. MUST be zero |
390
- | 24 | u32 | parent_table_offset | start of the parent section — the parent-page table, then the group-parent table. **0 means the page carries neither**, which is what every container written before `format_version` 4 says about itself |
391
- | 28 | u16 | parent_entry_count | entries in the group-parent table (§6.3) = Σ `parent_cluster_count` over this page's groups |
392
- | 30 | u16 | parent_page_count | entries in the parent-page table (§6.4) |
393
- | 32 | metric | metric | this page's own traversal metric (§6.2) — the root node's, repeated here so an installed page can be tested from itself |
394
-
395
- Group table — `group_count` × 8 B. Groups are the atomic LOD/residency unit; because a
396
- group never spans pages, installing/evicting a page carries whole groups by
397
- construction — no global group index exists or is needed:
398
-
399
- | offset | type | field | notes |
400
- |-------:|------|----------------------|-------|
401
- | 0 | u16 | cluster_first | index into this page's cluster records |
402
- | 2 | u16 | cluster_count | ≥ 1; ≤ 4096 (format cap, §11.5) |
403
- | 4 | u16 | parent_cluster_count | number of parent clusters referencing this group, `SAME_PAGE` references included; 0 exactly for the root page's topmost-level groups — drives activation (§10). Also the length of this group's run in the group-parent table |
404
- | 6 | u16 | parent_first | index of that run's first entry (§6.3). Groups tile the table in order, exactly as `cluster_first` tiles the cluster records |
405
-
406
- ### 6.3 Group-parent table (`parent_entry_count` × 4 B)
407
-
408
- The format's **only upward edge**, and the mirror of the child table with everything a parent does
409
- not need taken out. Every entry here is the reverse of exactly one cluster's
410
- `child_ref`/`child_group` pair, and §11.16 holds the two to each other.
411
-
412
- | offset | type | field | notes |
413
- |-------:|------|----------------|-------|
414
- | 0 | u16 | parent_ref | index into the parent-page table (§6.4), or `SAME_PAGE` |
415
- | 2 | u16 | parent_cluster | cluster index within that page |
416
-
417
- **Four bytes an entry, against the child table's 48**, because the two directions are not
418
- symmetric. A child reference carries a fetch range and a metric copy because the child may be
419
- absent and the decision to fetch it is taken before it arrives. A parent of a group in the cut is
420
- resident already — `VGEO_GROUP_FLAG_SELECTABLE` is exactly "every parent reference is live" — so a
421
- parent reference has nothing to decide and only has to say where.
422
-
423
- Measured across 8,899 pages of 30 converted containers: 17.4 entries a page on average and 26 at
424
- the worst, so the table runs to about 70 bytes against pages of 56–256 KB, and compresses with the
425
- rest of them (§5: tables are 7% of page bytes at 1.6×).
426
-
427
- What it is *for* is a cut that is kept between frames. A cut that has descended to a group and then
428
- sees the camera pull back has to draw the clusters that consumed that group instead, and without
429
- this it could only find them by descending from the root again — which is the cost keeping a cut is
430
- meant to remove, paid on the common case.
431
-
432
- ### 6.4 Parent-page table (`parent_page_count` × 4 B)
433
-
434
- One `page_index u32` per **distinct other** page holding a parent of one of this page's groups,
435
- deduplicated and ascending. A parent inside this page spends `SAME_PAGE` in §6.3 instead and has no
436
- entry here, so a page never appears in its own table.
437
-
438
- It lives in the decoded page rather than beside the plaintext child table because nothing needs it
439
- before decode: no fetch range is computed from it, and in the blob it compresses with the other
440
- tables. Measured: 4.9 entries a page on average, 18 at the worst.
441
-
442
- ### 6.1 Node table (`node_count` × 32 B)
443
-
444
- A tree over **this page's groups**, and nothing else. It is the part of the hierarchy below page
445
- granularity, and it has no paging question of its own: it arrives with the page it describes,
446
- leaves with it, and can never name anything that is not there.
447
-
448
- | offset | type | field | notes |
449
- |-------:|--------|-------------|-------|
450
- | 0 | metric | metric | bounds everything under this node (§6.2) |
451
- | 24 | u16 | child_first | first child — a node index when interior, a group index when a leaf |
452
- | 26 | u16 | child_count | 1..4 |
453
- | 28 | u16 | flags | bit 0: leaf (children are groups) |
454
- | 30 | u16 | reserved | |
455
-
456
- Node 0 is the root, always. Children occupy a **contiguous run**, and the tree is laid out
457
- breadth-first, so an interior node's `child_first` is strictly greater than its own index — which
458
- bounds any walk without a reader having to track one. Leaves **partition** the group table: every
459
- group is reached through exactly one leaf, no group twice and none not at all. A page with a single
460
- group still has a tree: one leaf node covering it, so a reader always starts at node 0 and never
461
- special-cases.
462
-
463
- Cost, measured across sphere assets from 3k to 98k triangles at page sizes from 8 KB to 256 KB: the
464
- node table plus every metric copy runs **0.3%–1.6% of asset size**, and trees come out one to three
465
- deep.
466
-
467
- ### 6.2 Traversal metric (24 B)
468
-
469
- The same three fields wherever they appear — a child-table entry (§5), a page header (§6), a node
470
- (§6.1):
471
-
472
- | offset | type | field | notes |
473
- |-------:|----------|------------------|-------|
474
- | 0 | [f32; 4] | sphere | contains every bound anything covered carries |
475
- | 16 | f32 | min_self_error | finest error covered |
476
- | 20 | f32 | max_parent_error | coarsest parent error covered; `+∞` where the root is covered |
477
-
478
- One sphere serves both the spatial test and the projection. It is the union of the covered clusters'
479
- `parent_sphere`s, and monotonicity does the rest: `parent_sphere` contains `self_sphere`, which
480
- contains the cluster's own vertices, so nothing under the node escapes it.
481
-
482
- `max_parent_error` is the load-bearing number. A cluster is drawn only while `project(parent) > τ`,
483
- so if the *largest* parent error under a node still projects to no more than τ, nothing under it can
484
- be drawn and the entire subtree is skippable. `min_self_error` answers the other question — whether
485
- anything here is drawable at all, or whether this is only a waypoint on the way to something finer —
486
- which is what a prefetch ranks its wants by.
487
-
488
- A group is the natural unit for all of this and needs no new geometry: every cluster of a group is
489
- consumed by one step and therefore shares that step's `parent_sphere` and `parent_error`, which is
490
- the same fact that makes the group the atomic LOD decision.
491
-
492
- **The copy in a child-table entry is the point of the whole thing.** It is what a traversal reads
493
- *before* fetching a page, and without it the only way to learn a page was not wanted is to fetch and
494
- decode it. Being a copy, it can drift; §11.11 requires it to match the page's own.
495
-
496
- ## 7. Cluster record (112 bytes)
497
-
498
- Everything the runtime cut rule, culling, wants, and upload need — denormalized per
499
- cluster so selection reads one record. Three kinds of bounds, deliberately distinct: the
500
- **LOD-metric pair** (`self_sphere`/`parent_sphere` — shared per group step so all
501
- siblings project identical values, monotonically inflated, never a tight fit), the
502
- **cluster's own box** (`aabb` — its tight fit, the meshlet metadata's `bounds_box`, culled
503
- downstream by the HZB), and the **subtree box** (`subtree_aabb` — this cluster's geometry and
504
- everything reachable below it, and the only one of the three the cut's per-cluster frustum test
505
- can use, because culling a cluster takes its whole child group out of the descent):
506
-
507
- | offset | type | field | notes |
508
- |-------:|----------|-----------------|-------|
509
- | 0 | [f32; 4] | self_sphere | LOD-metric sphere (center xyz + radius) of the group step that created this cluster; leaves: equals `bounds_sphere` |
510
- | 16 | f32 | self_error | object-space `r_e`; leaves: 0 |
511
- | 20 | f32 | parent_error | error of the step that consumed it; roots: +∞ |
512
- | 24 | [f32; 4] | parent_sphere | LOD-metric sphere of the consuming step; roots: duplicate of self_sphere |
513
- | 40 | [f32; 6] | subtree_aabb | object-space AABB of this cluster's vertices **and of every cluster reachable below it** through child groups (§11.17); leaves: equals `aabb`. What the runtime's per-cluster frustum cull reads |
514
- | 64 | [f32; 6] | aabb | this cluster's own object-space AABB — GPU metadata `bounds_box`, the downstream culls, and the (future) dequantization grid |
515
- | 88 | u16 | child_ref | index into this frame's child table; `SAME_PAGE` when the child group is in this page; `NO_CHILD` for leaves |
516
- | 90 | u16 | child_group | group index within the child page (or this page, for `SAME_PAGE`); `NO_GROUP` for leaves |
517
- | 92 | u32 | encoding_flags | frozen bit table in §8 |
518
- | 96 | u32 | payload_offset | into this page's payload section; ≡ 0 (mod 4) |
519
- | 100 | u32 | payload_size | bytes; ≡ 0 (mod 4) |
520
- | 104 | u8 | vertex_count | 3 ≤ n ≤ 128 |
521
- | 105 | u8 | triangle_count | 1 ≤ n ≤ 128 |
522
- | 106 | [u8; 6] | reserved | **write 0** |
523
-
524
- **What version 5 spent to fit `subtree_aabb` in the same 112 bytes.** `bounds_sphere`, the cluster's
525
- own miniball, which no runtime ever read — the cut tests the LOD sphere, the HZB and the small-primitive
526
- cull test `bounds_box`, and the builder keeps its own copy of the sphere for the object bounds and the
527
- group centroids. And `cone_reserved`, four bytes held for a backface-cone cull that is **declined
528
- outright** (design §9, plan §5): the earlier note kept it because reclaiming four bytes was not worth
529
- moving `format_version`, and moving it for twenty-four was.
530
-
531
- Monotonicity (`parent_error ≥ self_error`, `parent_sphere ⊇ self_sphere`) is a build
532
- invariant, not a reader concern. A single-cluster asset is valid: one root page, one
533
- group with `parent_cluster_count = 0`, one cluster with `parent_error = +∞`,
534
- `child_ref = NO_CHILD`, `child_page_count = 0`.
535
-
536
- ## 8. Payload encoding
537
-
538
- `payload_encoding = 1` (`SHADE_MESHLET_V1`). **Byte-compat scope**: the payload block is
539
- byte-compatible with `GPUMeshletManager`'s **data pool** — uploading it is a copy. The
540
- 40-byte GPU metadata rows (`MESHLET_METADATA_STRUCT`: bounds_box, address,
541
- primitive_count, vertex_count, flags) are **synthesized at install** from the cluster
542
- record (aabb → bounds_box, counts widened to u32, `address = slot_base_word +
543
- payload_offset/4`, flags = encoding_flags) — a small pack step that is part of the
544
- design's "VRAM paging mechanism is new work", not an existing patch path.
545
-
546
- Payload block layout:
547
-
548
- 1. Index section: `triangle_count × 3` local u8 indices, zero-padded to a 4-byte word
549
- boundary.
550
- 2. Attribute sections, in the frozen order below, each `words × vertex_count` u32
551
- words — unless its `encoding_flags` bit marks it constant-compressed, in which case
552
- exactly `words` u32s are stored once.
553
-
554
- Frozen tables (byte-identical to Shade's `ENCODED_ORDERED_ATTRIBUTES`,
555
- `ENCODED_ATTRIBUTE_SIZES`, `MESHLET_METADATA_FLAGS` as of this writing — the spec, not
556
- the source, is now the authority for `.vgeo` files):
557
-
558
- | # | attribute | words/vertex | encoding | flags bit (constant-compressed) | mask bit |
559
- |---|-----------|-------------:|----------------------------|-------------------------------:|---------:|
560
- | 0 | position | 3 | raw f32×3 | — (never compressed) | 0 |
561
- | 1 | normal | 1 | octahedral u32 | 1 (0x01) | 1 |
562
- | 2 | tangent | 1 | packed u32 | 2 (0x02) | 2 |
563
- | 3 | color | 1 | RGBA8 u32 | 4 (0x04) | 3 |
564
- | 4 | uv0 | 2 | raw f32×2 | 8 (0x08) | 4 |
565
- | 5 | uv1 | 1 | 2×unorm16 u32 | 16 (0x10) | 5 |
566
-
567
- - `encoding_flags` bits 5 (0x20, skinning joints) and 6 (0x40, skinning weights) MUST
568
- be clear, and **no skinning sections are stored** — `SHADE_MESHLET_V1` payloads end
569
- at uv1. (Load-bearing note: skinning attributes are last in Shade's attribute order
570
- and the static decode path never addresses them, which is exactly what makes their
571
- omission safe. VG is static-only, design §9.)
572
- - All other `encoding_flags` bits are reserved, MUST be 0.
573
- - An attribute the source lacks still ships its section, constant-compressed to the
574
- engine default — **sections are never omitted**; `vertex_attribute_mask` is advisory
575
- (which attributes carry real data). True section omission would shift every following
576
- offset and is only expressible as a new `payload_encoding` id.
577
-
578
- Quantized positions land as a **new `payload_encoding` id** (plus a page codec if
579
- entropy coding pays). The cluster record already carries the dequantization grid
580
- (`aabb`).
581
-
582
- ## 9. Page directory (auxiliary)
583
-
584
- Flat array of `total_pages` 32 B records at `directory_offset`; record `i` describes
585
- page index `i` (page index order == file order == coarse-first). Auxiliary and fully
586
- derivable from the frames (§3) — standard runtime traversal never reads it.
587
-
588
- | offset | type | field | notes |
589
- |-------:|------|----------------|-------|
590
- | 0 | u64 | frame_offset | absolute; frame per §5 |
591
- | 8 | u32 | fetch_size | as in the child table: the exact byte range to fetch is `[frame_offset, frame_offset + fetch_size)` |
592
- | 12 | u32 | decoded_size | MUST equal the frame's |
593
- | 16 | u16 | min_level | a `u8` here and another at 17 through version 5, which is what moved the three fields below by two — into bytes this record has always held in reserve |
594
- | 18 | u16 | max_level | |
595
- | 20 | u16 | flags | bit 0: root page |
596
- | 22 | u16 | group_count | |
597
- | 24 | u16 | cluster_count | |
598
- | 26 | [u8; 6] | reserved | |
599
-
600
- **Directory integrity**: `dir_block_checksums` holds one xxh3-64 per 128 directory
601
- records (one 4 KB block), seed = `asset_id_high64 XOR block_index`. Readers that use
602
- the directory fetch it at block granularity and MUST verify.
603
-
604
- ## 10. Reader requirements
605
-
606
- Standard runtime operation is **pure traversal** — the directory is not involved. Every item
607
- below is marked with what `format/read/` does about it: *(shipped)*, *(open)*, or a split where
608
- the mechanics are there and the policy is the caller's. Where a requirement is *policy*, the
609
- implementation is `renderer/geometry/virtual/residency/` rather than `format/read/` — a sibling of
610
- the format, not part of it, because what may be resident and what may be in flight are questions
611
- about a scene and not about a container.
612
-
613
- - **Cold start** *(shipped)*: fetch `[0, 256)`, verify `header_checksum`, magic, versions (§4 reject
614
- rules), then fetch `[root_page_offset, root_page_offset + root_page_fetch_size)` — the
615
- level table (§4.1) sits between the two, so a head prefix covering both brings the whole
616
- always-resident set in one request. The root frame's `decoded_size` MUST equal
617
- `root_page_decoded_size` and its frame extent MUST match `root_page_fetch_size`, else
618
- reject. Or speculatively fetch one larger head prefix — coarse-first layout makes every
619
- prefix byte useful.
620
-
621
- **How much of that prefix to keep, and for how long, is not a free choice at scale.** The
622
- always-resident set is small: invariant §11.7 puts exactly one root cluster in the file and the
623
- assembler gives the root page the complete topmost level, so a measured root page is one cluster —
624
- 1,732 B and 1,472 B on two torus builds, header and level table included coming to about 2.4 KB.
625
- A prefix past that is speculation about the *next* band, and at the 64 KiB default page size it
626
- buys about one page. `VGeoReadOptions.head_prefix_bytes` therefore defaults to 64 KiB rather
627
- than the 256 KiB it did, and the reader **releases the prefix** the first time a read falls
628
- outside it (`VGeoContainerReader.release_head`, and `head_bytes` to see it). Measured, at the old
629
- default a 256 KiB prefix over a 22 MB container covered 105 of its 4,000 pages and served nothing
630
- after those installed — while sitting outside `resident_bytes` and outside anything `evict` could
631
- reach, which across a thousand open assets was a quarter of a gigabyte no budget could see.
632
- - **Refinement** *(shipped)*: a want is a `child_ref` in an installed page; its child-table entry
633
- carries the exact byte range — one round-trip per cold page, no metadata fetch ever.
634
- Wants dedup on `page_index` across clusters, groups, and instances. A want whose
635
- target page is **already installed** resolves as an activation recheck of the
636
- referenced group (below), never a fetch.
637
-
638
- A want that goes stale mid-flight is cancellable: `VGeoByteSource.read` takes a per-read
639
- `AbortSignal`, `vgeo_fetch_byte_source` passes it to `fetch` alongside the source's own, and
640
- `VGeoContainerReader.cancel` abandons one page's request without touching the others.
641
- **What a stale want does, decided:** it is aborted while it can still be aborted, and installed
642
- once it cannot — the bytes are already paid for, and refusing to keep them costs the fetch *and*
643
- the page. An arrival nobody wanted enters residency untouched, which under LRU on last-touched
644
- makes it the first eviction candidate and free if the camera comes back.
645
- - **Activation (closure without dependency lists)** *(shipped)* — the selectable invariant:
646
-
647
- *A group `g` is selectable iff every one of its `parent_cluster_count` references is
648
- live — the referencing cluster's page is installed and that cluster's own group is
649
- selectable. Groups with `parent_cluster_count = 0` (exactly the root page's topmost
650
- level) are selectable at install.*
651
-
652
- By induction, a selectable cluster's entire ancestor cone is installed and
653
- selectable — the closure the cut rule needs, restored by a per-group counter instead
654
- of dependency lists. Runtime bookkeeping: each resident group keeps a live-reference
655
- counter; when a group becomes selectable, its clusters' references (child-table and
656
- `SAME_PAGE` alike) increment the referenced groups' counters — cascading
657
- transitively; installing a page counts the existing live references into it (the
658
- runtime maintains a reverse-reference index for pending pages — the same structure
659
- that carries wants). Counters only grow while the pages involved stay resident (the
660
- eviction rule below guarantees referencing parents outlive children). Activation
661
- flips whole groups; the cut rule's clamp reads *selectable*, not merely resident
662
- (design §4/§6): resident-but-inactive groups draw nothing, their coarser selectable
663
- ancestors keep covering that surface — **no holes and no double-draw, in any arrival
664
- order**, because a group never draws before all pages holding its parents are
665
- installed and selectable.
666
- - **In-flight protection** *(open)*: a fetch discovered through parent page P pins P until the
667
- child installs — known at queue time, purely local. Arrivals verify the
668
- identity-seeded frame checksum; activation makes any arrival order safe beyond that.
669
- The reader dedups in-flight fetches by page index but pins nothing.
670
- Still open, and the blocker named here is gone. It was that a `VGeoPageWant` carried the page it
671
- named and not the page it was *discovered through*, so neither the reader nor the manager could
672
- identify P — the format stores parent counts rather than parent references, deliberately, so
673
- nothing below the cut can name one. The cut says it: a want record is
674
- `(source_page_row, child_ref, child_page_index, priority)` and the source row is P. Nothing pins
675
- anything yet, and that is the whole of what remains. It is an efficiency item and not a
676
- correctness one — evicting P while its child is on the wire wastes the fetch, because the child
677
- installs non-selectable and activation is safe at every arrival order.
678
- - **Eviction** *(mechanics shipped in the reader, policy shipped above it)*: within each residency tier, a page is
679
- evictable only while it has no resident child pages in that tier — checked against its own
680
- child table (`SAME_PAGE` references never pin). Leaves-first order emerges from the rule;
681
- the root page is pinned. `VGeoContainerReader.evict` does the dropping and the activation
682
- retraction that goes with it — deliberately including the case where a caller evicts against
683
- this rule, where the cascade is what stops an orphaned child page from staying drawable — but
684
- it chooses nothing: which page to drop, and when, is the budget owner's.
685
- `VGeoContainerReader.resident_child_count` is the lookup the rule is written in terms of, and it is
686
- the whole of what the reader contributes.
687
- - **The budget owner** *(shipped, above the reader)*: `VGeoResidencyManager`. Readers register with
688
- one; it holds the RAM tier's byte budget across every registered reader, admits a want against it
689
- *before* the fetch out of the `decoded_size` the child table already carries, and evicts on the
690
- rule above ordered by design §6's LRU on **last-touched** with a protect window no shorter than
691
- feedback latency. It reaches nothing private: it is written entirely against the reader's public
692
- surface, which is the test that the split is real rather than nominal.
693
-
694
- A shared **fetch scheduler** comes with it, and has to: `VGeoReadOptions.max_concurrent_fetches`
695
- bounds one reader and cannot do better, and four readers at that cap were measured issuing sixteen
696
- concurrent range requests, eight readers at the default 6 issuing 48. `VGeoResidencyManager`
697
- holds one total across every reader in a single `ConcurrencyGate` (`core/process/`), as one
698
- priority queue — so a want's priority, which is pixels of projected error and therefore
699
- comparable across assets by construction, is compared globally rather than within whichever
700
- reader happened to ask first.
701
-
702
- **Most of that shape already exists in this engine twice, and the bullet is more useful read as
703
- the third one than as new work.** `renderer/texture/virtual/VTPhysicalCache` keeps a
704
- `#last_touch` stamp per slot, a lock flag for slots that may never go, and
705
- `find_eviction_candidate(current_frame, protect_window)` — LRU on last-touch under a protect
706
- window justified by feedback latency, which is the rule above. Design §6 said so when it pinned
707
- that rule ("the VT system already keys on touch for this reason"), so the resemblance is
708
- convergent by instruction rather than accidental. `engine/asset/AssetManager` caps concurrent
709
- top-level loads on `load_concurrency` for the reason the gate holds a total, and inverts
710
- priority into a `BinaryHeap` the same way in `get_pending_asset_priority_score` — being the
711
- third place to want that, and the second to write that inversion, is why the gate is now
712
- `core/process/ConcurrencyGate` rather than a file in this directory, and `AssetManager` keeps
713
- its own queue anyway for the reasons its `load_concurrency` doc records.
714
- `VirtualTextureManager` caps `max_concurrent_loads` at 32 and drops queued requests untouched
715
- for `queue_stale_window` frames, which is a stale-want policy of the kind `cancel` answers here.
716
-
717
- **The narrow claim that survives that** is a *byte* budget across *several* readers, admitted
718
- against before the fetch: `VTPhysicalCache` budgets a count of fixed-size slots and
719
- `AssetManager` budgets nothing and evicts nothing. The retry-into-quarantine and the per-fetch
720
- `AbortSignal` above are likewise new here — `AssetManager` records a failure and lets a later
721
- `get` retry, and neither precedent can abort one request. Granularity is why the pages are not
722
- simply `AssetManager` entries: an `AssetDescription` is a path and a type, where a page is a
723
- byte range in a container the format gives no global id to (a cluster is a page plus an index),
724
- at the 4,000 and 20,000 pages per container measured above and a thousand assets open at once.
725
- - *(shipped)* Verify every ranged response is `206` with the requested `Content-Range`; a `200`
726
- means the server ignored the range — abort the transfer immediately (design §6).
727
- `vgeo_fetch_byte_source` checks the status *and* the returned range: a proxy can answer 206
728
- with a range other than the one asked for.
729
- - *(shipped)* Verify frame checksums (identity-seeded, §5) before decode, and `decoded_size`
730
- against the child-table entry used to fetch and against the codec's decode bound
731
- (§5) before allocating for it. On mismatch: bounded retries with
732
- backoff, then **quarantine** the page — the clamped cut keeps a coarse stand-in
733
- correct indefinitely — and surface telemetry. Never retry-loop: corruption at a
734
- content-addressed URL is deterministic.
735
- The decode bound and the exact-length decode are unconditional. The frame checksum is
736
- `VGeoReadOptions.verify_frames`, off by default because a tool reading a file it just built is not
737
- defending against a network — a runtime registering a reader with `VGeoResidencyManager` should
738
- turn it on, and it is what lets the retries see corruption rather than only outages.
739
- Retry and backoff are `fetch_retries` (2) and `fetch_retry_backoff_ms` (50, doubling); after them
740
- the page enters `VGeoContainerReader.quarantine()` with its reason and is never fetched again, and
741
- `install_wants` skips it rather than failing the batch — one bad page out of a thousand assets'
742
- worth costs exactly that page. *When* to lift a quarantine is policy and is not the reader's:
743
- a checksum that failed at a content-addressed URL will fail again, while a run of 503s from one
744
- edge node is a different thing that a manager watching other assets arrive can tell apart, so
745
- `clear_quarantine` exists and `VGeoResidencyManager.clear_quarantines` is who calls it.
746
- - *(open)* After install, stamp each cluster's selection row with its own page index and its
747
- child page index/group (touch and want emission, design §6/§7). There is no selection buffer
748
- yet; `VGeoPage` keeps a page's tables and the traversal reads them directly.
749
- - *(open)* Fetch batching: coalesce adjacent wanted pages into contiguous single ranges; ranges
750
- never cross a band boundary by construction (coarse-first ordering), so coalescing
751
- lives within bands — the within-band spatial curve (§12) is what carries fetch
752
- locality and must be chosen with that as a criterion. Fetching through small unwanted
753
- gaps is permitted under an implementation waste threshold.
754
- - *(shipped)* Take a page's *actual* size from child tables (traversal), frames (prefix scan), or
755
- the directory (special tasks) — never from `nominal_page_size`, which is an upper bound and not a
756
- size. The bound itself may be relied on, and only when bit 0 of `feature_flags_ignorable` is set:
757
- then no page exceeds it (§11.14) and a reader may size fixed-stride page slots against it. A
758
- reader that depends on that MUST check the bit and refuse a container without it — containers
759
- written before the bound existed carry a `nominal_page_size` that pages ran over.
760
- - *(shipped)* **Nothing a runtime holds may be able to read the whole file.** The whole-file audit
761
- (§11, every checksum, the directory against a frame scan) and the install-every-page sweep are in
762
- `format/read/tool/` as `vgeo_audit_container` and `vgeo_install_all_pages`, not on the reader —
763
- `VGeoContainerReader.validate` and `.install_all_pages` remain as deprecated shims for the
764
- playground and the specs, and they reach the tools through a dynamic import so that a bundle that
765
- only opens containers does not carry the validator at all. The audit takes the container as one
766
- `ArrayBuffer`, because every §11 invariant is a cross-check between two places in the file; that is
767
- a ceiling and not a slope, so it refuses above `VGEO_MAX_AUDIT_BYTES` (1 GiB) naming the size,
768
- rather than failing in the allocator. Measured: a 514 MiB container audited in 19.4 s, and
769
- installing every page of a 2,000-page container of 544 KB pages made 514 MiB resident in 210 ms.
770
- - *(shipped)* The reverse-reference index of the activation rule is bounded by the **residency
771
- frontier**, not by how many pages a session has visited, and it is worth knowing which because it
772
- has the shape of an index that leaks. An entry exists only while a resident, selectable cluster
773
- refers into an absent page, and install/activation and evict/deactivation are symmetric. Measured
774
- on a 20,000-page container with the 3,280 pages of its eight coarse bands pinned resident:
775
- `pending_reference_page_count` was 6,561 after pinning and still 6,561 after touring all 6,561
776
- pages of the band below them a window at a time and evicting each window. Dropping the pinned
777
- pages too leaves 3.
778
-
779
- Build consequence (accepted): child tables hold downstream offsets — the format is
780
- optimized for reading, and the builder pays for it. Preferred strategy: emit frames
781
- with placeholder child tables, then **backpatch** them once all offsets are known, and
782
- finalize frame checksums last (the checksum covers the child table). Computing the full
783
- layout up front (compress all blobs → prefix-sum offsets → one forward emit) is equally
784
- valid. Both are cheap and non-circular **because child tables are plaintext in the
785
- frame**: a frame's extent (`24 + 48·children + stored_size`, with `stored_size` the
786
- blob's compressed size) never depends on the offset values, and patching plaintext
787
- bytes needs no re-compression. Build times are non-interactive by expectation —
788
- clustering and simplification dominate regardless.
789
-
790
- ## 11. Build invariants (validated at build, assumed by readers)
791
-
792
- 1. A group's clusters are contiguous in one page (`cluster_first/cluster_count`); a
793
- group never spans pages. Non-root pages span a single contiguous level band.
794
- 2. Child tables are exact: deduplicated, one entry per distinct **other** page
795
- referenced by this page's clusters (`SAME_PAGE` references appear in no child
796
- table); every `child_ref`/`child_group` resolves to a valid entry and group;
797
- `frame_offset`/`fetch_size`/`decoded_size` agree with the referenced frame and the
798
- directory (all referencing pages agree by construction).
799
- 3. Acyclicity and reachability: every child-table `frame_offset` is strictly greater
800
- than the owning page's frame offset (file order is topological), and every page is
801
- reachable from the root page through child tables.
802
- 4. File order (== page index order) is coarse-first: the root page first, bands by
803
- descending level, spatial locality within a band. The root page contains the
804
- complete topmost level(s) — at minimum a complete drawable cut of the DAG. A
805
- cluster's child group is always in a page whose band tops out strictly below its own
806
- page's, unless the reference is `SAME_PAGE`.
807
- 5. Group size: `1 ≤ cluster_count ≤ 4096` (format cap; builders target far less —
808
- design §3 grouping is adaptive from ~4). `parent_cluster_count` equals the exact
809
- number of clusters (across all pages, `SAME_PAGE` included) whose
810
- `child_ref/child_group` name this group; it is 0 exactly for the root page's
811
- topmost-level groups.
812
- 6. `1 ≤ triangle_count ≤ 128`, `3 ≤ vertex_count ≤ 128` (Shade meshlet limits);
813
- `total_clusters ≥ 1`; `source_vertex_count ≥ 3`, `source_face_count ≥ 1`.
814
- 7. Error/sphere monotonicity per design §4; `self_error = 0` and `child_ref = NO_CHILD`
815
- exactly for leaves; `parent_error = +∞` exactly for **the** root — there is exactly
816
- one, and it is in page 0. Several parentless clusters would be several entry points,
817
- and the cut only ever enters at one; it is also what an unconverged build leaves
818
- behind, which is the likelier cause. All floats NaN-free.
819
- `self_sphere`, `aabb` and `subtree_aabb` contain the cluster's decoded vertices; the header's
820
- `object_sphere` and `object_bounds` contain the normalized source mesh. The `self_sphere` half is
821
- what makes the LOD sphere usable as a conservative bound at all — the runtime's first frustum test
822
- is on it, and a reader meeting a pre-5 container stands its cube in for `subtree_aabb`.
823
- 8. Payload offsets and sizes are 4-byte multiples; every payload decodes/uploads
824
- against `payload_encoding` with no external state beyond its own cluster record.
825
- A leaf's `self_error` of 0 is only as honest as the simplifier's metric — see design
826
- §2, boundary constraint quadrics — and no reader-side check can distinguish an
827
- exact coarse cluster from one whose error went unrecorded.
828
- 9. `asset_id` and `header_checksum` verify per §4 — including its zeroing rule, which excludes
829
- every derived checksum field; every frame verifies per §5; the directory verifies per §9 and
830
- byte-for-byte matches what a frame scan reconstructs. `header_checksum` covers bytes 0–255
831
- only, so the level table's integrity rests on `asset_id`, which covers the whole file.
832
- 10. Every page's node table is a tree over exactly that page's groups: node 0 is the root, an
833
- interior node's children are a contiguous run at a strictly greater index, `1 ≤ child_count ≤ 4`,
834
- and the leaves partition the group table — each group reached through exactly one leaf. Each
835
- node's metric bounds its whole subtree: its sphere contains every child's, its `min_self_error`
836
- is no greater than any child's, its `max_parent_error` no less. The page header's metric equals
837
- the root node's. The table begins at `56 + 8·group_count` — derived, because the page header
838
- carries no offset for it since version 6 — and the `u16` at 22 that its offset used to occupy is
839
- zero.
840
- 11. Every child-table entry's metric equals the header metric of the page it names. It is a copy
841
- read before that page is fetched, so nothing else can catch it having drifted — and a traversal
842
- that trusts a stale one skips a page it needed and leaves a hole.
843
- 12. Every frame names a defined `codec` (§5); a raw frame's `stored_size` equals its
844
- `decoded_size`; and no frame's `decoded_size` exceeds what its codec could produce from
845
- `stored_size` bytes. Every blob decodes to exactly `decoded_size` — neither short nor long. A
846
- short decode is a truncated page, and a page truncated to a plausible prefix is the one
847
- corruption that reads as valid geometry with pieces missing; a long one is a blob that would
848
- write past what the reader allocated from `decoded_size`, and is stopped while it is producing
849
- rather than measured afterwards.
850
- 13. `root_page_offset = align16(256 + 32 · level_count)`; the level table's `cluster_count` and
851
- `group_count` columns sum to `total_clusters` and `total_groups`; every level holds at least
852
- one cluster and one group; `min_error ≤ max_error`, both finite and ≥ 0. `level_count ≥ 1`,
853
- and no page's `max_level` reaches it.
854
- 14. **Page size is a bound.** When bit 0 of `feature_flags_ignorable` is set, every page's
855
- `decoded_size` is ≤ `nominal_page_size`, and `nominal_page_size` is non-zero. The bit is what a
856
- reader checks; unset, `nominal_page_size` is the advisory build parameter it used to be and
857
- nothing here applies.
858
-
859
- The bound is what makes a page slot a fixed stride in VRAM — a slot address is a multiply, with
860
- no allocator, no growth path and no address migration behind it (design §6). It costs the
861
- internal fragmentation of each band's last page, measured at about 9% of a page across a
862
- 254-page build, and it is kept by splitting a band into more pages. What it cannot be kept by is
863
- splitting a **group**: §11.1 makes a group atomic to its page. So a build whose clusters group
864
- into more bytes than a page holds has no layout, and the builder refuses it naming the page size
865
- that would work, rather than writing a page over the size the header states.
866
-
867
- 15. **Parent-table bounds.** `parent_entry_count ≤ 65,535`, so that a group record's u16
868
- `parent_first` can index the whole table, and `parent_page_count ≤ parent_entry_count`, since
869
- no entry can name more than one page. The first is a cap rather than a wider field because the
870
- distance is not close: the worst page of 8,899 measured carries 26 entries, which leaves the
871
- bound standing 2,521× above it, and widening `parent_first` would cost the group record its
872
- 8-byte size. A builder that ever approached the cap would split the page.
873
-
874
- 16. **The two edges agree.** Every entry of a page's group-parent table (§6.3) resolves to a
875
- cluster whose `child_group` is the group whose run the entry is in; each group's run holds
876
- exactly the clusters that name it, with no repeats; and the run is sorted by
877
- (page index, cluster index). The first is what makes a coarsen legal at all. The third is what
878
- lets a runtime elect one appender out of a group's several parents by taking the first entry of
879
- the run, rather than carrying a dedup bitmask sized by instances × groups — so the order is
880
- load-bearing and not presentation.
881
-
882
- 17. **`subtree_aabb` bounds the subtree.** It contains the cluster's own `aabb` and, for every
883
- cluster of the group the cluster refines into, that cluster's `subtree_aabb` — so by induction
884
- it contains every vertex anywhere below the cluster in the DAG. A leaf's equals its `aabb`.
885
-
886
- **Exact, on the stored values, with no tolerance.** A union of float32 extremes is a float32
887
- extreme, so unlike the spheres there is nothing here for rounding to eat and nothing to inflate
888
- against it. That is half of why the runtime cull is a box: the other half is that a subtree of a
889
- surface is a flat patch, and its AABB is a fraction of the volume of any sphere around it.
890
-
891
- **Why the subtree and not the cluster.** The runtime's per-cluster frustum test decides whether
892
- a cluster refines into its child group, so culling it removes that whole group — and everything
893
- under it — from the cut. A step consumes one group and produces several parents, each naming the
894
- *whole* consumed group, so what is reachable below a cluster carries its siblings' surface too
895
- and grows with depth. A bound that covered only the cluster would cull a parent with on-screen
896
- descendants, and a culled subtree leaves a hole with nothing standing in over it.
897
-
898
- ## 12. Open
899
-
900
- - ~~Page codec ids beyond raw and LZ4~~ — `2` is raw DEFLATE through the platform's own zlib (§5)
901
- and is what the builder writes by default: 1.369x against LZ4's 1.295x on the same pages, for four
902
- and a half times the decode. Still open is a codec that knows what it is compressing —
903
- meshopt-style over the payload rather than an LZ matcher over its bytes. The quantized-position `payload_encoding` is the other half of the same
904
- question, and would take most of what either general-purpose codec currently finds.
905
- - ~~Default `nominal_page_size`~~ — 64 KiB, `VGEO_DEFAULT_PAGE_SIZE`: one page size rather than
906
- Nanite's two, set between hiding fetch latency and paying for it in response time and in VRAM per
907
- resident page on a low-end device, with a floor at the largest group a build produces. Provisional
908
- — reopen it on a device measurement, and when quantization lands, since that moves what a cluster
909
- costs.
910
- - Spatial ordering curve within a level band (Morton over group centroids is the
911
- default candidate; chosen with fetch-coalescing locality as an explicit criterion,
912
- §10).
913
- - Multi-geometry containers (v1: one geometry per file; scenes reference many files).