@woosh/meep-engine 3.27.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/build/bundle-worker-terrain.js +1 -1
  2. package/package.json +1 -1
  3. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts +15 -0
  4. package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts.map +1 -0
  5. package/src/core/geom/3d/mat4/m4_linear_determinant.js +24 -0
  6. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts +4 -2
  7. package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts.map +1 -1
  8. package/src/core/geom/3d/mat4/m4_normal_matrix3.js +21 -11
  9. package/src/engine/Engine.d.ts.map +1 -1
  10. package/src/engine/Engine.js +38 -1
  11. package/src/engine/graphics3/GraphicsEngine.d.ts +5 -1
  12. package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
  13. package/src/engine/graphics3/GraphicsEngine.js +834 -830
  14. package/src/shade/RENDERER_CONTRACT.md +19 -10
  15. package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +1 -1
  16. package/src/shade/playground/vgeo_runtime/README.md +27 -13
  17. package/src/shade/playground/vgeo_runtime/main.js +18 -5
  18. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +18 -0
  19. package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -0
  20. package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.js +9 -8
  21. package/src/shade/playground/vgeo_scene/README.md +408 -0
  22. package/src/shade/playground/vgeo_scene/index.html +144 -0
  23. package/src/shade/playground/vgeo_scene/main.d.ts +2 -0
  24. package/src/shade/playground/vgeo_scene/main.d.ts.map +1 -0
  25. package/src/shade/playground/vgeo_scene/main.js +1716 -0
  26. package/src/shade/renderer/Renderer.d.ts +4 -1
  27. package/src/shade/renderer/Renderer.d.ts.map +1 -1
  28. package/src/shade/renderer/Renderer.js +5 -2
  29. package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +1 -2
  30. package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
  31. package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +3 -29
  32. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts +22 -0
  33. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts.map +1 -0
  34. package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.js +49 -0
  35. package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +4 -2
  36. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +10 -8
  37. package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +12 -11
  38. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +1 -1
  39. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
  40. package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +1 -1
  41. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.d.ts.map +1 -1
  42. package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +5 -9
  43. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts +32 -0
  44. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts.map +1 -0
  45. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +59 -0
  46. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -0
  47. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts +44 -0
  48. package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts.map +1 -0
  49. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +36 -0
  50. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -0
  51. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_levels.js → level/vgeo_build_levels.js} +61 -95
  52. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts +19 -0
  53. package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts.map +1 -0
  54. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.js → level/vgeo_build_root_group.js} +1 -1
  55. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +73 -0
  56. package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -0
  57. package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.js → mesh/VirtualGeometryMesh.js} +7 -7
  58. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +29 -0
  59. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -0
  60. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_normals.js → mesh/vgeo_compute_vertex_normals.js} +3 -3
  61. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +31 -0
  62. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -0
  63. package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_tangents.js → mesh/vgeo_compute_vertex_tangents.js} +5 -5
  64. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +46 -0
  65. package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -0
  66. package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.js → mesh/vgeo_normalize_source.js} +9 -9
  67. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts +13 -0
  68. package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts.map +1 -0
  69. package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.js → page/vgeo_assemble_pages.js} +9 -9
  70. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts +13 -0
  71. package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts.map +1 -0
  72. package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.js → page/vgeo_build_page_nodes.js} +4 -4
  73. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts +22 -0
  74. package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts.map +1 -0
  75. package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.js → page/vgeo_page_node_count.js} +2 -2
  76. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +52 -0
  77. package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -0
  78. package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.js → partition/VGeoFaceGraph.js} +1 -1
  79. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts +14 -0
  80. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -0
  81. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts +29 -0
  82. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts.map +1 -0
  83. package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.js → partition/vgeo_partition_graph.js} +2 -2
  84. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts +41 -0
  85. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts.map +1 -0
  86. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.js +92 -0
  87. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts +10 -0
  88. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts.map +1 -0
  89. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.js → partition/vgeo_split_face_set.js} +12 -55
  90. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts +15 -0
  91. package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -0
  92. package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set_packed.js → partition/vgeo_split_face_set_packed.js} +3 -3
  93. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts +12 -0
  94. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts.map +1 -0
  95. package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.js → sphere/vgeo_bounding_sphere.js} +3 -3
  96. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts +18 -0
  97. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts.map +1 -0
  98. package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.js → sphere/vgeo_containing_sphere.js} +3 -3
  99. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts +9 -0
  100. package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts.map +1 -0
  101. package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.js → sphere/vgeo_sphere_inflate_to_contain.js} +1 -1
  102. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +24 -0
  103. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -0
  104. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +20 -0
  105. package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -0
  106. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +14 -0
  107. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -0
  108. package/src/shade/renderer/geometry/virtual/build/{vgeo_extract_submesh.js → submesh/vgeo_extract_submesh.js} +5 -5
  109. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +12 -0
  110. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -0
  111. package/src/shade/renderer/geometry/virtual/build/{vgeo_gather_faces.js → submesh/vgeo_gather_faces.js} +2 -2
  112. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts +11 -0
  113. package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts.map +1 -0
  114. package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.js → submesh/vgeo_narrow_indices.js} +3 -3
  115. package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
  116. package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +9 -11
  117. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts +8 -2
  118. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts.map +1 -1
  119. package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +8 -2
  120. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts +15 -0
  121. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts.map +1 -0
  122. package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.js +15 -0
  123. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +28 -0
  124. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
  125. package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +7 -3
  126. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
  127. package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +9 -4
  128. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
  129. package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +18 -5
  130. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
  131. package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +9 -1
  132. package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +21 -2
  133. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +52 -2
  134. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
  135. package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +97 -1
  136. package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +4 -4
  137. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
  138. package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +10 -0
  139. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
  140. package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +59 -34
  141. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +8 -8
  142. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
  143. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +11 -88
  144. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts +19 -0
  145. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -0
  146. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +98 -0
  147. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts +31 -0
  148. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -0
  149. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +99 -0
  150. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts +17 -0
  151. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts.map +1 -0
  152. package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.js +39 -0
  153. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -1
  154. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +7 -63
  155. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
  156. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +13 -30
  157. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
  158. package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +6 -52
  159. package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
  160. package/src/shade/renderer/loader/gltf/load_gltf.js +0 -19
  161. package/src/shade/renderer/material/chunk_material_draw_side.d.ts +14 -0
  162. package/src/shade/renderer/material/chunk_material_draw_side.d.ts.map +1 -0
  163. package/src/shade/renderer/material/chunk_material_draw_side.js +34 -0
  164. package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
  165. package/src/shade/renderer/material/standard/fragment_gbuffer.js +8 -1
  166. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
  167. package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +13 -1
  168. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts +19 -5
  169. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
  170. package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +23 -5
  171. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.d.ts.map +1 -1
  172. package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.js +12 -5
  173. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +22 -0
  174. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts.map +1 -0
  175. package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -0
  176. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +15 -0
  177. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -0
  178. package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +35 -0
  179. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts +10 -1
  180. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts.map +1 -1
  181. package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.js +38 -29
  182. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts +5 -0
  183. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts.map +1 -1
  184. package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.js +63 -55
  185. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.d.ts.map +1 -1
  186. package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.js +1 -0
  187. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
  188. package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +44 -53
  189. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
  190. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +6 -2
  191. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
  192. package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +6 -2
  193. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
  194. package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +19 -2
  195. package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +7 -4
  196. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
  197. package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +3 -2
  198. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.d.ts.map +1 -1
  199. package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.js +15 -4
  200. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  201. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +16 -5
  202. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.d.ts.map +1 -1
  203. package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +16 -5
  204. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
  205. package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +2 -1
  206. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.d.ts.map +1 -1
  207. package/src/shade/renderer/rasterize/native/rasterization_shader_resources.js +4 -0
  208. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
  209. package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +16 -7
  210. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
  211. package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +78 -88
  212. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
  213. package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +21 -31
  214. package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
  215. package/src/shade/renderer/scene/GPUSceneContext.js +2 -0
  216. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +19 -0
  217. package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts.map +1 -1
  218. package/src/shade/renderer/scene/NODE3D_STRUCT.js +34 -0
  219. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts +9 -0
  220. package/src/shade/renderer/scene/chunk_node3d_flags.d.ts.map +1 -0
  221. package/src/shade/renderer/scene/chunk_node3d_flags.js +23 -0
  222. package/src/shade/renderer/scene/chunk_scene_database_access.d.ts.map +1 -1
  223. package/src/shade/renderer/scene/chunk_scene_database_access.js +15 -1
  224. package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
  225. package/src/shade/renderer/scene/rows/GPUSceneRows.js +9 -1
  226. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts +7 -0
  227. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts.map +1 -1
  228. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.js +16 -4
  229. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts +12 -0
  230. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts.map +1 -0
  231. package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.js +18 -0
  232. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts +24 -0
  233. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts.map +1 -0
  234. package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.js +33 -0
  235. package/src/shade/playground/vgeo_viewer/README.md +0 -150
  236. package/src/shade/playground/vgeo_viewer/cut_geometry.js +0 -460
  237. package/src/shade/playground/vgeo_viewer/index.html +0 -77
  238. package/src/shade/playground/vgeo_viewer/main.js +0 -1343
  239. package/src/shade/playground/vgeo_viewer/select_cut.js +0 -481
  240. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +0 -258
  241. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +0 -1
  242. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +0 -49
  243. package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +0 -1
  244. package/src/shade/renderer/loader/gltf/fix_up_material_sides.js +0 -109
  245. package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.js +0 -78
  246. package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +0 -100
  247. package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.js +0 -26
  248. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts +0 -3
  249. package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts.map +0 -1
  250. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.js → level/VGeoLevel.js} +0 -0
  251. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.js → level/VGeoLevelOptions.js} +0 -0
  252. /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.js → level/VGeoLevelSummary.js} +0 -0
  253. /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.js → partition/vgeo_graph_components.js} +0 -0
  254. /package/src/shade/renderer/geometry/virtual/build/{VGeoGatheredFaces.js → submesh/VGeoGatheredFaces.js} +0 -0
  255. /package/src/shade/renderer/geometry/virtual/build/{VGeoSubmesh.js → submesh/VGeoSubmesh.js} +0 -0
