@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.
- package/build/bundle-worker-terrain.js +1 -1
- package/package.json +1 -1
- package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts +15 -0
- package/src/core/geom/3d/mat4/m4_linear_determinant.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_linear_determinant.js +24 -0
- package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts +4 -2
- package/src/core/geom/3d/mat4/m4_normal_matrix3.d.ts.map +1 -1
- package/src/core/geom/3d/mat4/m4_normal_matrix3.js +21 -11
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +38 -1
- package/src/engine/graphics3/GraphicsEngine.d.ts +5 -1
- package/src/engine/graphics3/GraphicsEngine.d.ts.map +1 -1
- package/src/engine/graphics3/GraphicsEngine.js +834 -830
- package/src/shade/RENDERER_CONTRACT.md +19 -10
- package/src/shade/device/timing/GPU_PROFILER_PROPOSAL_2026_08_28.md +1 -1
- package/src/shade/playground/vgeo_runtime/README.md +27 -13
- package/src/shade/playground/vgeo_runtime/main.js +18 -5
- package/src/shade/playground/vgeo_runtime/sample_asset.d.ts +18 -0
- package/src/shade/playground/vgeo_runtime/sample_asset.d.ts.map +1 -0
- package/src/shade/playground/{vgeo_viewer → vgeo_runtime}/sample_asset.js +9 -8
- package/src/shade/playground/vgeo_scene/README.md +408 -0
- package/src/shade/playground/vgeo_scene/index.html +144 -0
- package/src/shade/playground/vgeo_scene/main.d.ts +2 -0
- package/src/shade/playground/vgeo_scene/main.d.ts.map +1 -0
- package/src/shade/playground/vgeo_scene/main.js +1716 -0
- package/src/shade/renderer/Renderer.d.ts +4 -1
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +5 -2
- package/src/shade/renderer/extension/RENDER_EXTENSION_DESIGN.md +1 -2
- package/src/shade/renderer/geometry/meshlet/MeshletBatch.d.ts.map +1 -1
- package/src/shade/renderer/geometry/meshlet/MeshletBatch.js +3 -29
- package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts +22 -0
- package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.d.ts.map +1 -0
- package/src/shade/renderer/geometry/meshlet/encoding/ENCODED_ATTRIBUTE_DEFAULT_VALUES.js +49 -0
- package/src/shade/renderer/geometry/virtual/VGEO_FORMAT.md +4 -2
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_DESIGN.md +10 -8
- package/src/shade/renderer/geometry/virtual/VIRTUAL_GEOMETRY_PLAN.md +12 -11
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts +1 -1
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/VGeoBuildOptions.js +1 -1
- package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/gltf/gltf_open_document.js +5 -9
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts +32 -0
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevel.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts +59 -0
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelOptions.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts +44 -0
- package/src/shade/renderer/geometry/virtual/build/level/VGeoLevelSummary.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts +36 -0
- package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_levels.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_build_levels.js → level/vgeo_build_levels.js} +61 -95
- package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts +19 -0
- package/src/shade/renderer/geometry/virtual/build/level/vgeo_build_root_group.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_build_root_group.js → level/vgeo_build_root_group.js} +1 -1
- package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts +73 -0
- package/src/shade/renderer/geometry/virtual/build/mesh/VirtualGeometryMesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{VirtualGeometryMesh.js → mesh/VirtualGeometryMesh.js} +7 -7
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts +29 -0
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_normals.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_normals.js → mesh/vgeo_compute_vertex_normals.js} +3 -3
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_compute_vertex_tangents.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_compute_vertex_tangents.js → mesh/vgeo_compute_vertex_tangents.js} +5 -5
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts +46 -0
- package/src/shade/renderer/geometry/virtual/build/mesh/vgeo_normalize_source.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_normalize_source.js → mesh/vgeo_normalize_source.js} +9 -9
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts +13 -0
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_assemble_pages.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_assemble_pages.js → page/vgeo_assemble_pages.js} +9 -9
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts +13 -0
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_build_page_nodes.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_build_page_nodes.js → page/vgeo_build_page_nodes.js} +4 -4
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts +22 -0
- package/src/shade/renderer/geometry/virtual/build/page/vgeo_page_node_count.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_page_node_count.js → page/vgeo_page_node_count.js} +2 -2
- package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts +52 -0
- package/src/shade/renderer/geometry/virtual/build/partition/VGeoFaceGraph.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{VGeoFaceGraph.js → partition/VGeoFaceGraph.js} +1 -1
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts +14 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_graph_components.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts +29 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_graph.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_partition_graph.js → partition/vgeo_partition_graph.js} +2 -2
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts +41 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_partition_subgraph.js +92 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts +10 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set.js → partition/vgeo_split_face_set.js} +12 -55
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts +15 -0
- package/src/shade/renderer/geometry/virtual/build/partition/vgeo_split_face_set_packed.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_split_face_set_packed.js → partition/vgeo_split_face_set_packed.js} +3 -3
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts +12 -0
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_bounding_sphere.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_bounding_sphere.js → sphere/vgeo_bounding_sphere.js} +3 -3
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts +18 -0
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_containing_sphere.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_containing_sphere.js → sphere/vgeo_containing_sphere.js} +3 -3
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts +9 -0
- package/src/shade/renderer/geometry/virtual/build/sphere/vgeo_sphere_inflate_to_contain.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_sphere_inflate_to_contain.js → sphere/vgeo_sphere_inflate_to_contain.js} +1 -1
- package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts +24 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/VGeoGatheredFaces.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts +20 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/VGeoSubmesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts +14 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_extract_submesh.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_extract_submesh.js → submesh/vgeo_extract_submesh.js} +5 -5
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts +12 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_gather_faces.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_gather_faces.js → submesh/vgeo_gather_faces.js} +2 -2
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts +11 -0
- package/src/shade/renderer/geometry/virtual/build/submesh/vgeo_narrow_indices.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/build/{vgeo_narrow_indices.js → submesh/vgeo_narrow_indices.js} +3 -3
- package/src/shade/renderer/geometry/virtual/build/vgeo_build.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/build/vgeo_build.js +9 -11
- package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts +8 -2
- package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/attribute/vgeo_attributes_read.js +8 -2
- package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts +15 -0
- package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/format/page/VGEO_GROUP_OFFSET.js +15 -0
- package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts +28 -0
- package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/payload/vgeo_encode_cluster_payload.js +7 -3
- package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/read/vgeo_read_page.js +9 -4
- package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/vgeo_validate_container.js +18 -5
- package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/format/vgeo_write_container.js +9 -1
- package/src/shade/renderer/geometry/virtual/runtime/CUT_PLAN_2026_09_14.md +21 -2
- package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts +52 -2
- package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/GPUVirtualGeometryRuntime.js +97 -1
- package/src/shade/renderer/geometry/virtual/runtime/RUNTIME_PLAN.md +4 -4
- package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoFeedbackLoop.js +10 -0
- package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/VGeoResidency.js +59 -34
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts +8 -8
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_access.js +11 -88
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts +19 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_arena.js +98 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts +31 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_claim.js +99 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts +17 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.d.ts.map +1 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/chunk_vgeo_cut_queue.js +39 -0
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_reseed.js +7 -63
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_seed.js +13 -30
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.d.ts.map +1 -1
- package/src/shade/renderer/geometry/virtual/runtime/cut/shader_vgeo_cut_traverse.js +6 -52
- package/src/shade/renderer/loader/gltf/load_gltf.d.ts.map +1 -1
- package/src/shade/renderer/loader/gltf/load_gltf.js +0 -19
- package/src/shade/renderer/material/chunk_material_draw_side.d.ts +14 -0
- package/src/shade/renderer/material/chunk_material_draw_side.d.ts.map +1 -0
- package/src/shade/renderer/material/chunk_material_draw_side.js +34 -0
- package/src/shade/renderer/material/standard/fragment_gbuffer.d.ts.map +1 -1
- package/src/shade/renderer/material/standard/fragment_gbuffer.js +8 -1
- package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.d.ts.map +1 -1
- package/src/shade/renderer/path_tracer/shader/chunk_sample_material_data.js +13 -1
- package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts +19 -5
- package/src/shade/renderer/rasterize/bucket/construct_primitive_state.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/construct_primitive_state.js +23 -5
- package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/bucket/filter_materials_by_bucket.js +12 -5
- package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts +22 -0
- package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.d.ts.map +1 -0
- package/src/shade/renderer/rasterize/bucket/rasterization_opaque_buckets.js +36 -0
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts +15 -0
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.d.ts.map +1 -0
- package/src/shade/renderer/rasterize/chunk_triangle_winding_is_reversed.js +35 -0
- package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts +10 -1
- package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/RASTERIZATION_BUCKET_LIMIT.js +38 -29
- package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts +5 -0
- package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/bucket/chunk_rasterization_material_bucket.js +63 -55
- package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/expand/graph_meshlets_rasterize.js +1 -0
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/fast/graph_rasterize_scene_fast.js +44 -53
- package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_brick4.js +6 -2
- package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/forward/chunk_forward_shade_standard_fragment_ibl.js +6 -2
- package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/forward/vertex_shader_chunk.js +19 -2
- package/src/shade/renderer/rasterize/native/avboit/AVBOIT_PLAN_2026_09_03.md +7 -4
- package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/avboit/graph_rasterize_meshes_transparent_avboit.js +3 -2
- package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/chunk_rasterize_triangle.js +15 -4
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_alpha_tested_pass_descriptor.js +16 -5
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/depth/shadow_rasterization_pass_descriptor.js +16 -5
- package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/oit/graph_rasterize_meshes_transparent_oit.js +2 -1
- package/src/shade/renderer/rasterize/native/rasterization_shader_resources.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/rasterization_shader_resources.js +4 -0
- package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/native/viz/viz_rasterization_alpha_tested_pass_descriptor.js +16 -7
- package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_partial_opaque.js +78 -88
- package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.d.ts.map +1 -1
- package/src/shade/renderer/rasterize/standard/graph_rasterize_remaining_opaque.js +21 -31
- package/src/shade/renderer/scene/GPUSceneContext.d.ts.map +1 -1
- package/src/shade/renderer/scene/GPUSceneContext.js +2 -0
- package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts +19 -0
- package/src/shade/renderer/scene/NODE3D_STRUCT.d.ts.map +1 -1
- package/src/shade/renderer/scene/NODE3D_STRUCT.js +34 -0
- package/src/shade/renderer/scene/chunk_node3d_flags.d.ts +9 -0
- package/src/shade/renderer/scene/chunk_node3d_flags.d.ts.map +1 -0
- package/src/shade/renderer/scene/chunk_node3d_flags.js +23 -0
- package/src/shade/renderer/scene/chunk_scene_database_access.d.ts.map +1 -1
- package/src/shade/renderer/scene/chunk_scene_database_access.js +15 -1
- package/src/shade/renderer/scene/rows/GPUSceneRows.d.ts.map +1 -1
- package/src/shade/renderer/scene/rows/GPUSceneRows.js +9 -1
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts +7 -0
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_compute_normal_matrix_from_m4.js +16 -4
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts +12 -0
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/geometry/mat4/chunk_mat4_linear_determinant.js +18 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts +24 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.d.ts.map +1 -0
- package/src/shade/renderer/shader/chunk/geometry/triangle/chunk_triangle_list_reverse_winding.js +33 -0
- package/src/shade/playground/vgeo_viewer/README.md +0 -150
- package/src/shade/playground/vgeo_viewer/cut_geometry.js +0 -460
- package/src/shade/playground/vgeo_viewer/index.html +0 -77
- package/src/shade/playground/vgeo_viewer/main.js +0 -1343
- package/src/shade/playground/vgeo_viewer/select_cut.js +0 -481
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts +0 -258
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyManager.d.ts.map +0 -1
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts +0 -49
- package/src/shade/renderer/geometry/virtual/residency/VGeoResidencyOptions.d.ts.map +0 -1
- package/src/shade/renderer/loader/gltf/fix_up_material_sides.js +0 -109
- package/src/shade/renderer/rasterize/expand/graph_triangles_filter_by_material_side.js +0 -78
- package/src/shade/renderer/rasterize/expand/shader_filter_triangle_by_material_side.js +0 -100
- package/src/shade/renderer/rasterize/forward/chunk_material_side_should_draw.js +0 -26
- package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts +0 -3
- package/src/shade/renderer/shader/chunk/jobs/chunk_job_system.d.ts.map +0 -1
- /package/src/shade/renderer/geometry/virtual/build/{VGeoLevel.js → level/VGeoLevel.js} +0 -0
- /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelOptions.js → level/VGeoLevelOptions.js} +0 -0
- /package/src/shade/renderer/geometry/virtual/build/{VGeoLevelSummary.js → level/VGeoLevelSummary.js} +0 -0
- /package/src/shade/renderer/geometry/virtual/build/{vgeo_graph_components.js → partition/vgeo_graph_components.js} +0 -0
- /package/src/shade/renderer/geometry/virtual/build/{VGeoGatheredFaces.js → submesh/VGeoGatheredFaces.js} +0 -0
- /package/src/shade/renderer/geometry/virtual/build/{VGeoSubmesh.js → submesh/VGeoSubmesh.js} +0 -0
|
@@ -0,0 +1,1716 @@
|
|
|
1
|
+
import { Color } from "../../../core/color/Color.js";
|
|
2
|
+
import { m4_linear_determinant } from "../../../core/geom/3d/mat4/m4_linear_determinant.js";
|
|
3
|
+
import Vector3 from "../../../core/geom/Vector3.js";
|
|
4
|
+
import { EntityComponentDataset } from "../../../engine/ecs/EntityComponentDataset.js";
|
|
5
|
+
import { Transform64 } from "../../../engine/ecs/transform/Transform64.js";
|
|
6
|
+
import { OrbitalCameraController } from "../../renderer/camera/OrbitalCameraController.js";
|
|
7
|
+
import { PerspectiveCamera } from "../../renderer/camera/PerspectiveCamera.js";
|
|
8
|
+
import { FramePhase } from "../../renderer/extension/FramePhase.js";
|
|
9
|
+
import { RenderExtension } from "../../renderer/extension/RenderExtension.js";
|
|
10
|
+
import {
|
|
11
|
+
vgeo_byte_source_over_url
|
|
12
|
+
} from "../../renderer/geometry/virtual/format/read/vgeo_fetch_byte_source.js";
|
|
13
|
+
import {
|
|
14
|
+
VGeoContainerReader
|
|
15
|
+
} from "../../renderer/geometry/virtual/format/read/VGeoContainerReader.js";
|
|
16
|
+
import {
|
|
17
|
+
VGeoReadOptions
|
|
18
|
+
} from "../../renderer/geometry/virtual/format/read/VGeoReadOptions.js";
|
|
19
|
+
import { VGEO_CUT_STAT } from "../../renderer/geometry/virtual/runtime/layout/VGEO_CUT_STAT.js";
|
|
20
|
+
import {
|
|
21
|
+
VGeoResidencyOptions
|
|
22
|
+
} from "../../renderer/geometry/virtual/runtime/VGeoResidencyOptions.js";
|
|
23
|
+
import { VirtualGeometry } from "../../renderer/geometry/virtual/runtime/VirtualGeometry.js";
|
|
24
|
+
import { make_sunlight } from "../../renderer/light/make_sunlight.js";
|
|
25
|
+
import { place_light } from "../../renderer/light/place_light.js";
|
|
26
|
+
import { ShadeDrawSide } from "../../renderer/material/ShadeDrawSide.js";
|
|
27
|
+
import { StandardShadeMaterial } from "../../renderer/material/StandardShadeMaterial.js";
|
|
28
|
+
import {
|
|
29
|
+
RasterizationCollectionType
|
|
30
|
+
} from "../../renderer/rasterize/expand/RasterizationCollectionType.js";
|
|
31
|
+
import { Renderer } from "../../renderer/Renderer.js";
|
|
32
|
+
import { place_primitive } from "../../renderer/scene/rows/place_primitive.js";
|
|
33
|
+
import { Scene } from "../../renderer/scene/Scene.js";
|
|
34
|
+
import { load_default_environment } from "../load_default_environment.js";
|
|
35
|
+
import { install_profile_hotkey } from "../profile_hotkey.js";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A **whole converted scene** of virtual geometry: thousands of containers, tens of thousands of
|
|
39
|
+
* instances, one cut.
|
|
40
|
+
*
|
|
41
|
+
* `playground/vgeo_runtime/` draws one `.vgeo` and is the page that proves the engine seam. This is
|
|
42
|
+
* the other question that seam raises and it cannot answer: a container placed once by a page that
|
|
43
|
+
* frames the camera on its own bounding sphere is a *viewer*, and design §1's claim is about a
|
|
44
|
+
* **scene** — that rendering cost is a function of the threshold and the resolution rather than of
|
|
45
|
+
* the source, across every asset in view at once, with instances sharing pages and one residency
|
|
46
|
+
* budget deciding between all of them.
|
|
47
|
+
*
|
|
48
|
+
* So everything here that is not in the runtime page is about breadth rather than depth:
|
|
49
|
+
*
|
|
50
|
+
* - **One residency, N containers.** Every asset registers with the same `VGeoResidency`, whose
|
|
51
|
+
* budgets are scene-wide by construction — the RAM tier's bytes and the VRAM tier's slots are
|
|
52
|
+
* counted across every reader, and a page evicted from one asset is room for another's. That is
|
|
53
|
+
* the thing a single-asset page cannot show, because with one reader there is nothing to
|
|
54
|
+
* arbitrate between.
|
|
55
|
+
* - **One cut, N instances.** `shader_vgeo_cut_collect` runs over the visible mesh rows and emits
|
|
56
|
+
* one instance record per virtual one; the descent is seeded with one item per instance and every
|
|
57
|
+
* cull and error test downstream runs in that instance's own object space. Seventeen thousand
|
|
58
|
+
* instances are seventeen thousand seeds of one wavefront, not seventeen thousand cuts.
|
|
59
|
+
* - **Instances share the asset.** A geometry placed by four hundred nodes is one reader, one
|
|
60
|
+
* geometry row, one page lookup and one set of resident pages. That is what `VirtualGeometry`'s
|
|
61
|
+
* `equals` is for, and it is most of why this is affordable at all.
|
|
62
|
+
*
|
|
63
|
+
* What it takes as input is a **scene manifest** — `tools/geometry/virtual/vgeo_scene.js` writes
|
|
64
|
+
* one out of a builder manifest — naming each container and the matrices it is placed at. The
|
|
65
|
+
* containers are served by ranges: nothing is downloaded, each is opened out of its own head.
|
|
66
|
+
*
|
|
67
|
+
* `__state()` is the whole surface a headless session needs, and the panel shows nothing it does
|
|
68
|
+
* not.
|
|
69
|
+
*
|
|
70
|
+
* @author Alex Goldring
|
|
71
|
+
* @copyright Company Named Limited (c) 2026
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
const element = id => document.getElementById(id);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Where the containers are served from when the page is opened without a `?scene=`.
|
|
78
|
+
*
|
|
79
|
+
* `tools/geometry/virtual/vgeo_serve.js` binds this by default and answers `/scenes.json` with what
|
|
80
|
+
* it holds, so a page with no parameters finds the scene that is there rather than asking for a URL
|
|
81
|
+
* to be pasted. It is a *separate* origin on purpose: vite refuses `/@fs/` outside the workspace,
|
|
82
|
+
* and a forty-gigabyte directory has no business inside one.
|
|
83
|
+
*
|
|
84
|
+
* @type {string}
|
|
85
|
+
*/
|
|
86
|
+
const DEFAULT_ORIGIN = 'http://localhost:5200';
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Head bytes read on the first request of every container.
|
|
90
|
+
*
|
|
91
|
+
* §10's always-resident set is the header, the level table and the root frame, contiguous and in
|
|
92
|
+
* that order, and a reader that guesses short pays a second round trip to grow. The default is
|
|
93
|
+
* 64 KB, which is right for a page that opens one container and wasteful for one that opens three
|
|
94
|
+
* thousand: measured across Zorah's 2,966, the resident head is 2,560 bytes at the median and
|
|
95
|
+
* 6,528 at the very worst, so 8 KB opens **every** container in the set in exactly one range
|
|
96
|
+
* request and reads 7.4 MB in total rather than 190 MB.
|
|
97
|
+
*
|
|
98
|
+
* A container whose head is larger than this is not refused — `#grow_head` fetches the rest — so
|
|
99
|
+
* the number is a throughput choice and not a limit.
|
|
100
|
+
*
|
|
101
|
+
* @type {number}
|
|
102
|
+
*/
|
|
103
|
+
const HEAD_PREFIX_BYTES = 8 * 1024;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Containers being opened at once.
|
|
107
|
+
*
|
|
108
|
+
* A cold scene is one range request per container and they are independent, so the only thing this
|
|
109
|
+
* trades is head-of-line latency against the browser's own per-origin connection pool. Beyond about
|
|
110
|
+
* the pool's width the requests queue in the browser instead of on the wire and the number stops
|
|
111
|
+
* meaning anything.
|
|
112
|
+
*
|
|
113
|
+
* @type {number}
|
|
114
|
+
*/
|
|
115
|
+
const OPEN_CONCURRENCY = 24;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What `?side=` accepts, for a session asking whether a surface is missing or merely culled.
|
|
119
|
+
*
|
|
120
|
+
* @type {Readonly<Record<string, number>>}
|
|
121
|
+
*/
|
|
122
|
+
const DRAW_SIDES = Object.freeze({
|
|
123
|
+
front: ShadeDrawSide.Front,
|
|
124
|
+
back: ShadeDrawSide.Back,
|
|
125
|
+
double: ShadeDrawSide.Double,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Opens the VG frame before anything records a rasterization pass, because the seam inside those
|
|
130
|
+
* passes writes into the feedback buffer this creates.
|
|
131
|
+
*/
|
|
132
|
+
class VGeoFrameStart extends RenderExtension {
|
|
133
|
+
name = "vgeo/frame start";
|
|
134
|
+
|
|
135
|
+
phase = FramePhase.FrameStart;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @param {VGeoScenePlayground} page
|
|
139
|
+
*/
|
|
140
|
+
constructor(page) {
|
|
141
|
+
super();
|
|
142
|
+
|
|
143
|
+
this.page = page;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
record(frame) {
|
|
147
|
+
this.page.runtime.begin_frame({ graph: frame.graph });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Schedules the readback and closes the frame, after everything that could have cut has cut.
|
|
153
|
+
*/
|
|
154
|
+
class VGeoFrameEnd extends RenderExtension {
|
|
155
|
+
name = "vgeo/frame end";
|
|
156
|
+
|
|
157
|
+
phase = FramePhase.BeforePresent;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {VGeoScenePlayground} page
|
|
161
|
+
*/
|
|
162
|
+
constructor(page) {
|
|
163
|
+
super();
|
|
164
|
+
|
|
165
|
+
this.page = page;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
record(frame) {
|
|
169
|
+
this.page.feedback.read(frame.graph);
|
|
170
|
+
|
|
171
|
+
this.page.runtime.end_frame();
|
|
172
|
+
|
|
173
|
+
this.page.frames++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
class VGeoScenePlayground {
|
|
178
|
+
/**
|
|
179
|
+
* @type {Renderer}
|
|
180
|
+
*/
|
|
181
|
+
renderer;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @type {HTMLCanvasElement}
|
|
185
|
+
*/
|
|
186
|
+
canvas;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @type {Scene}
|
|
190
|
+
*/
|
|
191
|
+
scene;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @type {GPUSceneContext}
|
|
195
|
+
*/
|
|
196
|
+
scene_context;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @type {PerspectiveCamera}
|
|
200
|
+
*/
|
|
201
|
+
camera;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @type {OrbitalCameraController}
|
|
205
|
+
*/
|
|
206
|
+
controller;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The one material the whole scene is drawn with.
|
|
210
|
+
*
|
|
211
|
+
* Not a simplification this page is making: a `.vgeo` carries **geometry only** — design §7 —
|
|
212
|
+
* so a converted set has no material data in it at all, and the source's 2,714 materials did
|
|
213
|
+
* not survive the conversion because nothing asked them to. Giving each container a tinted
|
|
214
|
+
* material of its own would read better and would be a lie about where the colour came from,
|
|
215
|
+
* and it would also put 2,714 material buckets in front of the draw path to say it with.
|
|
216
|
+
*
|
|
217
|
+
* @type {StandardShadeMaterial}
|
|
218
|
+
*/
|
|
219
|
+
material;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* What a **reflected** instance is drawn with — one whose transform has a negative determinant,
|
|
223
|
+
* which in Zorah is 2,722 of 17,424.
|
|
224
|
+
*
|
|
225
|
+
* By default this is {@link material}, and the scene is one bucket. `?mirrored=` splits it, and
|
|
226
|
+
* the split is the only way to ask the question at all: a reflection reverses the screen-space
|
|
227
|
+
* winding of every triangle of the instance, `cullMode` is pipeline state chosen per material
|
|
228
|
+
* bucket, and nothing between `construct_primitive_state` and the rasterizer sees a transform.
|
|
229
|
+
* So the sign of the determinant cannot reach the cull unless the instance is put in a
|
|
230
|
+
* different bucket, and putting it in one is both the diagnosis and, for a page, the fix.
|
|
231
|
+
*
|
|
232
|
+
* @type {StandardShadeMaterial}
|
|
233
|
+
*/
|
|
234
|
+
mirror_material;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Instances placed with a negative-determinant transform.
|
|
238
|
+
* @type {number}
|
|
239
|
+
*/
|
|
240
|
+
mirrored = 0;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The scene manifest as loaded, or `undefined` before it arrives.
|
|
244
|
+
* @type {object|undefined}
|
|
245
|
+
*/
|
|
246
|
+
manifest;
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Where the manifest was loaded from, which every container's URL is resolved against.
|
|
250
|
+
* @type {string}
|
|
251
|
+
*/
|
|
252
|
+
base = '';
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* One entry per container that opened, held so nothing collects the reader out from under the
|
|
256
|
+
* residency and so a session can be told what is in the scene.
|
|
257
|
+
* @type {Array<{reader: VGeoContainerReader, geometry: VirtualGeometry, file: string, instances: number}>}
|
|
258
|
+
*/
|
|
259
|
+
assets = [];
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @type {GPUVirtualGeometryRuntime}
|
|
263
|
+
*/
|
|
264
|
+
runtime;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @type {VGeoFeedbackLoop}
|
|
268
|
+
*/
|
|
269
|
+
feedback;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @type {VGeoResidency}
|
|
273
|
+
*/
|
|
274
|
+
residency;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @type {number}
|
|
278
|
+
*/
|
|
279
|
+
frames = 0;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Containers opened, refused, and how many the manifest names.
|
|
283
|
+
* @type {number}
|
|
284
|
+
*/
|
|
285
|
+
opened = 0;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @type {number}
|
|
289
|
+
*/
|
|
290
|
+
refused = 0;
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @type {number}
|
|
294
|
+
*/
|
|
295
|
+
placed = 0;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* @type {number}
|
|
299
|
+
*/
|
|
300
|
+
total_containers = 0;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Wall-clock milliseconds the whole scene took to open, or 0 while it is still opening.
|
|
304
|
+
* @type {number}
|
|
305
|
+
*/
|
|
306
|
+
open_ms = 0;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @type {number}
|
|
310
|
+
*/
|
|
311
|
+
screen_height = 1;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @type {number}
|
|
315
|
+
*/
|
|
316
|
+
screen_width = 1;
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Frame time, exponentially smoothed — a scene page's one reading that is a cost rather than a
|
|
320
|
+
* count, and coarse enough to be honest about being one. A capture is what measures.
|
|
321
|
+
* @type {number}
|
|
322
|
+
*/
|
|
323
|
+
frame_ms = 0;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* @type {string}
|
|
327
|
+
*/
|
|
328
|
+
status_text = '';
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* The dropout catcher, when `?catch=` armed one.
|
|
332
|
+
* @type {FlickerCatcher|undefined}
|
|
333
|
+
*/
|
|
334
|
+
catcher;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Named viewpoints, so a session that cannot drag has somewhere to stand and a reader of the
|
|
338
|
+
* panel can get back to a reading. Seeded from the manifest's own camera.
|
|
339
|
+
* @type {Array<{name: string, eye: number[], target: number[]}>}
|
|
340
|
+
*/
|
|
341
|
+
viewpoints = [];
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @param {string} text
|
|
345
|
+
* @param {boolean} [error]
|
|
346
|
+
*/
|
|
347
|
+
status(text, error = false) {
|
|
348
|
+
this.status_text = text;
|
|
349
|
+
|
|
350
|
+
const node = element('status');
|
|
351
|
+
|
|
352
|
+
node.textContent = text;
|
|
353
|
+
node.classList.toggle('error', error);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Bring the canvas, the renderer and the camera up to the size the window actually is.
|
|
358
|
+
*
|
|
359
|
+
* **Measured from the document, never from the canvas.** `Renderer` writes back both of the
|
|
360
|
+
* canvas's sizes when it configures the context, including an *inline* `style.width` in CSS
|
|
361
|
+
* pixels — so a `fit` that read `clientWidth` back would be reading the renderer its own last
|
|
362
|
+
* answer and the canvas would never change size again.
|
|
363
|
+
*
|
|
364
|
+
* The cut's screen height and field of view follow from the renderer rather than from the
|
|
365
|
+
* window because τ is a count of **render-target** pixels: `GPUViewCut.set_projection` takes
|
|
366
|
+
* them off the view every frame, so nothing here has to push them.
|
|
367
|
+
*/
|
|
368
|
+
fit() {
|
|
369
|
+
const renderer = this.renderer;
|
|
370
|
+
|
|
371
|
+
const viewport = document.documentElement;
|
|
372
|
+
|
|
373
|
+
renderer.pixel_ratio = globalThis.devicePixelRatio || 1;
|
|
374
|
+
|
|
375
|
+
renderer.resize(
|
|
376
|
+
Math.max(1, Math.floor(viewport.clientWidth)),
|
|
377
|
+
Math.max(1, Math.floor(viewport.clientHeight))
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
this.camera.aspect = renderer.aspect_ratio;
|
|
381
|
+
|
|
382
|
+
const resolution = renderer.output_resolution;
|
|
383
|
+
const scale = renderer.internal_resolution_scale;
|
|
384
|
+
|
|
385
|
+
this.screen_width = Math.max(1, Math.floor(resolution.x * scale));
|
|
386
|
+
this.screen_height = Math.max(1, Math.floor(resolution.y * scale));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Open every container the manifest names and place its instances, as each one lands.
|
|
391
|
+
*
|
|
392
|
+
* **Placed as they arrive rather than at the end**, and that is the whole difference between a
|
|
393
|
+
* page you can look at in a second and one you wait a minute for. It costs nothing: a geometry
|
|
394
|
+
* that is not registered yet is a geometry the cut does not collect, and the residency's budgets
|
|
395
|
+
* are recomputed per reading rather than at registration, so a scene that grows under the camera
|
|
396
|
+
* is the case both tiers were designed for.
|
|
397
|
+
*
|
|
398
|
+
* A container that refuses — the wrong page size, a tier with no room for another root — leaves
|
|
399
|
+
* every other asset exactly as it was and is counted. Refusing one container out of three
|
|
400
|
+
* thousand is a hole in the scene, and a hole is what the count is for.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} manifest
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async open_scene(manifest) {
|
|
406
|
+
const geometries = manifest.geometries;
|
|
407
|
+
|
|
408
|
+
this.total_containers = geometries.length;
|
|
409
|
+
|
|
410
|
+
const options = new VGeoReadOptions();
|
|
411
|
+
|
|
412
|
+
/*
|
|
413
|
+
Frame verification on, for the same reason the runtime page has it on: these are bytes off
|
|
414
|
+
a network rather than bytes a tool just wrote, and §10 leaves it off by default precisely
|
|
415
|
+
because a builder reading back its own output is not defending against one.
|
|
416
|
+
*/
|
|
417
|
+
options.verify_frames = true;
|
|
418
|
+
|
|
419
|
+
// the runtime answers activation out of the arena; the reader's own counters would be
|
|
420
|
+
// tens of megabytes at seventeen thousand pages, read by nothing
|
|
421
|
+
options.track_selectability = false;
|
|
422
|
+
|
|
423
|
+
options.head_prefix_bytes = HEAD_PREFIX_BYTES;
|
|
424
|
+
|
|
425
|
+
const started = performance.now();
|
|
426
|
+
|
|
427
|
+
let next = 0;
|
|
428
|
+
|
|
429
|
+
const worker = async () => {
|
|
430
|
+
while (next < geometries.length) {
|
|
431
|
+
const record = geometries[next++];
|
|
432
|
+
|
|
433
|
+
await this.open_one(record, options);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const workers = [];
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < Math.min(OPEN_CONCURRENCY, geometries.length); i++) {
|
|
440
|
+
workers.push(worker());
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await Promise.all(workers);
|
|
444
|
+
|
|
445
|
+
this.open_ms = performance.now() - started;
|
|
446
|
+
|
|
447
|
+
this.status(
|
|
448
|
+
`${this.opened.toLocaleString()} containers`
|
|
449
|
+
+ `, ${this.placed.toLocaleString()} instances`
|
|
450
|
+
+ `, opened in ${(this.open_ms / 1000).toFixed(1)} s`
|
|
451
|
+
+ (this.refused > 0 ? ` — ${this.refused} refused` : '')
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @param {object} record one manifest geometry
|
|
457
|
+
* @param {VGeoReadOptions} options
|
|
458
|
+
* @returns {Promise<void>}
|
|
459
|
+
*/
|
|
460
|
+
async open_one(record, options) {
|
|
461
|
+
const url = new URL(record.file, this.base).href;
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
/*
|
|
465
|
+
The length comes from the manifest rather than from a `HEAD`. `vgeo_fetch_byte_source`
|
|
466
|
+
probes for it because a URL is usually all a caller has; here the builder wrote it down
|
|
467
|
+
beside the file name, and taking it saves 2,966 round trips that would tell this page
|
|
468
|
+
something it was already holding.
|
|
469
|
+
*/
|
|
470
|
+
const source = vgeo_byte_source_over_url(url, record.bytes);
|
|
471
|
+
|
|
472
|
+
const reader = await VGeoContainerReader.open(source, options);
|
|
473
|
+
|
|
474
|
+
/*
|
|
475
|
+
Only the root page is resident. Everything below it arrives because the cut asked for
|
|
476
|
+
it, which at this scale is the whole point: the manifest names 46.7 GiB of geometry and
|
|
477
|
+
what is fetched is what a threshold in pixels selected out of it.
|
|
478
|
+
*/
|
|
479
|
+
const geometry_row = this.residency.register(reader);
|
|
480
|
+
|
|
481
|
+
const geometry = new VirtualGeometry(reader);
|
|
482
|
+
|
|
483
|
+
geometry.vg_row = geometry_row;
|
|
484
|
+
|
|
485
|
+
const matrices = record.instances;
|
|
486
|
+
|
|
487
|
+
const transform = new Transform64();
|
|
488
|
+
|
|
489
|
+
for (let i = 0; i < matrices.length; i += 16) {
|
|
490
|
+
/*
|
|
491
|
+
The source matrix, decomposed. A transform row carries a TRS — the GPU composes the
|
|
492
|
+
world from it — so an instance carrying **shear** cannot be represented exactly and
|
|
493
|
+
is placed at the closest rigid-plus-scale fit instead. 391 of Zorah's 17,424 have
|
|
494
|
+
any, and the worst is about eleven degrees off orthogonal; the other 17,033 are
|
|
495
|
+
exact. That is a property of the engine's transform model rather than of this page,
|
|
496
|
+
and it is worth knowing when a wall looks subtly wrong.
|
|
497
|
+
*/
|
|
498
|
+
const matrix = matrices.slice(i, i + 16);
|
|
499
|
+
|
|
500
|
+
transform.fromMatrix(matrix);
|
|
501
|
+
|
|
502
|
+
/*
|
|
503
|
+
The engine reverses a reflected instance's winding in the vertex shader, off the
|
|
504
|
+
row's own `NODE3D_GLOBAL_FLAG_MIRRORED`, so nothing here has to be done for the
|
|
505
|
+
picture to be right. Splitting the bucket is what `?mirrored=` is for: a
|
|
506
|
+
diagnostic that can tint the reflected half, or reproduce the old bug by reversing
|
|
507
|
+
it a second time. See the README.
|
|
508
|
+
*/
|
|
509
|
+
const reflected = m4_linear_determinant(matrix) < 0;
|
|
510
|
+
|
|
511
|
+
if (reflected) {
|
|
512
|
+
this.mirrored++;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
place_primitive(
|
|
516
|
+
this.scene_context, geometry,
|
|
517
|
+
reflected ? this.mirror_material : this.material,
|
|
518
|
+
transform
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
this.placed++;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this.assets.push({
|
|
525
|
+
reader,
|
|
526
|
+
geometry,
|
|
527
|
+
file: record.file,
|
|
528
|
+
instances: matrices.length / 16,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
this.opened++;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
this.refused++;
|
|
534
|
+
|
|
535
|
+
// one line per refusal rather than a thrown scene: 2,965 of 2,966 is a scene
|
|
536
|
+
console.warn(`${record.file} — ${error.message ?? error}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Stand the camera somewhere, in world units.
|
|
542
|
+
*
|
|
543
|
+
* Everything with a length in it is taken from the distance between the two points rather than
|
|
544
|
+
* from the scene's extent, which is the difference between a camera that works in a town and
|
|
545
|
+
* one that works in a hundred-kilometre bounding box: a near plane derived from Zorah's world
|
|
546
|
+
* radius is fifty metres, and fifty metres puts every building the viewer is standing in behind
|
|
547
|
+
* it.
|
|
548
|
+
*
|
|
549
|
+
* @param {number[]} eye
|
|
550
|
+
* @param {number[]} target
|
|
551
|
+
*/
|
|
552
|
+
look(eye, target) {
|
|
553
|
+
const from = new Vector3(eye[0], eye[1], eye[2]);
|
|
554
|
+
const to = new Vector3(target[0], target[1], target[2]);
|
|
555
|
+
|
|
556
|
+
const distance = Math.max(Vector3.distance(from, to), 1e-3);
|
|
557
|
+
|
|
558
|
+
this.camera.near = distance * 0.002;
|
|
559
|
+
this.camera.far = Math.max(distance * 2000, this.world_radius * 4);
|
|
560
|
+
|
|
561
|
+
this.controller.distanceLimits.min = distance * 0.01;
|
|
562
|
+
this.controller.distanceLimits.max = Math.max(distance * 2000, this.world_radius * 4);
|
|
563
|
+
this.controller.movement_speed_scale = distance;
|
|
564
|
+
|
|
565
|
+
this.controller.look(from, to);
|
|
566
|
+
|
|
567
|
+
// the pixels the camera is about to cover have no correspondence in a TAA history written
|
|
568
|
+
// from where it was standing
|
|
569
|
+
this.renderer.indicate_view_change();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Half the diagonal of every instance's world box, which is the only length the manifest offers
|
|
574
|
+
* that is about the scene rather than about a viewpoint.
|
|
575
|
+
* @type {number}
|
|
576
|
+
*/
|
|
577
|
+
world_radius = 1;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Everything a headless session needs, and everything the panel shows.
|
|
581
|
+
*
|
|
582
|
+
* @returns {object}
|
|
583
|
+
*/
|
|
584
|
+
state() {
|
|
585
|
+
const stats = this.feedback.stats;
|
|
586
|
+
const pool = this.runtime.pool;
|
|
587
|
+
|
|
588
|
+
let bytes_read = this.residency.fetcher.worker_bytes_read;
|
|
589
|
+
|
|
590
|
+
for (const asset of this.assets) {
|
|
591
|
+
bytes_read += asset.reader.source.bytes_read;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
frames: this.frames,
|
|
596
|
+
frame_ms: this.frame_ms,
|
|
597
|
+
tau: this.runtime.tau,
|
|
598
|
+
status: this.status_text,
|
|
599
|
+
screen_width: this.screen_width,
|
|
600
|
+
screen_height: this.screen_height,
|
|
601
|
+
feedback_reads: this.feedback.read_count,
|
|
602
|
+
feedback_skipped: this.feedback.skipped_frames,
|
|
603
|
+
camera: {
|
|
604
|
+
eye: Array.from(this.camera.transform.translation),
|
|
605
|
+
target: this.controller.target.toJSON(),
|
|
606
|
+
distance: this.controller.distance,
|
|
607
|
+
},
|
|
608
|
+
scene: {
|
|
609
|
+
name: this.manifest?.source ?? '',
|
|
610
|
+
containers: this.opened,
|
|
611
|
+
declared: this.total_containers,
|
|
612
|
+
refused: this.refused,
|
|
613
|
+
instances: this.placed,
|
|
614
|
+
mirrored: this.mirrored,
|
|
615
|
+
faces: this.manifest === undefined ? 0 : scene_faces(this.manifest),
|
|
616
|
+
bytes: this.manifest?.total_bytes ?? 0,
|
|
617
|
+
bytes_read,
|
|
618
|
+
pages: this.manifest?.total_pages ?? 0,
|
|
619
|
+
open_ms: this.open_ms,
|
|
620
|
+
},
|
|
621
|
+
cut: {
|
|
622
|
+
drawn: stats[VGEO_CUT_STAT.DRAWN],
|
|
623
|
+
clamped: stats[VGEO_CUT_STAT.CLAMPED],
|
|
624
|
+
clamped_on_activation: stats[VGEO_CUT_STAT.CLAMPED_ON_ACTIVATION],
|
|
625
|
+
pages: stats[VGEO_CUT_STAT.PAGES],
|
|
626
|
+
items: stats[VGEO_CUT_STAT.ITEMS],
|
|
627
|
+
clusters_tested: stats[VGEO_CUT_STAT.CLUSTERS_TESTED],
|
|
628
|
+
culled_frustum: stats[VGEO_CUT_STAT.CULLED_FRUSTUM],
|
|
629
|
+
culled_cluster: stats[VGEO_CUT_STAT.CULLED_CLUSTER],
|
|
630
|
+
culled_lod: stats[VGEO_CUT_STAT.CULLED_LOD],
|
|
631
|
+
meshlets_dropped: stats[VGEO_CUT_STAT.MESHLETS_DROPPED],
|
|
632
|
+
wants_dropped: stats[VGEO_CUT_STAT.WANTS_DROPPED],
|
|
633
|
+
queue_dropped: stats[VGEO_CUT_STAT.QUEUE_DROPPED],
|
|
634
|
+
instances_dropped: stats[VGEO_CUT_STAT.INSTANCES_DROPPED],
|
|
635
|
+
wavefronts: stats[VGEO_CUT_STAT.WAVEFRONTS],
|
|
636
|
+
queue_remaining: stats[VGEO_CUT_STAT.QUEUE_REMAINING],
|
|
637
|
+
/*
|
|
638
|
+
The bound beside the use of it, because one is meaningless without the other: eight
|
|
639
|
+
wavefronts is a drained descent under a bound of 32 and a truncated one under a
|
|
640
|
+
bound of 8, and the panel could not tell them apart while it showed only the count.
|
|
641
|
+
*/
|
|
642
|
+
max_iterations: this.runtime.max_iterations,
|
|
643
|
+
iteration_escalations: this.runtime.iteration_escalations,
|
|
644
|
+
},
|
|
645
|
+
wants: this.feedback.last_want_count,
|
|
646
|
+
touches: this.feedback.last_touch_count,
|
|
647
|
+
vram: {
|
|
648
|
+
pages: pool.resident_page_count,
|
|
649
|
+
bytes: pool.resident_bytes,
|
|
650
|
+
capacity: pool.tables.page_capacity,
|
|
651
|
+
evicted: this.residency.evicted_vram_count,
|
|
652
|
+
refused: this.residency.refused_install_count,
|
|
653
|
+
},
|
|
654
|
+
ram: {
|
|
655
|
+
pages: this.residency.ram_page_count,
|
|
656
|
+
bytes: this.residency.ram_resident_bytes,
|
|
657
|
+
evicted: this.residency.evicted_ram_count,
|
|
658
|
+
refused: this.residency.refused_fetch_count,
|
|
659
|
+
},
|
|
660
|
+
fetches_in_flight: this.residency.fetches_in_flight,
|
|
661
|
+
worker_fetches: this.residency.fetcher.worker_fetch_count,
|
|
662
|
+
in_process_fetches: this.residency.fetcher.in_process_fetch_count,
|
|
663
|
+
deferred: this.residency.deferred_want_count,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Catches a frame that is **not like its neighbours** and keeps it.
|
|
670
|
+
*
|
|
671
|
+
* A cut re-selected every frame has a shimmer floor set by TAA (see the README), and that is a
|
|
672
|
+
* property of the content. A *dropout* — a single frame missing a large part of what the frames
|
|
673
|
+
* either side of it drew — is not, and the two are nothing alike to measure even though both read
|
|
674
|
+
* as "flicker": the first moves a tenth of a per cent of the pixels, the second moves tens of per
|
|
675
|
+
* cent. This looks only for the second.
|
|
676
|
+
*
|
|
677
|
+
* **It has to sample the canvas, not the counters.** `__state()`'s cut statistics come from the
|
|
678
|
+
* feedback readback, which is capped at two in flight and is skipped on more than half the frames —
|
|
679
|
+
* so on any given frame `drawn` is a reading from one to three frames ago, and a one-frame
|
|
680
|
+
* disappearance need never appear in it at all. The pixels are the only per-frame truth available,
|
|
681
|
+
* and `drawImage` into a 2D canvas is the only way to get at them: the swap chain is not
|
|
682
|
+
* `preserveDrawingBuffer`, so `getImageData` cannot reach it.
|
|
683
|
+
*
|
|
684
|
+
* A frame is judged against the **median of its two neighbours on each side** rather than against a
|
|
685
|
+
* running average, because the thing being looked for is one frame wide and a mean that includes it
|
|
686
|
+
* hides it. That costs a two-frame delay before a verdict, which is why the full-resolution ring
|
|
687
|
+
* below is five deep: by the time a frame is convicted it is two frames old, and it still has to be
|
|
688
|
+
* there to be looked at.
|
|
689
|
+
*/
|
|
690
|
+
class FlickerCatcher {
|
|
691
|
+
/**
|
|
692
|
+
* @param {HTMLCanvasElement} canvas the page's own, which is read and never written
|
|
693
|
+
* @param {number} threshold relative departure from the neighbours that counts, e.g. 0.15
|
|
694
|
+
* @param {boolean} freeze whether to stop on the first one and put it on screen
|
|
695
|
+
* @param {function(): string} describe answers a link that reopens the page where the camera
|
|
696
|
+
* stands — held per event, because where it happened is the one thing a report of one
|
|
697
|
+
* cannot be reconstructed without and the camera may have moved by the time it is read
|
|
698
|
+
*/
|
|
699
|
+
constructor(canvas, threshold, freeze, describe) {
|
|
700
|
+
this.source = canvas;
|
|
701
|
+
this.threshold = threshold;
|
|
702
|
+
this.freeze = freeze;
|
|
703
|
+
this.describe = describe;
|
|
704
|
+
|
|
705
|
+
this.width = 128;
|
|
706
|
+
this.height = 72;
|
|
707
|
+
|
|
708
|
+
this.scratch = document.createElement('canvas');
|
|
709
|
+
this.scratch.width = this.width;
|
|
710
|
+
this.scratch.height = this.height;
|
|
711
|
+
|
|
712
|
+
this.context = this.scratch.getContext('2d', { willReadFrequently: true });
|
|
713
|
+
|
|
714
|
+
/** Full-resolution frames, so a convicted one can be looked at rather than only counted. */
|
|
715
|
+
this.ring = [];
|
|
716
|
+
|
|
717
|
+
for (let i = 0; i < 5; i++) {
|
|
718
|
+
const frame = document.createElement('canvas');
|
|
719
|
+
|
|
720
|
+
this.ring.push({ canvas: frame, context: frame.getContext('2d'), frame: -1, mean: 0 });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
this.at = 0;
|
|
724
|
+
this.recent = [];
|
|
725
|
+
this.rows = [];
|
|
726
|
+
|
|
727
|
+
this.frames = 0;
|
|
728
|
+
this.events = [];
|
|
729
|
+
this.worst = 0;
|
|
730
|
+
this.frozen = false;
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Frames the scene has to have been drawing for before anything is judged.
|
|
734
|
+
*
|
|
735
|
+
* The first lit frame departs from the black ones before it by 103%, which is a real
|
|
736
|
+
* observation about a page that has just started and nothing at all about a dropout. Five
|
|
737
|
+
* is enough to clear it and short enough that arming is not something you wait for.
|
|
738
|
+
* @type {number}
|
|
739
|
+
*/
|
|
740
|
+
this.armed_after = 5;
|
|
741
|
+
|
|
742
|
+
this.drawing = 0;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* One frame. Called after the render, from the page's own loop.
|
|
747
|
+
*
|
|
748
|
+
* @param {object} state what {@link VGeoScenePlayground.state} answered this frame
|
|
749
|
+
*/
|
|
750
|
+
sample(state) {
|
|
751
|
+
if (this.frozen) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const source = this.source;
|
|
756
|
+
|
|
757
|
+
/*
|
|
758
|
+
The full-resolution copy is the expensive half — a whole-canvas `drawImage` every frame,
|
|
759
|
+
measured at rather more than the 128x72 read below — and it buys exactly one thing: a frame
|
|
760
|
+
that can be looked at after the fact. A run that is only counting does not need it, and
|
|
761
|
+
paying for it would slow the page enough to change the timing of whatever is being hunted.
|
|
762
|
+
*/
|
|
763
|
+
if (this.freeze) {
|
|
764
|
+
const slot = this.ring[this.at % this.ring.length];
|
|
765
|
+
|
|
766
|
+
if (slot.canvas.width !== source.width || slot.canvas.height !== source.height) {
|
|
767
|
+
slot.canvas.width = source.width;
|
|
768
|
+
slot.canvas.height = source.height;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
slot.context.drawImage(source, 0, 0);
|
|
772
|
+
slot.frame = state.frames;
|
|
773
|
+
|
|
774
|
+
this.at++;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
this.context.drawImage(source, 0, 0, this.width, this.height);
|
|
778
|
+
|
|
779
|
+
const pixels = this.context.getImageData(0, 0, this.width, this.height).data;
|
|
780
|
+
|
|
781
|
+
let sum = 0;
|
|
782
|
+
|
|
783
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
784
|
+
sum += 0.2126 * pixels[i] + 0.7152 * pixels[i + 1] + 0.0722 * pixels[i + 2];
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const mean = sum / (this.width * this.height);
|
|
788
|
+
|
|
789
|
+
this.frames++;
|
|
790
|
+
|
|
791
|
+
if (state.cut.drawn > 0) {
|
|
792
|
+
this.drawing++;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
this.rows.push([
|
|
796
|
+
state.frames, Math.round(mean * 100) / 100, state.cut.drawn, state.cut.clamped,
|
|
797
|
+
state.cut.queue_remaining, state.cut.meshlets_dropped, state.cut.instances_dropped,
|
|
798
|
+
state.cut.wavefronts, state.vram.pages, state.vram.refused, state.vram.evicted,
|
|
799
|
+
]);
|
|
800
|
+
|
|
801
|
+
if (this.rows.length > 4096) {
|
|
802
|
+
this.rows.shift();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
this.recent.push(mean);
|
|
806
|
+
|
|
807
|
+
if (this.recent.length > 5) {
|
|
808
|
+
this.recent.shift();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (this.recent.length < 5 || this.drawing < this.armed_after) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const middle = this.recent[2];
|
|
816
|
+
|
|
817
|
+
/*
|
|
818
|
+
**Both sides, same direction, and the smaller of the two departures.** Judging the middle
|
|
819
|
+
against the median of all four neighbours looks right and is not: when the picture takes a
|
|
820
|
+
persistent *step* — the scene streaming in, a coarse level replaced by a fine one — two of
|
|
821
|
+
the four neighbours sit on the far side of it, the median lands between the two levels, and
|
|
822
|
+
the frame *before* the step is convicted for being where it always was. Every one of
|
|
823
|
+
fourteen close-range poses "caught" one that way, and not one of them was a dropout.
|
|
824
|
+
|
|
825
|
+
A frame that is genuinely alone is above (or below) the pair before it *and* the pair after
|
|
826
|
+
it. Taking the smaller of the two departures is what makes a step score zero: on a step one
|
|
827
|
+
of the two is ~0 by construction, whichever side the middle belongs to.
|
|
828
|
+
*/
|
|
829
|
+
const before = (this.recent[0] + this.recent[1]) / 2;
|
|
830
|
+
const after = (this.recent[3] + this.recent[4]) / 2;
|
|
831
|
+
|
|
832
|
+
const from_before = (middle - before) / Math.max(before, 1e-6);
|
|
833
|
+
const from_after = (middle - after) / Math.max(after, 1e-6);
|
|
834
|
+
|
|
835
|
+
const departure = Math.sign(from_before) === Math.sign(from_after)
|
|
836
|
+
? Math.min(Math.abs(from_before), Math.abs(from_after))
|
|
837
|
+
: 0;
|
|
838
|
+
|
|
839
|
+
const local = (before + after) / 2;
|
|
840
|
+
|
|
841
|
+
this.worst = Math.max(this.worst, departure);
|
|
842
|
+
|
|
843
|
+
if (departure < this.threshold) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
const convicted = this.rows[this.rows.length - 3];
|
|
848
|
+
|
|
849
|
+
this.events.push({
|
|
850
|
+
frame: convicted[0],
|
|
851
|
+
departure: Math.round(departure * 10000) / 10000,
|
|
852
|
+
mean: Math.round(middle * 100) / 100,
|
|
853
|
+
neighbours: Math.round(local * 100) / 100,
|
|
854
|
+
url: this.describe(),
|
|
855
|
+
rows: this.rows.slice(Math.max(0, this.rows.length - 5)),
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
if (this.freeze) {
|
|
859
|
+
this.show(convicted[0]);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* @returns {boolean} whether a frame that was caught can still be put on screen
|
|
865
|
+
*/
|
|
866
|
+
get holds_frames() {
|
|
867
|
+
return this.freeze;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Put one of the held frames on screen and stop sampling, so what is on the page is that frame
|
|
872
|
+
* and a screenshot of the page is a screenshot of it.
|
|
873
|
+
*
|
|
874
|
+
* @param {number} frame
|
|
875
|
+
* @returns {string}
|
|
876
|
+
*/
|
|
877
|
+
show(frame) {
|
|
878
|
+
const slot = this.ring.find(entry => entry.frame === frame);
|
|
879
|
+
|
|
880
|
+
if (slot === undefined) {
|
|
881
|
+
return `frame ${frame} is no longer held; the ring has `
|
|
882
|
+
+ this.ring.map(entry => entry.frame).join(', ');
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
this.frozen = true;
|
|
886
|
+
|
|
887
|
+
for (const entry of this.ring) {
|
|
888
|
+
entry.canvas.remove();
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/*
|
|
892
|
+
**Under the panel, not over it.** At a z-index above it the held frame covered the readings
|
|
893
|
+
that explain it and the camera-link button that says where it happened — which is the one
|
|
894
|
+
moment both are wanted. The panel is at 10.
|
|
895
|
+
*/
|
|
896
|
+
slot.canvas.style.cssText = 'position:absolute;top:0;left:0;z-index:5;'
|
|
897
|
+
+ `width:${this.source.style.width};height:${this.source.style.height}`;
|
|
898
|
+
|
|
899
|
+
document.body.append(slot.canvas);
|
|
900
|
+
|
|
901
|
+
return `frozen on frame ${frame}`;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* @returns {object}
|
|
906
|
+
*/
|
|
907
|
+
report() {
|
|
908
|
+
return {
|
|
909
|
+
frames: this.frames,
|
|
910
|
+
threshold: this.threshold,
|
|
911
|
+
worst: Math.round(this.worst * 10000) / 10000,
|
|
912
|
+
events: this.events.length,
|
|
913
|
+
frozen: this.frozen,
|
|
914
|
+
held: this.ring.map(entry => entry.frame).filter(frame => frame >= 0),
|
|
915
|
+
last: this.events[this.events.length - 1],
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* @param {object} manifest
|
|
922
|
+
* @returns {number}
|
|
923
|
+
*/
|
|
924
|
+
function scene_faces(manifest) {
|
|
925
|
+
let faces = 0;
|
|
926
|
+
|
|
927
|
+
for (const geometry of manifest.geometries) {
|
|
928
|
+
faces += geometry.faces * (geometry.instances.length / 16);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return faces;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @param {number} bytes
|
|
936
|
+
* @returns {string}
|
|
937
|
+
*/
|
|
938
|
+
function human_bytes(bytes) {
|
|
939
|
+
if (bytes < 1024) {
|
|
940
|
+
return `${bytes} B`;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (bytes < 1024 * 1024) {
|
|
944
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
948
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return `${(bytes / (1024 ** 3)).toFixed(2)} GB`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Where the scene manifest is, out of `?scene=` or out of the asset server's own listing.
|
|
956
|
+
*
|
|
957
|
+
* The listing is what makes the page turn-key: `vgeo_serve.js` answers `/scenes.json` with the
|
|
958
|
+
* manifests in the directory it is serving, so a page opened with no parameters at all finds the
|
|
959
|
+
* one scene that is there. More than one and it takes the first and says which, because a page
|
|
960
|
+
* that refuses to choose between two is a page that shows nothing.
|
|
961
|
+
*
|
|
962
|
+
* @returns {Promise<string>}
|
|
963
|
+
*/
|
|
964
|
+
async function resolve_scene_url() {
|
|
965
|
+
const requested = new URL(globalThis.location.href).searchParams.get('scene');
|
|
966
|
+
|
|
967
|
+
if (requested !== null) {
|
|
968
|
+
return new URL(requested, globalThis.location.href).href;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const listing = await fetch(`${DEFAULT_ORIGIN}/scenes.json`);
|
|
972
|
+
|
|
973
|
+
if (!listing.ok) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
`no ?scene= was given and ${DEFAULT_ORIGIN} answered HTTP ${listing.status};`
|
|
976
|
+
+ ` start the asset server with`
|
|
977
|
+
+ ` "node tools/geometry/virtual/vgeo_serve.js <container directory>"`
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const scenes = await listing.json();
|
|
982
|
+
|
|
983
|
+
if (scenes.length === 0) {
|
|
984
|
+
throw new Error(
|
|
985
|
+
`${DEFAULT_ORIGIN} is serving no .vgeoscene.json;`
|
|
986
|
+
+ ` build one with "node tools/geometry/virtual/vgeo_scene.js <manifest.vgeo.json>"`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
return `${DEFAULT_ORIGIN}/${scenes[0]}`;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* A number that is the same for the same scene and comfortably above what it will ever need.
|
|
995
|
+
*
|
|
996
|
+
* Three of the runtime's capacities are properties of the *scene* rather than of the engine, and
|
|
997
|
+
* all three are knowable before a byte is fetched:
|
|
998
|
+
*
|
|
999
|
+
* - **`geometry_capacity`** is one row per container, and the default of 256 is an order of
|
|
1000
|
+
* magnitude short of a converted set.
|
|
1001
|
+
* - **`lookup_words`** is one word per page of every registered container, out of one arena. The
|
|
1002
|
+
* default of 65,536 holds a fourteenth of Zorah's 947,664, and it is allocated out of an
|
|
1003
|
+
* `OffsetAllocator` whose bins round *down* — it can refuse an allocation that fits — so the
|
|
1004
|
+
* slack here is the difference between a scene that registers and one that refuses its own
|
|
1005
|
+
* largest container for no visible reason.
|
|
1006
|
+
* - **`claim_word_capacity`** is one bit per (collected instance, page ordinal of its geometry).
|
|
1007
|
+
* The engine derives it from a hint of a thousand instances times the whole tier, which at this
|
|
1008
|
+
* scale is both far too small and far too large: the real bound is
|
|
1009
|
+
* `max instances of any one geometry × page capacity`, because ordinals come out of the tier and
|
|
1010
|
+
* the sum of `instances × ordinals` is maximised by giving every slot to the most-instanced
|
|
1011
|
+
* asset. For Zorah that is 381 × the tier, which is a fifth of what the default derivation asks
|
|
1012
|
+
* for and provably enough — doubled here, because an ordinal capacity is a high-water mark and a
|
|
1013
|
+
* scene that has churned can hold more of them than the tier holds pages.
|
|
1014
|
+
*
|
|
1015
|
+
* @param {object} manifest
|
|
1016
|
+
* @param {number} page_capacity
|
|
1017
|
+
* @returns {{geometry_capacity: number, lookup_words: number, claim_word_capacity: number, max_instances: number}}
|
|
1018
|
+
*/
|
|
1019
|
+
function scene_capacities(manifest, page_capacity) {
|
|
1020
|
+
let max_instances = 1;
|
|
1021
|
+
|
|
1022
|
+
for (const geometry of manifest.geometries) {
|
|
1023
|
+
max_instances = Math.max(max_instances, geometry.instances.length / 16);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return {
|
|
1027
|
+
geometry_capacity: manifest.geometry_count + 64,
|
|
1028
|
+
lookup_words: 1 << Math.ceil(Math.log2(Math.max(1 << 16, manifest.total_pages * 1.5))),
|
|
1029
|
+
claim_word_capacity: Math.ceil(max_instances * page_capacity / 32) * 2,
|
|
1030
|
+
max_instances,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function start() {
|
|
1035
|
+
const canvas = element('view');
|
|
1036
|
+
|
|
1037
|
+
const page = new VGeoScenePlayground();
|
|
1038
|
+
|
|
1039
|
+
page.canvas = canvas;
|
|
1040
|
+
|
|
1041
|
+
const parameters = new URL(globalThis.location.href).searchParams;
|
|
1042
|
+
|
|
1043
|
+
page.status('reading the scene manifest…');
|
|
1044
|
+
|
|
1045
|
+
const scene_url = await resolve_scene_url();
|
|
1046
|
+
|
|
1047
|
+
const manifest_response = await fetch(scene_url);
|
|
1048
|
+
|
|
1049
|
+
if (!manifest_response.ok) {
|
|
1050
|
+
throw new Error(`${scene_url} answered HTTP ${manifest_response.status}`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const manifest = await manifest_response.json();
|
|
1054
|
+
|
|
1055
|
+
if (manifest.format !== 1) {
|
|
1056
|
+
throw new Error(
|
|
1057
|
+
`${scene_url} is a format-${manifest.format} scene and this page reads format 1`
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
page.manifest = manifest;
|
|
1062
|
+
page.base = scene_url;
|
|
1063
|
+
|
|
1064
|
+
const box = manifest.world_bounds;
|
|
1065
|
+
|
|
1066
|
+
page.world_radius = 0.5 * Math.hypot(box[3] - box[0], box[4] - box[1], box[5] - box[2]);
|
|
1067
|
+
|
|
1068
|
+
/*
|
|
1069
|
+
Page slots the VRAM tier is built with, over the engine's default of 1,024 and the runtime
|
|
1070
|
+
page's 4,096. A slot holds a page's *runtime rows* rather than its payload — an eighth of the
|
|
1071
|
+
page size — so this is a 96 MB arena; what it really spends is the decoded budget the pool
|
|
1072
|
+
derives from it, which is 12,288 pages of payload in the meshlet pool, or 768 MB.
|
|
1073
|
+
|
|
1074
|
+
**The number is what the τ slider costs, measured.** Zorah from its authored viewpoint at
|
|
1075
|
+
1578×746 settles at 6,387 resident pages at τ=1 and 9,412 at τ=0.5, which is the slider's
|
|
1076
|
+
busiest end; at 8,192 slots that end fills the tier, `refused` climbs past two hundred
|
|
1077
|
+
thousand, `clamped` stops falling to zero and the cut is being selected by a **budget** rather
|
|
1078
|
+
than by τ — which is the one thing this page is not meant to be showing. A bigger window wants
|
|
1079
|
+
more, because the cut is a function of the resolution, so it is a URL parameter and `refused`
|
|
1080
|
+
is in the panel to say when it was the wrong one.
|
|
1081
|
+
*/
|
|
1082
|
+
const page_capacity = Number(parameters.get('pages') ?? 12288);
|
|
1083
|
+
|
|
1084
|
+
const capacities = scene_capacities(manifest, page_capacity);
|
|
1085
|
+
|
|
1086
|
+
const renderer = new Renderer();
|
|
1087
|
+
|
|
1088
|
+
page.renderer = renderer;
|
|
1089
|
+
|
|
1090
|
+
await renderer.initialize({
|
|
1091
|
+
context: canvas.getContext('webgpu'),
|
|
1092
|
+
pixelRatio: globalThis.devicePixelRatio || 1,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
/*
|
|
1096
|
+
TAA on. A cut is re-selected every frame against a threshold in pixels, so the seams between
|
|
1097
|
+
two levels of detail land on different pixels from one frame to the next and every one of them
|
|
1098
|
+
is an aliased edge that crawls without a temporal filter — and the crawl reads as the cut being
|
|
1099
|
+
unstable when it is the rasterizer being point-sampled. It costs the readings nothing: TAA is
|
|
1100
|
+
downstream of the G-buffer, long after the cut has been selected and drawn.
|
|
1101
|
+
*/
|
|
1102
|
+
renderer.feature_taa_enabled = true;
|
|
1103
|
+
renderer.feature_ssao_enabled = false;
|
|
1104
|
+
renderer.feature_bloom_enabled = false;
|
|
1105
|
+
|
|
1106
|
+
const scene = new Scene();
|
|
1107
|
+
|
|
1108
|
+
scene.dataset = new EntityComponentDataset();
|
|
1109
|
+
|
|
1110
|
+
page.scene = scene;
|
|
1111
|
+
|
|
1112
|
+
/*
|
|
1113
|
+
The scene's own sun when the manifest names one, and the engine's default otherwise.
|
|
1114
|
+
|
|
1115
|
+
A converted container carries geometry and nothing else — no material authoring, no lights —
|
|
1116
|
+
so the sun is most of what there is to shade it with, and which way it points is not
|
|
1117
|
+
decoration: the engine's default comes down from above and slightly behind the world axes,
|
|
1118
|
+
which over an outdoor scene authored around a different one puts every façade a viewer would
|
|
1119
|
+
stand in front of into ambient. `--sundirection` in Zorah's own config is the direction it
|
|
1120
|
+
shines *from*, which is the opposite of the direction a light points.
|
|
1121
|
+
*/
|
|
1122
|
+
const sun = manifest.sun === undefined
|
|
1123
|
+
? make_sunlight()
|
|
1124
|
+
: make_sunlight({
|
|
1125
|
+
direction: new Vector3(-manifest.sun[0], -manifest.sun[1], -manifest.sun[2]),
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
place_light(scene, sun);
|
|
1129
|
+
|
|
1130
|
+
/*
|
|
1131
|
+
Image-based lighting, and for the geometry rather than for the picture: a sun alone puts every
|
|
1132
|
+
surface facing away from it at ambient zero, which is exactly the silhouettes and the creases a
|
|
1133
|
+
cut is worth looking at. Not awaited — `test_assets/` is gitignored, so a checkout without it
|
|
1134
|
+
gets the sun alone and still draws every triangle the cut selected.
|
|
1135
|
+
*/
|
|
1136
|
+
load_default_environment(scene);
|
|
1137
|
+
|
|
1138
|
+
const camera = new PerspectiveCamera();
|
|
1139
|
+
|
|
1140
|
+
camera.fov_degrees = manifest.camera?.fov ?? 60;
|
|
1141
|
+
|
|
1142
|
+
page.camera = camera;
|
|
1143
|
+
page.controller = new OrbitalCameraController(camera, document.body);
|
|
1144
|
+
|
|
1145
|
+
page.fit();
|
|
1146
|
+
|
|
1147
|
+
globalThis.addEventListener('resize', () => page.fit());
|
|
1148
|
+
|
|
1149
|
+
// one frame to let the renderer build its scene context, which is what owns the geometry manager
|
|
1150
|
+
renderer.render(camera, scene, 1 / 60);
|
|
1151
|
+
|
|
1152
|
+
const scene_context = renderer.scenes.obtain(scene);
|
|
1153
|
+
|
|
1154
|
+
page.scene_context = scene_context;
|
|
1155
|
+
|
|
1156
|
+
/*
|
|
1157
|
+
The residency's knobs, which are per-reading budgets rather than sizes — nothing here scales
|
|
1158
|
+
with how much is resident. What a scene changes about them is the breadth each reading has to
|
|
1159
|
+
cover: the wants of one reading now come from thousands of containers rather than from one, so
|
|
1160
|
+
a fetch budget sized for a single asset spends the whole reading on whichever asset happened to
|
|
1161
|
+
be first in the want list.
|
|
1162
|
+
*/
|
|
1163
|
+
const residency = new VGeoResidencyOptions();
|
|
1164
|
+
|
|
1165
|
+
residency.ram_budget_bytes = 1024 * 1024 * 1024;
|
|
1166
|
+
|
|
1167
|
+
/*
|
|
1168
|
+
VRAM is a strict subset of RAM, so the RAM tier has to be able to hold the whole VRAM tier and
|
|
1169
|
+
every container's pinned root besides — 8,192 and 2,966 for Zorah, against a default of 16,384
|
|
1170
|
+
slots that the two together are already within sight of.
|
|
1171
|
+
*/
|
|
1172
|
+
residency.ram_slot_capacity = Math.max(1 << 14, (page_capacity + manifest.geometry_count) * 2);
|
|
1173
|
+
|
|
1174
|
+
residency.max_fetches_per_reading = 64;
|
|
1175
|
+
residency.max_concurrent_fetches = 24;
|
|
1176
|
+
residency.max_promotions_per_reading = 128;
|
|
1177
|
+
|
|
1178
|
+
page.runtime = scene_context.enable_virtual_geometry({
|
|
1179
|
+
page_size: manifest.page_size,
|
|
1180
|
+
table_options: {
|
|
1181
|
+
page_capacity,
|
|
1182
|
+
geometry_capacity: capacities.geometry_capacity,
|
|
1183
|
+
lookup_words: capacities.lookup_words,
|
|
1184
|
+
},
|
|
1185
|
+
claim_word_capacity: capacities.claim_word_capacity,
|
|
1186
|
+
/*
|
|
1187
|
+
Pages one frame may ask for, across every view. A single asset's cut wants a handful once
|
|
1188
|
+
it has settled; a cold scene of three thousand containers wants from all of them at once,
|
|
1189
|
+
and a want the frame had no room for is one more reading before that page arrives.
|
|
1190
|
+
*/
|
|
1191
|
+
want_capacity: 8192,
|
|
1192
|
+
/*
|
|
1193
|
+
Traversal items one wavefront may hold, over the engine's 1 << 16. This is the capacity a
|
|
1194
|
+
*scene* reaches first and the only one whose overflow is invisible without looking: at
|
|
1195
|
+
τ=0.5 with 17,424 instances the descent pushed past 65,536 items in flight and
|
|
1196
|
+
`VGEO_CUT_STAT.QUEUE_DROPPED` came back at 32,676 — a third of the frontier thrown away,
|
|
1197
|
+
which showed up not as an error but as `drawn` wandering over ten thousand clusters
|
|
1198
|
+
between one frame and the next as different subtrees lost the race.
|
|
1199
|
+
|
|
1200
|
+
It is two buffers of `capacity × 16` bytes per expansion site, so this is 4 MiB a site
|
|
1201
|
+
against the 1 MiB the default costs. At 1 << 18 the same view drops nothing.
|
|
1202
|
+
*/
|
|
1203
|
+
queue_capacity: 1 << 18,
|
|
1204
|
+
/*
|
|
1205
|
+
The wavefront bound is the engine's and is deliberately **not** set here. It starts at 8
|
|
1206
|
+
and `GPUVirtualGeometryRuntime.note_incomplete_descent` raises it from what the cut
|
|
1207
|
+
reports, so this page gets the same escalation any host does — and pinning it here would
|
|
1208
|
+
hide the one behaviour worth watching from the page best placed to watch it.
|
|
1209
|
+
|
|
1210
|
+
`?iterations=` overrides it for an experiment. At 8 with no escalation, this scene at
|
|
1211
|
+
3419×1286 left up to 13,467 items queued on 750 frames of 900 and `drawn` swung 11,857
|
|
1212
|
+
clusters; the panel's `dropped: queue N` row is that reading.
|
|
1213
|
+
*/
|
|
1214
|
+
...(parameters.has('iterations')
|
|
1215
|
+
? { max_iterations: Number(parameters.get('iterations')), max_iteration_ceiling: Number(parameters.get('iterations')) }
|
|
1216
|
+
: {}),
|
|
1217
|
+
residency,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
page.residency = scene_context.virtual_geometry_residency;
|
|
1221
|
+
page.feedback = scene_context.virtual_geometry_feedback;
|
|
1222
|
+
|
|
1223
|
+
/*
|
|
1224
|
+
What the rasterization collections are primed to expect. Both of these grow on their own from
|
|
1225
|
+
what the frame reports, so priming is about the first seconds rather than the steady state —
|
|
1226
|
+
but the first seconds are when the scene is being placed, and a `Meshes` limit of 1e4 against
|
|
1227
|
+
17,424 instances drops the difference on the floor and counts them as `instances_dropped` while
|
|
1228
|
+
the collection catches up.
|
|
1229
|
+
*/
|
|
1230
|
+
const limits = renderer.graphics.collection_limits;
|
|
1231
|
+
|
|
1232
|
+
limits.prime(
|
|
1233
|
+
RasterizationCollectionType.Meshes,
|
|
1234
|
+
Math.max(1e4, Math.ceil(manifest.instance_count * 1.25))
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
limits.prime(RasterizationCollectionType.Meshlets, 5e5);
|
|
1238
|
+
|
|
1239
|
+
const material = new StandardShadeMaterial();
|
|
1240
|
+
|
|
1241
|
+
material.diffuse_color.copy(new Color(0.72, 0.74, 0.78));
|
|
1242
|
+
material.roughness_factor = 0.55;
|
|
1243
|
+
material.metallic_factor = 0;
|
|
1244
|
+
|
|
1245
|
+
/*
|
|
1246
|
+
`?side=front|back|double` sets the whole scene's draw side, which is the cheapest way to ask
|
|
1247
|
+
whether what you are looking at is a winding problem: `double` culls nothing, so any surface
|
|
1248
|
+
that appears under it and not under `front` was being culled rather than being absent.
|
|
1249
|
+
*/
|
|
1250
|
+
material.draw_side = DRAW_SIDES[parameters.get('side')] ?? ShadeDrawSide.Front;
|
|
1251
|
+
|
|
1252
|
+
page.material = material;
|
|
1253
|
+
|
|
1254
|
+
/*
|
|
1255
|
+
`?mirrored=flip|tint|double` puts reflected instances in a bucket of their own — see
|
|
1256
|
+
{@link VGeoScenePlayground.mirror_material}. Off by default: the engine reverses their winding
|
|
1257
|
+
per instance, so the default is already the correct picture.
|
|
1258
|
+
|
|
1259
|
+
- **tint** leaves the side alone and colours them, so you can see which geometry in the frame
|
|
1260
|
+
is reflected without changing what is drawn.
|
|
1261
|
+
- **flip** draws them `Back`-sided. This was the page's own fix before the engine had one; now
|
|
1262
|
+
it reverses a second time and reproduces the old inside-out picture on demand.
|
|
1263
|
+
- **double** draws both sides of them, which fills the holes and gets the shading wrong, and is
|
|
1264
|
+
here because it is the fix people reach for first.
|
|
1265
|
+
*/
|
|
1266
|
+
const mirrored_mode = parameters.get('mirrored');
|
|
1267
|
+
|
|
1268
|
+
const mirror_material = new StandardShadeMaterial();
|
|
1269
|
+
|
|
1270
|
+
mirror_material.copy(material);
|
|
1271
|
+
|
|
1272
|
+
if (mirrored_mode === 'flip') {
|
|
1273
|
+
mirror_material.draw_side = ShadeDrawSide.Back;
|
|
1274
|
+
} else if (mirrored_mode === 'double') {
|
|
1275
|
+
mirror_material.draw_side = ShadeDrawSide.Double;
|
|
1276
|
+
} else if (mirrored_mode === 'tint') {
|
|
1277
|
+
mirror_material.diffuse_color.copy(new Color(0.85, 0.16, 0.12));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
page.mirror_material = mirrored_mode === null ? material : mirror_material;
|
|
1281
|
+
|
|
1282
|
+
globalThis.addEventListener('pagehide', () => page.feedback.destroy());
|
|
1283
|
+
|
|
1284
|
+
renderer.add_extension(new VGeoFrameStart(page));
|
|
1285
|
+
renderer.add_extension(new VGeoFrameEnd(page));
|
|
1286
|
+
|
|
1287
|
+
element('tau').addEventListener('input', event => {
|
|
1288
|
+
page.runtime.tau = Number(event.target.value);
|
|
1289
|
+
|
|
1290
|
+
element('tau-value').textContent = page.runtime.tau.toFixed(2);
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
/*
|
|
1294
|
+
τ before the first container, for a session measuring one. Residency is a **high-water mark** —
|
|
1295
|
+
the tier only evicts under pressure — so a page swept downwards through the thresholds reports,
|
|
1296
|
+
at every step, the pages the *busiest* setting it has already been at asked for. A row of a τ
|
|
1297
|
+
table is only about τ if the page has been at nothing else, which means a load per row.
|
|
1298
|
+
*/
|
|
1299
|
+
const requested_tau = Number(parameters.get('tau'));
|
|
1300
|
+
|
|
1301
|
+
if (Number.isFinite(requested_tau) && requested_tau > 0) {
|
|
1302
|
+
page.runtime.tau = requested_tau;
|
|
1303
|
+
|
|
1304
|
+
element('tau').value = String(requested_tau);
|
|
1305
|
+
element('tau-value').textContent = requested_tau.toFixed(2);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// a drag on the τ slider is a drag on the body, and the camera controller listens there
|
|
1309
|
+
element('panel').addEventListener('pointerdown', event => event.stopPropagation());
|
|
1310
|
+
|
|
1311
|
+
install_profile_hotkey({
|
|
1312
|
+
renderer,
|
|
1313
|
+
name: "vgeo_scene",
|
|
1314
|
+
status: element('profile-status'),
|
|
1315
|
+
settings: () => {
|
|
1316
|
+
const state = page.state();
|
|
1317
|
+
|
|
1318
|
+
return [
|
|
1319
|
+
`τ=${state.tau}px`,
|
|
1320
|
+
state.scene.name,
|
|
1321
|
+
`${state.scene.containers} containers`,
|
|
1322
|
+
`${state.scene.instances} instances`,
|
|
1323
|
+
`${state.cut.drawn} clusters`,
|
|
1324
|
+
`${state.vram.pages} of ${state.vram.capacity} pages resident`,
|
|
1325
|
+
`${state.screen_width}×${state.screen_height}`,
|
|
1326
|
+
];
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
globalThis.__state = () => page.state();
|
|
1331
|
+
globalThis.__page = page;
|
|
1332
|
+
globalThis.__look = (eye, target) => page.look(eye, target);
|
|
1333
|
+
globalThis.__camera_url = () => camera_url(page);
|
|
1334
|
+
|
|
1335
|
+
/*
|
|
1336
|
+
The camera before the first container, so the cut that runs while the scene is arriving is the
|
|
1337
|
+
one the viewpoint asked for rather than a cut of everything from the origin.
|
|
1338
|
+
|
|
1339
|
+
The manifest's camera when it has one — a converted scene's own authored pose, which is the
|
|
1340
|
+
only thing that knows which forty metres of a hundred-kilometre box the scene is *about*. With
|
|
1341
|
+
no camera, the box: wrong for Zorah and right for a scene that is its own extent, and the
|
|
1342
|
+
parameter overrides both.
|
|
1343
|
+
*/
|
|
1344
|
+
const eye = read_vector(parameters.get('eye')) ?? manifest.camera?.eye;
|
|
1345
|
+
const target = read_vector(parameters.get('at')) ?? manifest.camera?.target;
|
|
1346
|
+
|
|
1347
|
+
const center = [
|
|
1348
|
+
(box[0] + box[3]) / 2, (box[1] + box[4]) / 2, (box[2] + box[5]) / 2,
|
|
1349
|
+
];
|
|
1350
|
+
|
|
1351
|
+
page.viewpoints = [
|
|
1352
|
+
{
|
|
1353
|
+
name: 'scene',
|
|
1354
|
+
eye: [
|
|
1355
|
+
center[0] + page.world_radius * 0.9,
|
|
1356
|
+
center[1] + page.world_radius * 0.5,
|
|
1357
|
+
center[2] + page.world_radius * 0.9,
|
|
1358
|
+
],
|
|
1359
|
+
target: center,
|
|
1360
|
+
},
|
|
1361
|
+
];
|
|
1362
|
+
|
|
1363
|
+
if (manifest.camera !== undefined) {
|
|
1364
|
+
page.viewpoints.unshift({
|
|
1365
|
+
name: 'authored',
|
|
1366
|
+
eye: manifest.camera.eye,
|
|
1367
|
+
target: manifest.camera.target,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const start_view = eye !== undefined && target !== undefined
|
|
1372
|
+
? { name: 'url', eye, target }
|
|
1373
|
+
: page.viewpoints[0];
|
|
1374
|
+
|
|
1375
|
+
page.look(start_view.eye, start_view.target);
|
|
1376
|
+
|
|
1377
|
+
wire_viewpoints(page);
|
|
1378
|
+
wire_camera_capture(page);
|
|
1379
|
+
|
|
1380
|
+
/*
|
|
1381
|
+
`?catch=0.15` arms the dropout catcher — see {@link FlickerCatcher} for why it reads pixels
|
|
1382
|
+
rather than counters. Off by default: it is a `drawImage` and a `getImageData` every frame, and
|
|
1383
|
+
a page that is a measurement harness should not pay for a diagnostic nobody asked for.
|
|
1384
|
+
|
|
1385
|
+
`&freeze=1` stops on the first one and leaves it on screen, which is what makes a screenshot of
|
|
1386
|
+
the page a screenshot of the offending frame. Without it the catcher keeps counting and the
|
|
1387
|
+
panel shows the running tally, which is the mode for flying around looking for one.
|
|
1388
|
+
*/
|
|
1389
|
+
const catch_threshold = Number(parameters.get('catch'));
|
|
1390
|
+
|
|
1391
|
+
if (Number.isFinite(catch_threshold) && catch_threshold > 0) {
|
|
1392
|
+
page.catcher = new FlickerCatcher(
|
|
1393
|
+
canvas, catch_threshold, parameters.get('freeze') !== null, () => camera_url(page)
|
|
1394
|
+
);
|
|
1395
|
+
|
|
1396
|
+
globalThis.__caught = () => page.catcher.report();
|
|
1397
|
+
globalThis.__show_frame = frame => page.catcher.show(frame);
|
|
1398
|
+
globalThis.__catch_rows = () => page.catcher.rows;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
let stopped = false;
|
|
1402
|
+
let last_time;
|
|
1403
|
+
|
|
1404
|
+
function tick(time) {
|
|
1405
|
+
if (stopped) {
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const dt = last_time === undefined ? 1 / 60 : Math.min((time - last_time) / 1000, 0.05);
|
|
1410
|
+
|
|
1411
|
+
if (last_time !== undefined) {
|
|
1412
|
+
page.frame_ms += ((time - last_time) - page.frame_ms) * 0.1;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
last_time = time;
|
|
1416
|
+
|
|
1417
|
+
try {
|
|
1418
|
+
page.controller.update(dt);
|
|
1419
|
+
|
|
1420
|
+
renderer.render(camera, scene, dt);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
stopped = true;
|
|
1423
|
+
|
|
1424
|
+
page.status(error.message ?? String(error), true);
|
|
1425
|
+
|
|
1426
|
+
console.error(error);
|
|
1427
|
+
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
const state = page.state();
|
|
1432
|
+
|
|
1433
|
+
// after the render, so the canvas it reads is this frame's
|
|
1434
|
+
page.catcher?.sample(state);
|
|
1435
|
+
|
|
1436
|
+
paint(page, capacities, state);
|
|
1437
|
+
|
|
1438
|
+
requestAnimationFrame(tick);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
requestAnimationFrame(tick);
|
|
1442
|
+
|
|
1443
|
+
page.status(`opening ${manifest.geometry_count.toLocaleString()} containers…`);
|
|
1444
|
+
|
|
1445
|
+
/*
|
|
1446
|
+
Not awaited: the frame loop above is already running, and a scene that placed its first
|
|
1447
|
+
container two hundred frames before its last is the loop this page exists to show.
|
|
1448
|
+
*/
|
|
1449
|
+
page.open_scene(manifest);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* `"x,y,z"` out of a URL parameter.
|
|
1454
|
+
*
|
|
1455
|
+
* @param {string|null} text
|
|
1456
|
+
* @returns {number[]|undefined}
|
|
1457
|
+
*/
|
|
1458
|
+
function read_vector(text) {
|
|
1459
|
+
if (text === null) {
|
|
1460
|
+
return undefined;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const numbers = text.split(',').map(Number);
|
|
1464
|
+
|
|
1465
|
+
if (numbers.length !== 3 || numbers.some(n => !Number.isFinite(n))) {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
return numbers;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
/**
|
|
1473
|
+
* A link that reopens the page **where it is standing now**, with everything else about how it was
|
|
1474
|
+
* opened left alone.
|
|
1475
|
+
*
|
|
1476
|
+
* This exists because a viewpoint is the one piece of state that a person has and a session does
|
|
1477
|
+
* not. Everything else this page can be told — the scene, τ, the tier's size, which side to draw —
|
|
1478
|
+
* is a URL parameter already; where the camera got dragged to is not, and a phenomenon that happens
|
|
1479
|
+
* "in many places" is useless to chase without one of them. So the button hands over a URL rather
|
|
1480
|
+
* than three numbers: paste it back and the page opens on the same view, at the same threshold,
|
|
1481
|
+
* with the same tier, and with whatever diagnostic was already armed still armed.
|
|
1482
|
+
*
|
|
1483
|
+
* `?eye=`/`?at=` were already the way in; this is the way out.
|
|
1484
|
+
*
|
|
1485
|
+
* @param {VGeoScenePlayground} page
|
|
1486
|
+
* @returns {string}
|
|
1487
|
+
*/
|
|
1488
|
+
function camera_url(page) {
|
|
1489
|
+
const url = new URL(globalThis.location.href);
|
|
1490
|
+
|
|
1491
|
+
const state = page.state();
|
|
1492
|
+
|
|
1493
|
+
// nine significant digits, which is exact for a float and about half the characters an f64
|
|
1494
|
+
// print of one costs — the same trim `vgeo_scene.js` writes its matrices with
|
|
1495
|
+
const trim = value => Number(value.toPrecision(9));
|
|
1496
|
+
|
|
1497
|
+
const triple = values => values.map(trim).join(',');
|
|
1498
|
+
|
|
1499
|
+
url.searchParams.set('eye', triple(state.camera.eye));
|
|
1500
|
+
url.searchParams.set('at', triple([
|
|
1501
|
+
state.camera.target.x, state.camera.target.y, state.camera.target.z,
|
|
1502
|
+
]));
|
|
1503
|
+
|
|
1504
|
+
/*
|
|
1505
|
+
τ as it stands rather than as it was asked for. The slider moves and the parameter does not
|
|
1506
|
+
follow it, so a link that carried only what the page was opened with would reopen at a
|
|
1507
|
+
different cut from the one being looked at — which for a page about a cut is the whole thing.
|
|
1508
|
+
*/
|
|
1509
|
+
url.searchParams.set('tau', String(trim(state.tau)));
|
|
1510
|
+
|
|
1511
|
+
/*
|
|
1512
|
+
`URLSearchParams` percent-encodes the separators, and `eye=27.9%2C8.9%2C-4` is correct, ugly,
|
|
1513
|
+
and about to be read by a person and pasted into a message. A comma is legal unencoded in a
|
|
1514
|
+
query string, so it goes back.
|
|
1515
|
+
*/
|
|
1516
|
+
return url.href.replace(/%2C/g, ',');
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* @param {VGeoScenePlayground} page
|
|
1521
|
+
*/
|
|
1522
|
+
function wire_camera_capture(page) {
|
|
1523
|
+
const button = element('copy-camera');
|
|
1524
|
+
const output = element('camera-url');
|
|
1525
|
+
|
|
1526
|
+
button.addEventListener('click', async () => {
|
|
1527
|
+
const url = camera_url(page);
|
|
1528
|
+
|
|
1529
|
+
output.textContent = url;
|
|
1530
|
+
output.hidden = false;
|
|
1531
|
+
|
|
1532
|
+
/*
|
|
1533
|
+
The clipboard is a permission and a secure context away, and this page is served over
|
|
1534
|
+
plain HTTP from a dev server on whatever host someone pointed it at. So the text is put on
|
|
1535
|
+
screen first and copied second: the button works either way, and the label says which
|
|
1536
|
+
happened rather than failing silently.
|
|
1537
|
+
*/
|
|
1538
|
+
try {
|
|
1539
|
+
await navigator.clipboard.writeText(url);
|
|
1540
|
+
|
|
1541
|
+
button.textContent = 'copied to clipboard';
|
|
1542
|
+
|
|
1543
|
+
// the controller's keyboard device listens on the body, and a click leaves focus here.
|
|
1544
|
+
// Only on the happy path: the fallback below has just made a selection, and moving
|
|
1545
|
+
// focus would drop it.
|
|
1546
|
+
document.body.focus();
|
|
1547
|
+
} catch {
|
|
1548
|
+
/*
|
|
1549
|
+
Selected rather than merely shown, so the fallback is one Ctrl+C rather than a drag
|
|
1550
|
+
across four lines of wrapped URL. `user-select: all` makes a click do the same thing,
|
|
1551
|
+
and this makes the click unnecessary.
|
|
1552
|
+
*/
|
|
1553
|
+
const range = document.createRange();
|
|
1554
|
+
|
|
1555
|
+
range.selectNodeContents(output);
|
|
1556
|
+
|
|
1557
|
+
const selection = globalThis.getSelection();
|
|
1558
|
+
|
|
1559
|
+
selection.removeAllRanges();
|
|
1560
|
+
selection.addRange(range);
|
|
1561
|
+
|
|
1562
|
+
button.textContent = 'selected below — Ctrl+C';
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
setTimeout(() => { button.textContent = 'copy camera link'; }, 2500);
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* @param {VGeoScenePlayground} page
|
|
1571
|
+
*/
|
|
1572
|
+
function wire_viewpoints(page) {
|
|
1573
|
+
const container = element('views');
|
|
1574
|
+
|
|
1575
|
+
container.innerHTML = '';
|
|
1576
|
+
|
|
1577
|
+
for (const viewpoint of page.viewpoints) {
|
|
1578
|
+
const button = document.createElement('button');
|
|
1579
|
+
|
|
1580
|
+
button.textContent = viewpoint.name;
|
|
1581
|
+
|
|
1582
|
+
button.addEventListener('click', () => {
|
|
1583
|
+
page.look(viewpoint.eye, viewpoint.target);
|
|
1584
|
+
|
|
1585
|
+
// the controller's keyboard device listens on the body, and a click leaves focus here
|
|
1586
|
+
document.body.focus();
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
container.append(button);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
/**
|
|
1594
|
+
* @param {VGeoScenePlayground} page
|
|
1595
|
+
* @param {object} capacities
|
|
1596
|
+
*/
|
|
1597
|
+
function paint(page, capacities, state) {
|
|
1598
|
+
const scene = state.scene;
|
|
1599
|
+
|
|
1600
|
+
element('progress').firstElementChild.style.width =
|
|
1601
|
+
`${scene.declared === 0 ? 0 : (100 * scene.containers / scene.declared).toFixed(1)}%`;
|
|
1602
|
+
|
|
1603
|
+
rows('scene', [
|
|
1604
|
+
['source', escape_html(scene.name)],
|
|
1605
|
+
['containers', `${scene.containers.toLocaleString()} / ${scene.declared.toLocaleString()}`],
|
|
1606
|
+
['instances', scene.instances.toLocaleString()],
|
|
1607
|
+
['reflected', scene.mirrored.toLocaleString()],
|
|
1608
|
+
['source triangles', scene.faces.toLocaleString()],
|
|
1609
|
+
['on the server', human_bytes(scene.bytes)],
|
|
1610
|
+
['read', human_bytes(scene.bytes_read)],
|
|
1611
|
+
['view', `${state.screen_width}×${state.screen_height}`],
|
|
1612
|
+
['frame', `${state.frame_ms.toFixed(1)} ms`],
|
|
1613
|
+
]);
|
|
1614
|
+
|
|
1615
|
+
rows('cut', [
|
|
1616
|
+
['drawn clusters', state.cut.drawn.toLocaleString()],
|
|
1617
|
+
['clamped', state.cut.clamped.toLocaleString()],
|
|
1618
|
+
['pages visited', state.cut.pages.toLocaleString()],
|
|
1619
|
+
['clusters tested', state.cut.clusters_tested.toLocaleString()],
|
|
1620
|
+
['subtrees culled', (state.cut.culled_frustum + state.cut.culled_lod).toLocaleString()],
|
|
1621
|
+
['clusters culled', state.cut.culled_cluster.toLocaleString()],
|
|
1622
|
+
[
|
|
1623
|
+
'wavefronts',
|
|
1624
|
+
`${state.cut.wavefronts} / ${state.cut.max_iterations}`
|
|
1625
|
+
+ (state.cut.iteration_escalations > 0 ? ` (${state.cut.iteration_escalations}↑)` : ''),
|
|
1626
|
+
],
|
|
1627
|
+
['wants', state.wants],
|
|
1628
|
+
]);
|
|
1629
|
+
|
|
1630
|
+
/*
|
|
1631
|
+
Four counts of the cut giving up, kept apart from the six of it succeeding, because at this
|
|
1632
|
+
scale every one of them names a different capacity and a scene is the first thing big enough
|
|
1633
|
+
to reach any of them.
|
|
1634
|
+
*/
|
|
1635
|
+
const dropped = [
|
|
1636
|
+
['meshlets', state.cut.meshlets_dropped],
|
|
1637
|
+
['instances', state.cut.instances_dropped],
|
|
1638
|
+
['queue', state.cut.queue_dropped + state.cut.queue_remaining],
|
|
1639
|
+
['wants', state.cut.wants_dropped],
|
|
1640
|
+
].filter(([, count]) => count > 0);
|
|
1641
|
+
|
|
1642
|
+
rows('residency', [
|
|
1643
|
+
['VRAM pages', `${state.vram.pages.toLocaleString()} / ${state.vram.capacity.toLocaleString()}`],
|
|
1644
|
+
['VRAM bytes', human_bytes(state.vram.bytes)],
|
|
1645
|
+
['RAM pages', state.ram.pages.toLocaleString()],
|
|
1646
|
+
['RAM bytes', human_bytes(state.ram.bytes)],
|
|
1647
|
+
['evicted', `${state.vram.evicted.toLocaleString()} / ${state.ram.evicted.toLocaleString()}`],
|
|
1648
|
+
['fetching', state.fetches_in_flight],
|
|
1649
|
+
[
|
|
1650
|
+
'refused',
|
|
1651
|
+
`<span class="${state.vram.refused > 0 ? 'bad' : 'good'}">`
|
|
1652
|
+
+ `${state.vram.refused.toLocaleString()}</span>`,
|
|
1653
|
+
],
|
|
1654
|
+
[
|
|
1655
|
+
'dropped',
|
|
1656
|
+
dropped.length === 0
|
|
1657
|
+
? '<span class="good">0</span>'
|
|
1658
|
+
: `<span class="bad">${dropped.map(([n, c]) => `${n} ${c.toLocaleString()}`).join(', ')}</span>`,
|
|
1659
|
+
],
|
|
1660
|
+
['claim words', capacities.claim_word_capacity.toLocaleString()],
|
|
1661
|
+
]);
|
|
1662
|
+
|
|
1663
|
+
const catcher = page.catcher;
|
|
1664
|
+
|
|
1665
|
+
if (catcher === undefined) {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const report = catcher.report();
|
|
1670
|
+
|
|
1671
|
+
element('catch-section').hidden = false;
|
|
1672
|
+
|
|
1673
|
+
rows('catch', [
|
|
1674
|
+
['sampled', report.frames.toLocaleString()],
|
|
1675
|
+
['threshold', `${(100 * report.threshold).toFixed(0)}%`],
|
|
1676
|
+
['worst so far', `${(100 * report.worst).toFixed(1)}%`],
|
|
1677
|
+
[
|
|
1678
|
+
'caught',
|
|
1679
|
+
`<span class="${report.events > 0 ? 'bad' : 'good'}">${report.events}</span>`,
|
|
1680
|
+
],
|
|
1681
|
+
['frozen', report.frozen ? `<span class="bad">yes</span>` : 'no'],
|
|
1682
|
+
['last at frame', report.last === undefined ? '—' : report.last.frame],
|
|
1683
|
+
]);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* @param {string} id
|
|
1688
|
+
* @param {Array<[string, string|number]>} entries
|
|
1689
|
+
*/
|
|
1690
|
+
function rows(id, entries) {
|
|
1691
|
+
element(id).innerHTML = entries
|
|
1692
|
+
.map(([name, value]) => `<div class="row"><span>${name}</span><span>${value}</span></div>`)
|
|
1693
|
+
.join('');
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* A manifest's source name lands in the panel's markup, so it is a string from outside the page in
|
|
1698
|
+
* the one place a string from outside the page can do something.
|
|
1699
|
+
*
|
|
1700
|
+
* @param {string} text
|
|
1701
|
+
* @returns {string}
|
|
1702
|
+
*/
|
|
1703
|
+
function escape_html(text) {
|
|
1704
|
+
return text.replace(/[&<>"]/g, character => ({
|
|
1705
|
+
'&': '&', '<': '<', '>': '>', '"': '"',
|
|
1706
|
+
})[character]);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
start().catch(error => {
|
|
1710
|
+
element('status').textContent = error.message ?? String(error);
|
|
1711
|
+
element('status').classList.add('error');
|
|
1712
|
+
|
|
1713
|
+
globalThis.__state = () => ({ error: error.message ?? String(error) });
|
|
1714
|
+
|
|
1715
|
+
console.error(error);
|
|
1716
|
+
});
|