@@ -277,16 +277,25 @@ change every frame, you are on the wrong path — see §8.
277
277
 
278
278
  `ShadeMaterial` is the base: `transparency_mode`, `draw_mode` and `draw_side`.
279
279
 
280
- **`draw_side` is part of the rasterization bucket key, and it changes what is drawn.** `Front` (the
281
- default) culls back faces, `Back` culls front faces, and `Double` culls neither — the three map
282
- straight onto the pipeline's cull mode, so a bucket exists per side that any material in the scene
283
- asks for. A face seen from behind is shaded with its normal flipped, so `Double` is a working
284
- double-sided material and not a hint. glTF `doubleSided: true` arrives as `Double` and stays that
285
- way; the loader's fix-up pass only rewrites `Back` to `Front`, by flipping the geometry.
286
-
287
- Shadow passes invert the mapping — `Front` culls *front* faces there — so a one-sided surface casts
288
- from its back face and does not shadow-acne against its own lit side. `Double` has no opposite and
289
- stays unculled in both.
280
+ **`draw_side` changes what is drawn.** `Front` (the default) draws the front face, `Back` the back
281
+ face, `Double` both. A face seen from behind is shaded with its normal flipped, so `Double` is a
282
+ working double-sided material and not a hint. glTF `doubleSided: true` arrives as `Double` and
283
+ stays that way; glTF cannot ask for `Back`, which comes from authoring.
284
+
285
+ **Only `Double` costs a pipeline of its own.** It culls nothing, and no amount of winding expresses
286
+ that. `Front` and `Back` differ by winding alone, and the vertex shader reverses winding per
287
+ instance, so the two share a rasterization bucket and draw in one pass — which matters because a
288
+ bucket is not a pipeline here but a whole chain: extract, expand to meshlets, select a
289
+ virtual-geometry cut, one render pass.
290
+
291
+ **That is also why a mirrored instance works.** Place a mesh under a negative scale, or under a
292
+ parent that reflects, and it draws right way out, with the handedness of its tangent frame flipped
293
+ to match — the reflection and the material's side are the same one bit, combined before a corner is
294
+ chosen. glTF makes this the rule rather than an extra: the determinant of a node's world transform
295
+ decides its winding, and a conformant renderer has to read it.
296
+
297
+ Shadow passes cull the other face — so a one-sided surface casts from its back face and does not
298
+ shadow-acne against its own lit side. `Double` has no opposite and stays unculled in both.
290
299
 
291
300
  `StandardShadeMaterial` is the PBR one everything uses:
292
301
 
@@ -693,7 +693,7 @@ what they are looking at is the observer.
693
693
  ## 7. The inspector
694
694
 
695
695
  `packages/gpu-inspector-tool/`. A static site: drop a `.sgpt` on it, or pass `?file=` for a
696
- bookmarkable view — the affordance the `vgeo_viewer` playground already establishes in this codebase.
696
+ bookmarkable view — the affordance the `.vgeo` playgrounds already establish in this codebase.
697
697
 
698
698
  ### 7.1 Technology
699
699
 
@@ -6,19 +6,26 @@ This page draws nothing itself: every triangle on screen is there because the co
6
6
  the HZB meshlet filter, the material bucket, the indirect draw and the G-buffer took it from there
7
7
  without knowing that virtual geometry exists.
8
8
 
9
- That is the whole difference from [`../vgeo_viewer/`](../vgeo_viewer/README.md), and the two are
10
- worth keeping apart. The viewer draws a **CPU** cut through Shade's direct geometry path — it
11
- proves the DAG, the format and the residency clamp, and by construction it proves nothing about the
12
- engine. This page proves the engine seam and nothing about the DAG, because the DAG has its own
13
- tests and its own oracle.
14
-
15
- And it proves one thing neither the specs nor the viewer can, which is what the surface **looks
16
- like**. The viewer draws with `cullMode: 'none'` and colours its cut per cluster; this page draws
17
- through a `Front`-sided standard material with a light on it. That is the difference that caught
18
- the shared sample torus being wound inside-out — the near surface culled away, the far inner wall
19
- showing through the hole where it should have been, and normals pointing into the tube so what
20
- survived was lit from inside. Eighteen thousand specs and a second playground had drawn that
21
- container without a single one of them being able to notice.
9
+ This page proves the engine seam and nothing about the DAG, because the DAG has its own tests and
10
+ its own oracle: `renderer/geometry/virtual/runtime/cut/__fixtures__/vgeo_select_cut.js`, a **CPU**
11
+ traversal of the same rule that every cut spec is held to. A `vgeo_viewer` page used to draw that
12
+ traversal on screen through Shade's direct geometry path, carrying its own renderer, its own camera
13
+ and its own streaming loop; it was retired rather than kept as a second runtime, and everything it
14
+ showed is shown here or on the scene page below.
15
+
16
+ It is also not [`../vgeo_scene/`](../vgeo_scene/README.md), which opens a whole converted set —
17
+ thousands of containers, tens of thousands of instances, one cut — and is where the claim about a
18
+ *scene* is measured. This page is one container framed on its own bounding sphere, which is what
19
+ makes it a viewer and what makes its readings about the seam rather than about breadth.
20
+
21
+ And it proves one thing no spec can, which is what the surface **looks like**. The retired viewer
22
+ drew with `cullMode: 'none'` and coloured its cut per cluster; this page draws through a
23
+ `Front`-sided standard material with a light on it. That is the difference that caught the sample
24
+ torus being wound inside-out — the near surface culled away, the far inner wall showing through the
25
+ hole where it should have been, and normals pointing into the tube so what survived was lit from
26
+ inside. Eighteen thousand specs and a second playground had drawn that container without a single
27
+ one of them being able to notice, which is the argument for this page and part of the argument
28
+ against the other one.
22
29
 
23
30
  ## How it is lit, and why that is not decoration
24
31
 
@@ -138,6 +145,13 @@ The panel's `RAM evicted`, `fetching` and `worker fetches` rows are new with it,
138
145
  the reading to keep watching: it climbs only while the tier is full of pages the cut still draws
139
146
  from, which is a budget saying no and not a tier scanning.
140
147
 
148
+ **`quarantined`** came here from the retired viewer, and it is the row that tells two coarse
149
+ surfaces apart. A page that fails its retries is quarantined with its reason and is never asked for
150
+ again; the clamped cut then keeps that region correct with a coarser ancestor, indefinitely. On
151
+ screen that is indistinguishable from a container that is merely still arriving, and `clamped`
152
+ alone cannot separate them — a count that will not fall while this is non-zero is a page that is
153
+ never coming.
154
+
141
155
  Two things the tour still shows. The four long tasks left are `GPUMeshletManager` compactions —
142
156
  a refused allocation with free space behind it rebuilds every batch, which is the one O(resident)
143
157
  event the rebuild deferred. And under a vite dev server the first seconds after a container opens
@@ -28,15 +28,15 @@ import { place_primitive } from "../../renderer/scene/rows/place_primitive.js";
28
28
  import { Scene } from "../../renderer/scene/Scene.js";
29
29
  import { load_default_environment } from "../load_default_environment.js";
30
30
  import { install_profile_hotkey } from "../profile_hotkey.js";
31
- import { vgeo_sample_source } from "../vgeo_viewer/sample_asset.js";
31
+ import { vgeo_sample_source } from "./sample_asset.js";
32
32
 
33
33
  /**
34
34
  * A `.vgeo` container drawn **through the meshlet pipeline**, by a cut selected on the GPU.
35
35
  *
36
- * This is the other half of what `playground/vgeo_viewer/` proves, and the two are worth keeping
37
- * apart. The viewer draws a CPU cut through Shade's direct geometry path: it proves the DAG, the
38
- * format and the clamp, and by construction it proves nothing about the engine. This page draws
39
- * nothing itself. It puts an ordinary mesh row in an ordinary `Scene` with an ordinary material,
36
+ * What proves the DAG, the format and the clamp is not a page:
37
+ * `renderer/geometry/virtual/runtime/cut/__fixtures__/vgeo_select_cut.js` is a CPU traversal of the
38
+ * same rule, and every cut spec is held to it. This page proves the other half — the engine seam —
39
+ * and it draws nothing itself. It puts an ordinary mesh row in an ordinary `Scene` with an ordinary material,
40
40
  * and every triangle on screen arrives because the compute cut appended `MESHLET_DEFINITION_STRUCT`
41
41
  * records into the same collection a static mesh's meshlets go into — through the HZB meshlet
42
42
  * filter, the material bucket, the indirect draw and the G-buffer, none of which know that virtual
@@ -617,6 +617,7 @@ class VGeoPlayground {
617
617
  bytes: this.residency.ram_resident_bytes,
618
618
  evicted: this.residency.evicted_ram_count,
619
619
  refused: this.residency.refused_fetch_count,
620
+ quarantined: this.residency.quarantined_page_count,
620
621
  total_pages: reader?.header.total_pages ?? 0,
621
622
  },
622
623
  fetches_in_flight: this.residency.fetches_in_flight,
@@ -917,6 +918,18 @@ async function start() {
917
918
  `<span class="${state.vram.refused > 0 ? 'bad' : 'good'}">`
918
919
  + `${state.vram.refused.toLocaleString()}</span>`,
919
920
  ],
921
+ /*
922
+ A page that failed its retries is quarantined with its reason and is never asked for
923
+ again, and the clamped cut keeps its surface correct with a coarser ancestor
924
+ indefinitely. That is the difference between a container that is coarse because the
925
+ camera just moved and one that is coarse because a page will never arrive — which look
926
+ identical on screen, and which `clamped` alone cannot tell apart.
927
+ */
928
+ [
929
+ 'quarantined',
930
+ `<span class="${state.ram.quarantined > 0 ? 'bad' : 'good'}">`
931
+ + `${state.ram.quarantined}</span>`,
932
+ ],
920
933
  ['dropped', `<span class="${dropped > 0 ? 'bad' : 'good'}">${dropped}</span>`],
921
934
  ].map(([name, value]) => `<div class="row"><span>${name}</span><span>${value}</span></div>`)
922
935
  .join('');
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Build a sample container and hand back a source over it.
3
+ *
4
+ * The page size is deliberately far below the 256 KB default: a sample that came out as one page
5
+ * would be a container with nothing to stream, and the point of opening this one is to watch it
6
+ * arrive.
7
+ *
8
+ * @param {number} [major]
9
+ * @param {number} [minor]
10
+ * @param {number} [page_size] target decoded bytes per page
11
+ * @returns {{source: VGeoByteSource, build_ms: number}}
12
+ */
13
+ export function vgeo_sample_source(major?: number, minor?: number, page_size?: number): {
14
+ source: VGeoByteSource;
15
+ build_ms: number;
16
+ };
17
+ import { VGeoByteSource } from "../../renderer/geometry/virtual/format/read/VGeoByteSource.js";
18
+ //# sourceMappingURL=sample_asset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sample_asset.d.ts","sourceRoot":"","sources":["../../../../../src/shade/playground/vgeo_runtime/sample_asset.js"],"names":[],"mappings":"AA2GA;;;;;;;;;;;GAWG;AACH,2CALW,MAAM,UACN,MAAM,cACN,MAAM,GACJ;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,CAiBtD;+BA3HM,+DAA+D"}
@@ -12,7 +12,7 @@ import {
12
12
  } from "../../renderer/geometry/virtual/format/read/VGeoByteSource.js";
13
13
 
14
14
  /**
15
- * A `.vgeo` built in the page, so the viewer has something to open without a file.
15
+ * A `.vgeo` built in the page, so there is something to open without a file.
16
16
  *
17
17
  * The builder is environment-agnostic by design (`VIRTUAL_GEOMETRY_DESIGN.md` §8) — typed arrays and
18
18
  * topology, no file or process API anywhere under it — so the same call the node CLI makes runs here.
@@ -31,8 +31,8 @@ import {
31
31
  *
32
32
  * Curvature is what makes it a useful sample rather than a shape: a flat face simplifies at zero
33
33
  * quadric cost, so a box would produce a DAG whose every step records no error and whose every
34
- * threshold selects the same cut. A torus has an error at every level, which is the thing the viewer
35
- * is drawing.
34
+ * threshold selects the same cut. A torus has an error at every level, which is the thing the cut
35
+ * is selecting against.
36
36
  *
37
37
  * @param {number} major segments the long way round
38
38
  * @param {number} minor segments around the tube
@@ -61,12 +61,13 @@ function torus(major, minor) {
61
61
  `CullPolicy.Standard` means by a front face and what `vgeo_compute_vertex_normals` takes the
62
62
  sign of its normals from.
63
63
 
64
- It was the other way round, and both of the pages that build this were blind to it: the viewer
65
- draws its cut with `cullMode: 'none'` and colours it per cluster, so neither the culling nor
66
- the shading could report it. The moment the same sample went through the engine's material
67
- path — `playground/vgeo_runtime/`, where a `Front`-sided material culls back faces — the torus
64
+ It was the other way round, and the CPU viewer this sample was written for could not see it:
65
+ that page drew its cut with `cullMode: 'none'` and coloured it per cluster, so neither the
66
+ culling nor the shading could report it. The moment the same sample went through the engine's
67
+ material path — this page, where a `Front`-sided material culls back faces — the torus
68
68
  rendered as its own far inner wall with the near surface missing, and the normals pointed into
69
- the tube so what survived was lit from inside.
69
+ the tube so what survived was lit from inside. The viewer has since been retired; this is the
70
+ only page that builds it now, and it is the one that could have caught it.
70
71
 
71
72
  The winding of a sample is not a detail of the sample. Everything downstream of the builder
72
73
  carries it: the cluster's triangles are stored in this order, the DAG's simplification
@@ -0,0 +1,408 @@
1
+ # `vgeo_scene` — a whole converted scene, one cut
2
+
3
+ Thousands of `.vgeo` containers, tens of thousands of instances, **one** compute cut, drawn through
4
+ the ordinary meshlet pipeline.
5
+
6
+ [`../vgeo_runtime/`](../vgeo_runtime/README.md) is the page that proves the engine seam, and it does
7
+ it with one container framed on its own bounding sphere. That makes it a *viewer*, and by
8
+ construction it cannot show the other half of what
9
+ [`VIRTUAL_GEOMETRY_DESIGN.md`](../../renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md) §1
10
+ claims — that rendering cost is a function of the threshold and the resolution rather than of the
11
+ source, **across every asset in view at once**, with instances sharing pages and one residency
12
+ budget arbitrating between all of them. With one reader there is nothing to arbitrate.
13
+
14
+ So everything here that the runtime page does not do is about breadth:
15
+
16
+ - **One residency, N containers.** Every asset registers with the same `VGeoResidency`, whose two
17
+ budgets are scene-wide by construction: the RAM tier's bytes and the VRAM tier's slots are counted
18
+ across every reader, and a page evicted from one asset is room for another's.
19
+ - **One cut, N instances.** `shader_vgeo_cut_collect` runs over the visible mesh rows and emits one
20
+ instance record per virtual one; the descent is seeded with one item per instance and every cull
21
+ and error test downstream runs in that instance's own object space. Seventeen thousand instances
22
+ are seventeen thousand seeds of **one** wavefront, not seventeen thousand cuts.
23
+ - **Instances share the asset.** A geometry placed by 381 nodes is one reader, one geometry row, one
24
+ page lookup and one set of resident pages — which is `VirtualGeometry.equals`, and most of why
25
+ this is affordable at all.
26
+
27
+ ## Running it
28
+
29
+ Three things: the dev server in front of the page, an asset server in front of the containers, and
30
+ a scene manifest telling the page which is which.
31
+
32
+ ```
33
+ npm run dev --workspace @woosh/meep-engine
34
+
35
+ node tools/geometry/virtual/vgeo_scene.js <manifest.vgeo.json> --camera "..." --sun "..."
36
+ node tools/geometry/virtual/vgeo_serve.js <container directory>
37
+ ```
38
+
39
+ Then open `/src/shade/playground/vgeo_scene/index.html`. With no parameters it asks the asset server
40
+ what it is holding and opens the one scene that is there; `?scene=<url>` overrides.
41
+
42
+ **The asset server is not optional and vite cannot replace it.** A `.vgeo` is opened out of its head
43
+ and streamed by `Range`, and `vgeo_fetch_byte_source` refuses anything but a `206` naming the range
44
+ it asked for — a `200` is a server ignoring the request and answering the head of the file where the
45
+ reader asked for the middle of a frame, silently and with plausible numbers. Vite also refuses
46
+ `/@fs/` outside the workspace, and a converted scene is tens of gigabytes that has no business
47
+ inside one. `vgeo_serve.js` is ranges, CORS and nothing else; the header that is easy to miss is
48
+ `Access-Control-Expose-Headers`, because a cross-origin response's `Content-Range` is invisible to
49
+ the page without it.
50
+
51
+ ## What a scene manifest is, and what it is not
52
+
53
+ `gltf_to_vgeo.js` already writes everything the page needs — a glTF primitive is one container and
54
+ the nodes that placed it are its instances — but its manifest is a *build report*: every level of
55
+ every DAG, every cluster and group count, the weld distance each geometry settled on. At this scale
56
+ that is twenty-odd megabytes of numbers a renderer has no use for. `vgeo_scene.js` is the projection
57
+ to four fields and a list of matrices: **2.2 MiB for Zorah's 2,966 containers and 17,424 instances**,
58
+ and nothing in it is discovered — it is all in the build manifest or in the containers' own headers.
59
+
60
+ The headers are **read rather than trusted**. A manifest is written by the builder that produced the
61
+ files and a directory is whatever is on disk now, so the probe is what catches a half-finished
62
+ conversion and a container rebuilt at another page size. It costs one head read each and 1.8 s for
63
+ all 2,966.
64
+
65
+ Two fields are neither: `--camera` and `--sun`.
66
+
67
+ - **A bounding box is a bad place to put a camera.** Zorah's instances span 107 km, almost all of it
68
+ terrain, and the town the scene is *about* is forty metres across at the origin — a viewer framed
69
+ on the box opens a hundred kilometres up looking at a speck. The pose belongs to the source, and
70
+ Zorah's own `.cfg` carries one, so it is passed in rather than guessed at either end.
71
+ - **A converted container carries geometry and nothing else.** Design §7: VG is geometry only, so a
72
+ converted set arrives with normals, no material authoring and no lights. The sun is most of what
73
+ there is to shade it with, and the engine's default direction over a scene authored around a
74
+ different one puts every façade a viewer stands in front of into ambient.
75
+
76
+ ## What it measures
77
+
78
+ Zorah, 2,966 containers and 17,424 instances of them, **46.71 GiB** on the server across 947,664
79
+ pages, 1.36 billion source triangles and **10.75 billion instanced** ones. Measured on an
80
+ nvidia/lovelace adapter at a render target of 1578×746, from the scene's authored viewpoint, with a
81
+ **fresh page load per row** — residency is a high-water mark, so a page swept downwards through the
82
+ thresholds reports at every step what the busiest setting it has already been at asked for, and a
83
+ row of this table is only about τ if the page has been at nothing else.
84
+
85
+ | τ (px) | drawn clusters | pages visited | clusters tested | resident pages | bytes read | frame |
86
+ |---:|---:|---:|---:|---:|---:|---:|
87
+ | 0.25 | 387,875 | 79,574 | 1,232,352 | 12,288 *(full)* | 452 MB | 7.3 ms |
88
+ | 0.5 | 182,887 | 51,035 | 617,540 | 9,365 | 242 MB | 5.0 ms |
89
+ | 1 | 84,481 | 31,842 | 279,088 | 6,387 | 128 MB | 4.7 ms |
90
+ | 2 | 45,302 | 22,356 | 139,290 | 4,939 | 76 MB | 4.8 ms |
91
+ | 4 | 28,874 | 17,483 | 79,651 | 4,121 | 52 MB | 5.2 ms |
92
+ | 8 | 21,334 | 15,235 | 56,561 | 3,704 | 40 MB | 4.8 ms |
93
+ | 16 | 18,137 | 14,289 | 45,717 | 3,489 | 35 MB | 6.0 ms |
94
+
95
+ Monotone in τ, and a *fraction* of the asset at every setting: at τ=1 the scene is on screen out of
96
+ **0.67% of its pages** and 128 MB of 46.71 GiB, and at τ=16 out of 0.37% and 35 MB. `clamped` is
97
+ zero at every row but the first, which is the streaming having finished rather than a budget having
98
+ given up — the distinction the `refused` counter beside it exists to make.
99
+
100
+ **τ=0.25 is the budget wall, and it is in the table on purpose.** At the slider's minimum the cut
101
+ wants more than 12,288 page slots, `refused` climbs to 1.1 million, `clamped` stops falling to zero,
102
+ and the cut is then being selected by a **budget** rather than by a threshold — which is the one
103
+ thing this page is not meant to be showing you. It is one slider notch away at all times, and
104
+ `refused` going red is how you know that is what you are looking at. `?pages=` raises the tier.
105
+
106
+ A tour of five viewpoints at τ=1 — a hundred metres up, three hundred out, a nose against one
107
+ façade, the whole 107 km box, and back to the authored pose — held 4.5 to 5.8 ms a frame with
108
+ `clamped`, `refused` and every drop counter at zero throughout, and residency settling at 7,948 of
109
+ 12,288 pages without a single eviction.
110
+
111
+ ## Opening it, and what the first five seconds are
112
+
113
+ Measured cold, three times: **2,966 containers open in about four seconds**, and the whole scene is
114
+ drawing about a second and a half later.
115
+
116
+ The opens are one range request each. §10's always-resident set — header, level table, root frame —
117
+ is 2,560 bytes at the median across this set and 6,528 at the very worst, so the page asks for an
118
+ **8 KB head prefix** rather than the reader's default of 64 KB: every container in the set opens in
119
+ exactly one round trip and the cold read is 7.4 MB of heads instead of 190 MB of over-fetch. The
120
+ byte length comes from the manifest rather than from a `HEAD`, which is 2,966 round trips not spent
121
+ learning something the page was already holding.
122
+
123
+ **About four and a half seconds of that is not this page.** The first frame that draws geometry pays
124
+ the engine's pipeline warm-up, and `vgeo_runtime` — driven the same way, with a thirteen-page torus
125
+ built in-page — sits on exactly the same stall before *its* first frame. It is a fixed cost of a
126
+ cold device under a dev server, it is not a function of the scene, and a bundled build does not pay
127
+ all of it. Containers keep opening underneath it.
128
+
129
+ Containers are **placed as they land** rather than at the end, which costs nothing: a geometry that
130
+ is not registered yet is one the cut does not collect, and both residency tiers recompute their
131
+ budgets per reading rather than at registration, so a scene that grows under the camera is the case
132
+ they were designed for. A container that refuses leaves every other asset exactly as it was and is
133
+ counted — 2,965 of 2,966 is a scene, and the count is what says there is a hole in it.
134
+
135
+ ## The four capacities a scene reaches first
136
+
137
+ These are the page's real finding, because every one of them is an engine default that a
138
+ single-asset page never touches, and two of them degrade *silently*.
139
+
140
+ - **`queue_capacity`, and this is the one that hurt.** At τ=0.5 with 17,424 instances the descent
141
+ pushed past the engine's 65,536 items in flight and `VGEO_CUT_STAT.QUEUE_DROPPED` came back at
142
+ **32,676** — a third of the frontier thrown away. It does not surface as an error or as a visible
143
+ hole; it surfaces as `drawn` wandering over ten thousand clusters between one frame and the next,
144
+ as different subtrees lose the race. At `1 << 18` the same view drops nothing and `drawn` is
145
+ stable, at a cost of 4 MiB a site against 1 MiB.
146
+ - **`lookup_words`** is one word per page of every registered container out of one arena, and the
147
+ default of 65,536 holds a fourteenth of Zorah's 947,664. It is allocated out of an
148
+ `OffsetAllocator` whose bins round *down* — it can refuse an allocation that fits — so this is
149
+ sized to the next power of two above 1.5× the total rather than to the total.
150
+ - **`geometry_capacity`** is one row per container against a default of 256.
151
+ - **`claim_word_capacity`** is one bit per (collected instance, page ordinal of its geometry), and
152
+ the engine derives it from a hint of a thousand instances times the whole tier. At this scale that
153
+ is both too small and too large, and the real bound is knowable exactly: ordinals come out of the
154
+ tier, so `Σ instances × ordinals` is maximised by giving every slot to the most-instanced asset,
155
+ which makes it `max instances of any one geometry × page capacity`. For Zorah that is 381 × the
156
+ tier — a fifth of what the default derivation asks for — doubled here, because an ordinal capacity
157
+ is a high-water mark and a scene that has churned can hold more of them than the tier holds pages.
158
+
159
+ `RasterizationCollectionType.Meshes` is a fifth, and it self-corrects: it grows from what the frame
160
+ reports, so priming it to the instance count only buys the first seconds. But the first seconds are
161
+ when the scene is being placed, and against a default of 1e4 the other 7,424 instances would be
162
+ counted as `instances_dropped` while the collection caught up.
163
+
164
+ ## Two things it approximates, and says so
165
+
166
+ - **Shear.** A transform row carries a TRS and the GPU composes the world from it, so an instance
167
+ whose matrix has shear in it cannot be represented exactly and is placed at the closest
168
+ rigid-plus-scale fit. 391 of Zorah's 17,424 have any and the worst is about eleven degrees off
169
+ orthogonal; the other 17,033 are exact, including the 2,722 reflected ones — `m4_decompose_array`
170
+ folds a negative determinant into a scale axis, so a reflection survives. This is a property of
171
+ the engine's transform model rather than of this page. The reflections surviving is what the next
172
+ section is about.
173
+ - **One material for the whole scene.** Not a simplification being made here: a `.vgeo` carries
174
+ geometry only, so the source's 2,714 materials did not survive a conversion that never asked them
175
+ to. Tinting each container would read better, would be a lie about where the colour came from, and
176
+ would put 2,714 material buckets in front of the draw path to say it with.
177
+
178
+ ## Reflected instances — the bug this page was built to show, and where it went
179
+
180
+ 2,722 of Zorah's 17,424 instances have a negative-determinant transform: the courtyard is
181
+ bilaterally symmetric and the left half is the mirror of the right. Until 2026-09-15 every one of
182
+ them rendered with its near surface culled and its far surface showing through, because:
183
+
184
+ 1. A reflection reverses the orientation of every triangle it transforms, so a reflected instance
185
+ presents the **opposite screen-space winding** from the same geometry under a rotation.
186
+ 2. Front-face determination is fixed-function and reads that winding and nothing else.
187
+ 3. `cullMode` is **pipeline state chosen per material bucket**, and the bucket key was three fields
188
+ of a *material* — transparency, topology and side — with `construct_primitive_state` turning the
189
+ third into a cull mode. Nothing from there down to the rasterizer saw a transform.
190
+
191
+ The sign was never lost — `m4_decompose_array` preserves it and the row the GPU composes reflects
192
+ exactly as the source did — it was simply never consulted.
193
+
194
+ **It is consulted now, and not through the bucket key.** A node's row carries
195
+ `NODE3D_GLOBAL_FLAG_MIRRORED`, derived from its composed world matrix by whoever publishes it, and
196
+ every vertex shader over a meshlet emits that instance's triangle corners in the opposite order —
197
+ `triangle_list_reverse_winding`. The draw is non-indexed and the vertex shader resolves its own
198
+ index, so which corner lands where is already the shader's choice; reversing it costs a `select`,
199
+ no second pipeline, and no fourth field in the bucket key. It is what Nanite does for the same
200
+ reason (`FInstanceSceneData::DeterminantSign`, read in `HWRasterizeVS`), and what the glTF spec
201
+ requires of a conformant renderer. The tangent frame's handedness flips with it.
202
+
203
+ The parameters remain, and now read as diagnostics rather than as fixes:
204
+
205
+ - `?mirrored=tint` colours reflected instances red — in this view the **whole left arcade** comes up
206
+ red while the right stays grey. Nothing about which of them is *drawn* changes.
207
+ - `?mirrored=flip` draws them `Back`-sided. This used to be the page's own fix; `Back` is now drawn
208
+ by reversing winding too, so on a reflected instance the two cancel and it reproduces the old bug
209
+ on demand.
210
+ - `?mirrored=double` draws both sides of them — the fix people reach for first, and still the wrong
211
+ one: it doubles the work and gets the shading wrong.
212
+ - `?side=front|back|double` sets the whole scene's side, for the cruder question of whether a
213
+ surface is missing or merely culled.
214
+
215
+ The bug was hard to recognise because `fragment_gbuffer` flips normal, tangent and face normal when
216
+ `dot(world_face_normal, view_direction) < 0` — a test about the *view*, not about the winding — so
217
+ the far wall you were left looking at was correctly lit, and it read as bad geometry rather than as
218
+ a culling problem. Nothing about any of it was virtual-geometry-specific: it reached every mesh
219
+ placed under a mirroring transform, by any loader or prefab.
220
+
221
+ ## Two different things are called flicker here, and they have different causes
222
+
223
+ **A dropout is not a shimmer and they do not measure alike.** A shimmer moves a tenth of a per cent
224
+ of the pixels and comes from the TAA jitter under sub-pixel geometry; a frame missing a large part
225
+ of what the frames either side of it drew moves tens of per cent and comes from the descent running
226
+ out of wavefronts. One section each, below. Measuring the first when asked about the second cost a
227
+ whole pass of this investigation, and the tell that separates them is the amplified frame
228
+ difference: an **edge map** is the shimmer, **solid blobs** are the dropout.
229
+
230
+ ## The shimmer is the TAA jitter, and the geometry is finer than a pixel
231
+
232
+ There is a visible shimmer over the ornament, and it is **exactly 16 frames long**.
233
+
234
+ Measured over 600 consecutive frame pairs at 780×368 samples (~2×2 screen pixels each), camera
235
+ static, everything settled:
236
+
237
+ | | pixels changing >8/255 per frame | >24/255 | worst single step | period |
238
+ |---|---:|---:|---:|---:|
239
+ | as shipped | 350 (0.12%) | 17 | 57/255 | 16 (r=0.97) |
240
+ | τ=16 | 182 (0.06%) | 13 | 69/255 | 16 (r=0.95) |
241
+ | `?mirrored=flip` | 314 (0.11%) | 21 | 56/255 | 16 (r=0.96) |
242
+ | TAA off | **79,391 (27.66%)** | 33,841 | 161/255 | 16 (r=0.97) |
243
+ | jitter pinned | 89 (0.03%) | 3 | 88/255 | 16 (r=0.96) |
244
+
245
+ Sixteen is `TAA.#jitter_sequence_size`, and the attribution follows from the last two rows. **Pin the
246
+ jitter and fifteen frames out of every sixteen go pixel-static** — 18 to 123 changed samples out of
247
+ 287,040, which is the noise floor — against 240 to 470 *every* frame as shipped. The flicker is the
248
+ Halton jitter cycling the projection under geometry whose detail is finer than a pixel, with TAA
249
+ resolving most but not all of it.
250
+
251
+ An amplified `|frame(t) − frame(t−1)|` over eighteen consecutive pairs says the same thing in one
252
+ picture: it is an **edge map**. Every silhouette and every piece of filigree sparkles; the flat
253
+ courtyard floor is dead black. LOD popping would light up in cluster-shaped patches and does not.
254
+
255
+ Three things fall out of it that are worth writing down:
256
+
257
+ - **`feature_taa_enabled = false` does not turn the jitter off.** `Renderer` writes
258
+ `view_ctx.camera.setViewportOffset` from the jitter sequence unconditionally, and only the
259
+ *resolve* is behind the flag. So TAA off is not "no temporal AA", it is "jitter with nothing to
260
+ resolve it": 27.66% of pixels move every frame, which is forty times worse than shipping it on and
261
+ worse than never jittering at all. Anyone A/B-ing TAA on this content is not measuring what they
262
+ think.
263
+ - **The jitter does move the cut, slightly, and it is not where the flicker comes from.** `drawn`
264
+ and `pages visited` are weakly 16-periodic (r=0.34 and r=0.40) because the cut's frustum planes
265
+ come out of the *jittered* view-projection while `projection_k` — the error metric — does not. So
266
+ what moves is which clusters are inside the frustum, by up to 117 of 84,455 (0.14%), confined to
267
+ the screen border. `vgeo_runtime`'s README says the jitter does not move the cut and its τ table is
268
+ identical either way; at this scale that is very nearly true rather than exactly true, and the
269
+ difference is not visible in the diff image.
270
+ - **`GPUViewContext.setJitter` and `setJitterDelta` write the same two fields.** Only the first is
271
+ called, so nothing is broken today; a caller that used the second would silently overwrite the
272
+ jitter with its delta.
273
+
274
+ None of this is the scene's fault and τ=16 halving the count is the proof: less sub-pixel detail,
275
+ less shimmer, same period. It is what a temporal filter does on content authored at a density no
276
+ rasterizer resolves, which is the content virtual geometry exists to put on screen — so it is worth
277
+ knowing that the page's picture has a floor set by TAA rather than by the cut.
278
+
279
+ ## The dropout: the descent's wavefront bound, and how the engine finds it
280
+
281
+ **`max_iterations` starts at 8, and at a large viewport that is the binding bound rather than the
282
+ safety one.** The descent then stops with items still queued, and a queued item is a **subtree that
283
+ was never expanded** — so what it would have drawn is not on screen that frame. It is the one
284
+ degradation of this cut that costs a *hole* rather than a coarser surface, which is why it reads as
285
+ geometry disappearing rather than as anything softening.
286
+
287
+ Reported from a live session at **3441×1440** with `dropped: queue 82` in the panel and `wavefronts`
288
+ jittering between 8 and 13. Reproduced at that pose and viewport, 900 frames, camera static, with
289
+ the bound pinned:
290
+
291
+ | pinned `max_iterations` | `queue_remaining` non-zero | worst | `drawn` swing |
292
+ |---|---:|---:|---:|
293
+ | **8** | **750 of 900 frames** | 13,467 items | **11,857 clusters** |
294
+ | 16 / 32 / 64 | 0 | 0 | 81 |
295
+
296
+ `drawn` going from a swing of 11,857 clusters to **81** is the flicker, in one number. An amplified
297
+ `|frame(t) − frame(t−1)|` says the same thing in a picture: at 8 it is **solid white blobs** — whole
298
+ pieces of carved ornament blinking on and off — and above it those are gone, leaving only the thin
299
+ edge shimmer of the section above. The two phenomena look nothing alike once they are separated.
300
+
301
+ **It is a resolution story, and that is why it took so long to find.** The cut is a function of the
302
+ render target, so the same viewpoint at 1578×746 — a quarter of the pixels — drains inside 8
303
+ wavefronts and never drops an item. Every negative result in this file's earlier drafts was measured
304
+ at that size. A quarter-sized window is not a smaller version of this test.
305
+
306
+ ### The bound raises itself
307
+
308
+ Nothing here pins it. `GPUVirtualGeometryRuntime.note_incomplete_descent` takes the one reading that
309
+ says the descent was truncated and **doubles the bound, permanently**, which is the same shape the
310
+ rasterizer already uses for meshlet counts: a quantity that cannot be known before the work runs,
311
+ learned from what the work reported, kept as a high-water mark.
312
+
313
+ It has to be learned, because how many wavefronts a cut needs is a function of the frontier's
314
+ *breadth* — the scene, the viewpoint and the render target together — and neither the kernel before
315
+ it runs nor the host at any time can compute it. Doubling rather than solving for it, because
316
+ `QUEUE_REMAINING` is a count of *items* and there is no ratio converting those into wavefronts. It
317
+ is never lowered: a frame that drained is evidence about that frame, not that the scene got smaller,
318
+ and a bound that fell back would re-learn the same lesson at a hole a time every time the camera
319
+ turned away and back. An unused wavefront is an empty indirect dispatch, which is the cheap side of
320
+ that trade.
321
+
322
+ **What it costs when it is wrong is one frame.** Measured cold at 3419×1286 on the reported
323
+ viewpoint: `queue_remaining` non-zero on **1 frame of 600**, and zero for the rest of the session.
324
+ The ceiling is 64, so the worst case is three such frames, once, ever.
325
+
326
+ The panel shows `wavefronts` as *used / bound*, with an arrow and a count when the bound has moved,
327
+ because eight wavefronts is a drained descent under a bound of 32 and a truncated one under a bound
328
+ of 8 — and the reading could not tell them apart while it showed only the count.
329
+
330
+ `?iterations=N` pins both the start and the ceiling, which turns the escalation off and reproduces
331
+ the hole on demand: at 8 the same view gives `queue_remaining` on 395 frames of 500 and a `drawn`
332
+ swing of 15,874.
333
+
334
+ **`descent_steps` is the wrong knob for this.** It sets how many levels one dispatch advances, so it
335
+ answers depth; what runs out here is *breadth* — a wave keeps one child per lane and spills the
336
+ rest, and the spill generations are what consume the bound. Measured at the small viewport, 24
337
+ against 48 left `drawn` bit-identical.
338
+
339
+ ### Catching it where it happens
340
+
341
+ `?catch=0.15` arms {@link FlickerCatcher} — a per-frame detector in the page itself. It judges each
342
+ frame against the pair before it **and** the pair after it, taking the smaller of the two
343
+ departures and only when both point the same way. Judging against the median of all four is the
344
+ obvious thing and is wrong: when the picture takes a persistent *step* — the scene streaming in, a
345
+ coarse level replaced by a fine one — two of the four sit on the far side, the median lands between
346
+ the levels, and the frame *before* the step is convicted for being where it always was. Fourteen
347
+ poses each "caught" one that way. On a step the smaller departure is ~0 by construction, which is
348
+ what makes the corrected test score the reported event at 62% and every one of those artifacts at
349
+ or under 1%.
350
+
351
+ It works on **pixels rather than counters**:
352
+ the cut's statistics come from a readback capped at two in flight and skipped on more than half the
353
+ frames, so `drawn` on any given frame is a reading from one to three frames ago and a one-frame
354
+ disappearance need never appear in it at all. That is why the panel's numbers look calm during one.
355
+
356
+ ```
357
+ ?catch=0.15 count them, panel shows a running tally — fly around looking for one
358
+ ?catch=0.15&freeze=1 stop on the first one and leave that frame on screen
359
+ ```
360
+
361
+ **"copy camera link" is how a viewpoint gets out of the browser.** A viewpoint is the one piece of
362
+ state a person has and a session does not: everything else this page can be told is a URL parameter
363
+ already, and a phenomenon that happens "in many places" is not chaseable without one of them. The
364
+ button writes a link that reopens the page where the camera is standing — `?eye=`/`?at=` from the
365
+ live pose, `?tau=` from the slider as it stands rather than as the page was opened, and every other
366
+ parameter left exactly as it was, so a link captured with `?catch=0.15&mirrored=tint` on comes back
367
+ with both still armed. It goes to the clipboard, and to a selected box under the button when a
368
+ browser declines that. `__camera_url()` is the same string for a session with no pointer.
369
+
370
+ **An event carries its own camera link**, captured at the instant it fires, so `__caught()` is
371
+ self-contained: where it happened is the one thing a report of a one-frame dropout cannot be
372
+ reconstructed without, and by the time anybody reads the report the camera may have moved. The held
373
+ frame goes on *under* the panel, so freezing does not hide the readings that explain it.
374
+
375
+ `__caught()` reports what it has seen and the counters for the five frames around the last one;
376
+ `__catch_rows()` is the per-frame log. Freezing also holds the five frames around it, and
377
+ `__show_frame(n)` pages between them — the frame before and the frame itself, side by side, is what
378
+ says whether the geometry went missing or merely moved.
379
+
380
+ It costs a whole-canvas `drawImage` and a small `getImageData` per frame: 4.2 ms a frame becomes
381
+ 10.3 ms here, which is slack on a 60 Hz display and is why the full-resolution ring is only kept
382
+ when `freeze=1` is asked for.
383
+
384
+ ## Driving it
385
+
386
+ `__state()` is the whole surface a headless session needs and the panel shows nothing it does not:
387
+ the scene's counts, the cut's counters, both residency tiers, the render target's size and the
388
+ camera. `__page.runtime.tau` is the threshold, `__look(eye, target)` stands the camera somewhere,
389
+ and `__page.viewpoints` is what the buttons in the panel are.
390
+
391
+ URL parameters, all of them for a session rather than for a person: `?scene=` the manifest,
392
+ `?tau=` the threshold **before the first container** (see the table above for why a load per row),
393
+ `?pages=` the VRAM tier's slots, `?iterations=` the descent's wavefront ceiling,
394
+ `?eye=x,y,z&at=x,y,z` a viewpoint, `?side=front|back|double` the whole scene's draw side,
395
+ `?mirrored=flip|tint|double` the reflected instances' — and `?catch=<fraction>[&freeze=1]` the
396
+ dropout catcher. `__state().scene.mirrored` is the reflected count the panel shows, and `__caught()`
397
+ the catcher's report.
398
+
399
+ **`T` records a GPU capture and `T` again downloads an `.sgpt`** — the same key and the same module
400
+ (`playground/profile_hotkey.js`) every other playground binds. It answers the question the panel
401
+ cannot: every reading above is a *count* and not one of them is a cost. The capture's note carries
402
+ τ, the container and instance counts, the cut it selected and the resolution, because the same
403
+ 0.4 ms is a triumph at eighty thousand clusters and a scandal at eighteen thousand.
404
+
405
+ The headless recipe is `vgeo_runtime`'s and has not changed — `--enable-unsafe-webgpu` and nothing
406
+ else, pick the CDP target by URL, resize the window rather than the viewport, and poll for `__state`
407
+ rather than trusting `load`. Its README is where those four are written down and why each one costs
408
+ an hour.