From 6e263fd60e7bcb718790aee418b765e4981efcd3 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Mon, 7 Sep 2026 21:08:44 +0100 Subject: [PATCH 1/2] refactor: remove distant terrain LOD and retain full-detail streaming Signed-off-by: MichaelFisher1997 --- .agents/skills/test-writer/SKILL.md | 1 - .github/workflows/benchmark.yml | 83 +- .github/workflows/build.yml | 38 +- .gitignore | 2 - AGENTS.md | 13 +- CONTRIBUTING.md | 7 +- README.md | 15 +- assets/config/presets.json | 10 - assets/shaders/vulkan/g_pass.frag | 43 +- .../shaders/vulkan/lod_compact_terrain.frag | 60 - .../vulkan/lod_compact_terrain.frag.spv | Bin 5356 -> 0 bytes .../shaders/vulkan/lod_compact_terrain.vert | 221 - assets/shaders/vulkan/lod_compact_water.frag | 73 - .../shaders/vulkan/lod_compact_water.frag.spv | Bin 5940 -> 0 bytes assets/shaders/vulkan/lod_compact_water.vert | 158 - assets/shaders/vulkan/lod_culling.comp | 90 - assets/shaders/vulkan/shadow.vert | 6 +- assets/shaders/vulkan/shadow.vert.spv | Bin 4596 -> 4220 bytes assets/shaders/vulkan/terrain.frag | 52 +- assets/shaders/vulkan/terrain.frag.spv | Bin 62660 -> 59992 bytes assets/shaders/vulkan/terrain.vert | 21 +- assets/shaders/vulkan/terrain.vert.spv | Bin 10504 -> 9292 bytes assets/shaders/vulkan/terrain_debug.frag | 1 - assets/shaders/vulkan/water.frag | 36 +- assets/shaders/vulkan/water.frag.spv | Bin 20796 -> 19156 bytes assets/shaders/vulkan/water.vert | 20 +- assets/shaders/vulkan/water.vert.spv | Bin 8800 -> 7588 bytes build.zig | 300 +- devenv.nix | 10 +- docs/benchmarks/README.md | 379 +- docs/benchmarks/acceptance-baseline.json | 16105 ---------------- docs/benchmarks/baseline.json | 16105 ---------------- docs/benchmarks/gpu-culling-baseline.json | 995 - docs/decisions/917-compact-gpu-lod-tiles.md | 165 - docs/lod-quality-controls.md | 157 - docs/lod-water-and-latency-steering-752.md | 123 - docs/shaders/spirv-sizes.json | 5 - docs/worldgen-biomes-and-terrain.md | 1 - modules/engine-core/src/job_system.zig | 32 +- modules/engine-core/src/lod_types.zig | 32 - modules/engine-core/src/root.zig | 2 - modules/engine-ecs/src/systems/render.zig | 4 +- modules/engine-graphics/src/cloud_system.zig | 16 +- modules/engine-graphics/src/render_graph.zig | 23 +- modules/engine-graphics/src/render_system.zig | 3 - modules/engine-graphics/src/rhi_tests.zig | 173 +- modules/engine-graphics/src/rhi_vulkan.zig | 57 +- modules/engine-graphics/src/root.zig | 1 - .../src/vulkan/descriptor_bindings.zig | 2 - .../src/vulkan/descriptor_manager.zig | 146 +- .../vulkan/descriptor_manager_error_tests.zig | 3 - .../src/vulkan/descriptor_manager_tests.zig | 46 - .../src/vulkan/fxaa_system.zig | 2 + .../src/vulkan/lod_culling_system.zig | 601 - .../src/vulkan/pipeline_manager.zig | 8 - .../src/vulkan/pipeline_specialized.zig | 72 - .../src/vulkan/rhi_context_factory.zig | 12 - .../src/vulkan/rhi_context_types.zig | 14 - .../src/vulkan/rhi_draw_submission.zig | 120 +- .../src/vulkan/rhi_frame_orchestration.zig | 56 - .../src/vulkan/rhi_init_deinit.zig | 7 - .../src/vulkan/rhi_pass_orchestration.zig | 1 - .../src/vulkan/rhi_render_state.zig | 91 +- .../src/vulkan/rhi_resource_setup.zig | 7 - .../src/vulkan/rhi_state_control.zig | 20 - .../engine-graphics/src/vulkan/rhi_timing.zig | 31 +- .../src/vulkan/shader_registry.zig | 5 - .../src/vulkan/vulkan_frame_tests.zig | 1 - modules/engine-input/src/interfaces.zig | 1 - modules/engine-rhi/src/culling.zig | 107 - modules/engine-rhi/src/render_settings.zig | 153 - modules/engine-rhi/src/rhi.zig | 96 +- modules/engine-rhi/src/rhi_contract_tests.zig | 13 - modules/engine-rhi/src/rhi_types.zig | 203 +- modules/engine-rhi/src/root.zig | 15 - modules/engine-rhi/src/world_contracts.zig | 23 +- modules/engine-ui/src/debug_frustum.zig | 2 +- modules/engine-ui/src/debug_menu.zig | 2 - modules/engine-ui/src/root.zig | 4 - modules/engine-ui/src/timing_overlay.zig | 147 +- modules/game-core/src/benchmark.zig | 1950 +- modules/game-core/src/block_outline.zig | 2 +- modules/game-core/src/hand_renderer.zig | 2 +- modules/game-core/src/input_mapper.zig | 11 +- modules/game-core/src/input_settings.zig | 82 +- modules/game-core/src/root.zig | 1 - modules/game-core/src/session.zig | 519 +- modules/game-core/src/settings.zig | 1 - modules/game-core/src/settings/data.zig | 25 +- .../game-core/src/settings/json_presets.zig | 19 +- .../game-core/src/settings/persistence.zig | 70 +- modules/game-core/src/settings/tests.zig | 52 +- modules/game-core/src/ui/session_hud.zig | 8 +- modules/game-ui/src/screens/graphics.zig | 45 +- modules/game-ui/src/screens/rml_settings.zig | 42 - modules/game-ui/src/screens/settings.zig | 54 +- modules/game-ui/src/screens/world.zig | 211 +- modules/game-ui/src/screens/world_debug.zig | 13 +- modules/game-ui/src/settings_ui.zig | 1 - modules/game-ui/src/settings_ui_tests.zig | 11 - modules/world-core/src/lod_data.zig | 811 - modules/world-core/src/root.zig | 12 - modules/world-lod/build.zig | 5 - modules/world-lod/build.zig.zon | 1 - .../world-lod/src/biome_color_provider.zig | 122 - modules/world-lod/src/lod_cache.zig | 682 - modules/world-lod/src/lod_cache_io.zig | 296 - modules/world-lod/src/lod_chunk.zig | 1126 -- modules/world-lod/src/lod_compact_pool.zig | 290 - modules/world-lod/src/lod_generator.zig | 20 - modules/world-lod/src/lod_geometry.zig | 1674 -- modules/world-lod/src/lod_ingest.zig | 722 - modules/world-lod/src/lod_manager.zig | 757 - .../src/lod_manager_benchmark_fixture_ops.zig | 131 - .../world-lod/src/lod_manager_cache_ops.zig | 439 - modules/world-lod/src/lod_manager_context.zig | 298 - .../world-lod/src/lod_manager_core_ops.zig | 575 - .../src/lod_manager_eviction_ops.zig | 525 - .../src/lod_manager_generation_ops.zig | 715 - .../src/lod_manager_ingestion_ops.zig | 406 - .../src/lod_manager_internal_tests.zig | 1495 -- modules/world-lod/src/lod_manager_tests.zig | 679 - .../world-lod/src/lod_manager_upload_ops.zig | 417 - modules/world-lod/src/lod_materials.zig | 23 - modules/world-lod/src/lod_mesh.zig | 1224 -- modules/world-lod/src/lod_mesh_resources.zig | 196 - modules/world-lod/src/lod_mesh_tests.zig | 1557 -- modules/world-lod/src/lod_renderer.zig | 3319 ---- modules/world-lod/src/lod_scheduler.zig | 753 - modules/world-lod/src/lod_seam.zig | 154 - modules/world-lod/src/lod_stats.zig | 803 - modules/world-lod/src/lod_store.zig | 611 - .../src/lod_streaming_coordinator.zig | 276 - modules/world-lod/src/lod_tile.zig | 839 - modules/world-lod/src/lod_types.zig | 15 - modules/world-lod/src/lod_upload_queue.zig | 246 - modules/world-lod/src/lod_vegetation.zig | 11 - modules/world-lod/src/lod_vertex_pool.zig | 849 - modules/world-lod/src/root.zig | 54 - modules/world-lod/src/tests.zig | 14 - modules/world-lod/src/world_lod.zig | 169 - modules/world-meshing/src/chunk_mesh.zig | 2 +- modules/world-meshing/src/chunk_storage.zig | 17 - .../src/meshing/quadric_simplifier.zig | 855 - modules/world-meshing/src/root.zig | 1 - .../src/chunk_queue_coordinator.zig | 33 - modules/world-runtime/src/world.zig | 363 +- .../world-runtime/src/world_facade_tests.zig | 651 +- modules/world-runtime/src/world_renderer.zig | 44 +- modules/world-runtime/src/world_streamer.zig | 209 +- modules/world-worldgen/src/root.zig | 1 - modules/worldgen-api/src/root.zig | 39 +- modules/worldgen-flat/src/root.zig | 49 - .../src/lod_sampling.zig | 266 - modules/worldgen-overworld-v2/src/root.zig | 82 - modules/worldgen-overworld/src/biome.zig | 3 +- .../src/biome_color_provider.zig | 4 +- .../worldgen-overworld/src/biome_selector.zig | 16 +- .../src/biome_selector_tests.zig | 154 - .../worldgen-overworld/src/biome_source.zig | 6 - modules/worldgen-overworld/src/gen_region.zig | 212 - .../worldgen-overworld/src/height_sampler.zig | 4 +- .../worldgen-overworld/src/lod_coloring.zig | 94 - .../worldgen-overworld/src/noise_sampler.zig | 11 +- .../src/overworld_generator.zig | 596 +- modules/worldgen-overworld/src/root.zig | 2 - .../src/terrain_shape_generator.zig | 6 +- modules/worldgen-overworld/src/tree_hints.zig | 287 - .../worldgen-overworld/src/world_class.zig | 6 +- modules/worldgen-overworld/src/world_map.zig | 6 - modules/worldgen-test/src/root.zig | 48 - phase5_benchmark_gate_tests.zig | 179 - scripts/benchmark_baseline.py | 688 +- scripts/check_phase5_benchmark_config.sh | 33 - scripts/check_phase5_visual_smoke.py | 541 - scripts/compare_benchmarks.sh | 2 +- scripts/run_benchmark.sh | 80 +- scripts/run_phase5_visual_smoke.sh | 117 - src/game/app.zig | 48 +- src/game/player_tests.zig | 366 +- src/game/session_tests.zig | 92 +- src/integration_test.zig | 162 +- src/interface_mock_tests.zig | 577 +- src/lod_bench_main.zig | 74 - src/tests.zig | 42 +- src/world_inline_tests.zig | 24 - src/worldgen_climate_snapshot_main.zig | 2 +- src/worldgen_tests.zig | 27 - 188 files changed, 1101 insertions(+), 70099 deletions(-) delete mode 100644 assets/shaders/vulkan/lod_compact_terrain.frag delete mode 100644 assets/shaders/vulkan/lod_compact_terrain.frag.spv delete mode 100644 assets/shaders/vulkan/lod_compact_terrain.vert delete mode 100644 assets/shaders/vulkan/lod_compact_water.frag delete mode 100644 assets/shaders/vulkan/lod_compact_water.frag.spv delete mode 100644 assets/shaders/vulkan/lod_compact_water.vert delete mode 100644 assets/shaders/vulkan/lod_culling.comp delete mode 100644 docs/benchmarks/acceptance-baseline.json delete mode 100644 docs/benchmarks/baseline.json delete mode 100644 docs/benchmarks/gpu-culling-baseline.json delete mode 100644 docs/decisions/917-compact-gpu-lod-tiles.md delete mode 100644 docs/lod-quality-controls.md delete mode 100644 docs/lod-water-and-latency-steering-752.md delete mode 100644 modules/engine-core/src/lod_types.zig delete mode 100644 modules/engine-graphics/src/vulkan/lod_culling_system.zig delete mode 100644 modules/world-core/src/lod_data.zig delete mode 100644 modules/world-lod/build.zig delete mode 100644 modules/world-lod/build.zig.zon delete mode 100644 modules/world-lod/src/biome_color_provider.zig delete mode 100644 modules/world-lod/src/lod_cache.zig delete mode 100644 modules/world-lod/src/lod_cache_io.zig delete mode 100644 modules/world-lod/src/lod_chunk.zig delete mode 100644 modules/world-lod/src/lod_compact_pool.zig delete mode 100644 modules/world-lod/src/lod_generator.zig delete mode 100644 modules/world-lod/src/lod_geometry.zig delete mode 100644 modules/world-lod/src/lod_ingest.zig delete mode 100644 modules/world-lod/src/lod_manager.zig delete mode 100644 modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_cache_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_context.zig delete mode 100644 modules/world-lod/src/lod_manager_core_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_eviction_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_generation_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_ingestion_ops.zig delete mode 100644 modules/world-lod/src/lod_manager_internal_tests.zig delete mode 100644 modules/world-lod/src/lod_manager_tests.zig delete mode 100644 modules/world-lod/src/lod_manager_upload_ops.zig delete mode 100644 modules/world-lod/src/lod_materials.zig delete mode 100644 modules/world-lod/src/lod_mesh.zig delete mode 100644 modules/world-lod/src/lod_mesh_resources.zig delete mode 100644 modules/world-lod/src/lod_mesh_tests.zig delete mode 100644 modules/world-lod/src/lod_renderer.zig delete mode 100644 modules/world-lod/src/lod_scheduler.zig delete mode 100644 modules/world-lod/src/lod_seam.zig delete mode 100644 modules/world-lod/src/lod_stats.zig delete mode 100644 modules/world-lod/src/lod_store.zig delete mode 100644 modules/world-lod/src/lod_streaming_coordinator.zig delete mode 100644 modules/world-lod/src/lod_tile.zig delete mode 100644 modules/world-lod/src/lod_types.zig delete mode 100644 modules/world-lod/src/lod_upload_queue.zig delete mode 100644 modules/world-lod/src/lod_vegetation.zig delete mode 100644 modules/world-lod/src/lod_vertex_pool.zig delete mode 100644 modules/world-lod/src/root.zig delete mode 100644 modules/world-lod/src/tests.zig delete mode 100644 modules/world-lod/src/world_lod.zig delete mode 100644 modules/world-meshing/src/meshing/quadric_simplifier.zig delete mode 100644 modules/worldgen-overworld-v2/src/lod_sampling.zig delete mode 100644 modules/worldgen-overworld/src/lod_coloring.zig delete mode 100644 modules/worldgen-overworld/src/tree_hints.zig delete mode 100644 phase5_benchmark_gate_tests.zig delete mode 100644 scripts/check_phase5_benchmark_config.sh delete mode 100644 scripts/check_phase5_visual_smoke.py delete mode 100644 scripts/run_phase5_visual_smoke.sh delete mode 100644 src/lod_bench_main.zig diff --git a/.agents/skills/test-writer/SKILL.md b/.agents/skills/test-writer/SKILL.md index 60007201..bdef15be 100644 --- a/.agents/skills/test-writer/SKILL.md +++ b/.agents/skills/test-writer/SKILL.md @@ -41,7 +41,6 @@ modules/ world-core/ # chunk, block, block_registry, coordinates, lighting world-worldgen/ # terrain generation, biomes, caves, decorations world-meshing/ # chunk_storage, chunk_mesh, meshing/, GPU block buffers - world-lod/ # LOD chunks, meshes, scheduler, renderer, manager world-runtime/ # world facade, streaming, renderer, mutation, GPU mesher world-persistence/# level data, region files, save manager src/ diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index ae340cf9..7ac1d72b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -14,11 +14,6 @@ on: description: Benchmark duration per preset (seconds) required: false default: "60" - capture_gpu_culling: - description: Capture the bounded gpu-culling-scale extreme/traversal CPU-vs-GPU source pair - required: false - type: boolean - default: false concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -104,7 +99,7 @@ jobs: name: Benchmark timeout: ${{ github.event_name == 'schedule' && '55m' || github.event_name == 'workflow_dispatch' && '55m' || '20m' }} log-file: benchmark.log - command: mkdir -p "$MESA_SHADER_CACHE_DIR" && devenv shell --profile graphics -- bash scripts/run_benchmark.sh --duration "$BENCHMARK_DURATION" --presets low,medium,high --scenarios stationary,traversal,rapid-turn,teleport-eviction --compact-modes off,auto --benchmark-world overworld --output-dir benchmark-results --per-preset-timeout 600 + command: mkdir -p "$MESA_SHADER_CACHE_DIR" && devenv shell --profile graphics -- bash scripts/run_benchmark.sh --duration "$BENCHMARK_DURATION" --presets low,medium,high --scenarios stationary,traversal,rapid-turn,teleport-eviction --benchmark-world overworld --output-dir benchmark-results --per-preset-timeout 600 env: XDG_RUNTIME_DIR: /tmp/runtime-runner WAYLAND_DISPLAY: headless @@ -114,58 +109,13 @@ jobs: # PR/push runs are bounded canaries. Scheduled and manually requested # captures are longer acceptance evidence, not synthetic fixtures. BENCHMARK_DURATION: ${{ github.event_name == 'schedule' && '60' || github.event_name == 'workflow_dispatch' && github.event.inputs.duration || '5' }} - ZIGCRAFT_BENCHMARK_GPU_ADAPTER: Lavapipe (Mesa software Vulkan) - ZIGCRAFT_BENCHMARK_GPU_DRIVER: Mesa Lavapipe supplied by the pinned devenv profile - ZIGCRAFT_BENCHMARK_RUNNER: blacksmith-2vcpu-ubuntu-2404 GitHub Actions runner - ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN: Zig 0.16.0 supplied by the pinned devenv inputs - - - name: Run Phase 5 long-session streaming stress gate - if: steps.gate.outputs.run == 'true' - uses: ./.github/actions/run-with-log - with: - name: Phase 5 Streaming Stress - timeout: 15m - log-file: phase5-streaming-stress.log - command: devenv shell --profile unit -- zig build phase5-stress-gate -Dphase5-stress-iterations="$PHASE5_STRESS_ITERATIONS" - env: - # The scheduled run is longer by operation count, never by a flaky - # elapsed-time threshold. PR feedback stays bounded. - PHASE5_STRESS_ITERATIONS: ${{ github.event_name == 'schedule' && '768' || '64' }} - - name: Validate compact benchmark evidence matrix + - name: Validate benchmark results if: steps.gate.outputs.run == 'true' - run: python3 scripts/benchmark_baseline.py validate-compact-matrix benchmark-results - - - name: Capture dedicated CPU-vs-GPU culling sources - # Scheduled captures always retain a fresh pair. Manual runs opt in so - # ordinary canaries stay fast; each source is independently bounded. - if: steps.gate.outputs.run == 'true' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.capture_gpu_culling == 'true')) - uses: ./.github/actions/run-with-log - with: - name: GPU Culling Baseline Sources - timeout: 32m - log-file: gpu-culling-benchmark.log - command: mkdir -p "$MESA_SHADER_CACHE_DIR" && devenv shell --profile graphics -- bash -c 'scripts/run_benchmark.sh --duration 60 --presets extreme --scenarios traversal --compact-modes auto --gpu-culling off --gpu-culling-threshold 128 --benchmark-fixture gpu-culling-scale --benchmark-horizon-distance 4096 --benchmark-lod-memory-budget-mb 2048 --benchmark-require-gpu-candidates 1024 --benchmark-world flat --output-dir gpu-culling-results/cpu --per-preset-timeout 900 && scripts/run_benchmark.sh --duration 60 --presets extreme --scenarios traversal --compact-modes auto --gpu-culling on --gpu-culling-threshold 128 --benchmark-fixture gpu-culling-scale --benchmark-horizon-distance 4096 --benchmark-lod-memory-budget-mb 2048 --benchmark-require-gpu-candidates 1024 --benchmark-world flat --output-dir gpu-culling-results/gpu --per-preset-timeout 900' - env: - XDG_RUNTIME_DIR: /tmp/runtime-runner - WAYLAND_DISPLAY: headless - MESA_SHADER_CACHE_DIR: /tmp/mesa_shader_cache - SDL_AUDIODRIVER: dummy - ZIGCRAFT_SAFE_MODE: "1" - ZIGCRAFT_BENCHMARK_GPU_ADAPTER: Lavapipe (Mesa software Vulkan) - ZIGCRAFT_BENCHMARK_GPU_DRIVER: Mesa Lavapipe supplied by the pinned devenv profile - ZIGCRAFT_BENCHMARK_RUNNER: blacksmith-2vcpu-ubuntu-2404 GitHub Actions runner - ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN: Zig 0.16.0 supplied by the pinned devenv inputs - - - name: Validate fresh CPU-vs-GPU culling pair - if: steps.gate.outputs.run == 'true' && (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.capture_gpu_culling == 'true')) run: | - python3 scripts/benchmark_baseline.py assemble-gpu-culling --overwrite \ - --output gpu-culling-results/gpu-culling-baseline.json \ - gpu-culling-results/cpu/auto/extreme/traversal.json \ - gpu-culling-results/gpu/auto/extreme/traversal.json - python3 scripts/benchmark_baseline.py validate-gpu-culling \ - gpu-culling-results/gpu-culling-baseline.json + for result in benchmark-results/*/*.json; do + bash scripts/validate_benchmark_artifact.sh --result "$result" + done - name: Stop headless Wayland compositor if: always() && steps.gate.outputs.run == 'true' @@ -179,20 +129,6 @@ jobs: exit 1 fi - - name: Compare against baseline - # A 5-second canary must not be numerically compared with a 60-second - # acceptance capture. The compatibility command also enforces this. - if: steps.gate.outputs.run == 'true' && github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' - id: compare - run: | - if ! python3 scripts/benchmark_baseline.py compatibility docs/benchmarks/baseline.json benchmark-results/auto/low/stationary.json --preset low --scenario stationary; then - printf 'Skipping numeric baseline comparison: checked reference hardware/provenance differs from this CI runner. Matrix, SLO, validation, and Bencher gates still apply.\n' - exit 0 - fi - for preset in low medium high; do for scenario in stationary traversal rapid-turn teleport-eviction; do - bash scripts/compare_benchmarks.sh docs/benchmarks/baseline.json "benchmark-results/auto/${preset}/${scenario}.json" --preset "$preset" --scenario "$scenario" - done; done - - name: Install Bencher CLI if: steps.gate.outputs.run == 'true' && env.BENCHER_API_TOKEN != '' && env.BENCHER_PROJECT != '' uses: bencherdev/bencher@30a740a2e4246560b1a5fd424057d84aa2b188d6 @@ -210,7 +146,7 @@ jobs: --token "$BENCHER_API_TOKEN" \ --adapter json \ --err \ - "cat benchmark-results/auto/${preset}/${scenario}.json" + "cat benchmark-results/${preset}/${scenario}.json" done; done - name: Comment Bencher dashboard @@ -232,10 +168,7 @@ jobs: name: benchmark-results path: | benchmark-results - gpu-culling-results benchmark.log - gpu-culling-benchmark.log - phase5-streaming-stress.log weston.log retention-days: 30 @@ -244,12 +177,10 @@ jobs: uses: actions/github-script@v9 env: BENCHMARK_RUN_OUTCOME: ${{ steps.run_benchmark.outcome }} - BENCHMARK_COMPARE_OUTCOME: ${{ steps.compare.outcome }} with: script: | const runOutcome = process.env.BENCHMARK_RUN_OUTCOME; - const compareOutcome = process.env.BENCHMARK_COMPARE_OUTCOME; - const failed = runOutcome === 'failure' || compareOutcome === 'failure'; + const failed = runOutcome === 'failure'; const description = failed ? 'Benchmark regression or runtime failure' : 'Benchmark completed'; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23f96ce8..dbee6b79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -200,14 +200,6 @@ jobs: log-file: unit-test-${{ matrix.optimize }}.log command: devenv shell --profile unit -- zig build -Doptimize=${{ matrix.optimize }} test - - name: Run Phase 5 compact LOD gate - if: needs.changes.outputs.code_changes == 'true' && matrix.optimize == 'Debug' - run: devenv shell --profile unit -- zig build phase5-gate - - - name: Run bounded Phase 5 streaming stress gate - if: needs.changes.outputs.code_changes == 'true' && matrix.optimize == 'Debug' - run: devenv shell --profile unit -- zig build phase5-stress-gate -Dphase5-stress-iterations=64 - - name: Upload unit test log if: failure() uses: actions/upload-artifact@v7 @@ -291,38 +283,15 @@ jobs: ZIGCRAFT_SMOKE_FRAMES: "3" ZIGCRAFT_SAFE_MODE: "1" - - name: Run Phase 5 production visual-motion and saved-world gate - if: needs.changes.outputs.code_changes == 'true' - uses: ./.github/actions/run-with-log - with: - name: Phase 5 Visual Motion and Saved-World Reload Gate - timeout: 35m - log-file: phase5-visual-gate.log - command: devenv shell --profile graphics -- zig build phase5-visual-gate - env: - XDG_RUNTIME_DIR: /tmp/runtime-runner - WAYLAND_DISPLAY: headless - - - name: Upload Phase 5 visual evidence - if: always() && needs.changes.outputs.code_changes == 'true' - uses: actions/upload-artifact@v7 - with: - name: phase5-visual-evidence - path: | - zig-out/phase5-visual-smoke/ - phase5-visual-gate.log - if-no-files-found: ignore - retention-days: 14 - - name: Fail on Vulkan validation log errors if: needs.changes.outputs.code_changes == 'true' run: | set -euo pipefail - if rg -n -e 'Vulkan validation error|Validation Error:|VUID-' integration-test.log world-smoke-test.log phase5-visual-gate.log; then - echo "Vulkan validation errors were found in integration or Phase 5 visual logs." >&2 + if rg -n -e 'Vulkan validation error|Validation Error:|VUID-' integration-test.log world-smoke-test.log; then + echo "Vulkan validation errors were found in integration logs." >&2 exit 1 fi - echo "No Vulkan validation errors found in integration or Phase 5 visual logs." + echo "No Vulkan validation errors found in integration logs." - name: Stop headless Wayland compositor if: always() && needs.changes.outputs.code_changes == 'true' @@ -336,7 +305,6 @@ jobs: path: | integration-test.log world-smoke-test.log - phase5-visual-gate.log weston.log if-no-files-found: ignore retention-days: 7 diff --git a/.gitignore b/.gitignore index 66a6b269..a5b42a3e 100644 --- a/.gitignore +++ b/.gitignore @@ -30,8 +30,6 @@ zigcraft-minidumps/ *.spv !assets/shaders/vulkan/lpv_inject.comp.spv !assets/shaders/vulkan/lpv_propagate.comp.spv -!assets/shaders/vulkan/lod_compact_terrain.frag.spv -!assets/shaders/vulkan/lod_compact_water.frag.spv !assets/shaders/vulkan/water.frag.spv !assets/shaders/vulkan/water.vert.spv wiki/ diff --git a/AGENTS.md b/AGENTS.md index b7822dc8..2dd81a37 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ devenv shell zig fmt src/ modules/ devenv shell zig fmt --check src/ modules/ ``` -- `zig build test` is the broad suite: aggregate/module tests, fuzz roots, shader compilation/validation, SPIR-V size checks, shadow ABI checks, and Phase 5 policy tests. +- `zig build test` is the broad suite: aggregate/module tests, fuzz roots, shader compilation/validation, SPIR-V size checks, and shadow ABI checks. ```bash devenv shell zig build test devenv shell zig build test -Dtest-filter="name" @@ -25,11 +25,9 @@ ```bash devenv shell zig build test-integration -Dskip-present=true devenv shell zig build test-robustness - devenv shell zig build phase5-gate - devenv shell zig build phase5-visual-gate ``` - `-Dskip-present=true` only suppresses presentation; SDL/Vulkan initialization still needs a display/compositor and driver. Use the repo skills `headless-crash-test`, `headless-screenshot`, `headless-benchmark`, or `headless-graphics-verification`, and always bound game/graphics commands with a timeout. -- For deterministic startup checks, combine `-Dskip-present`, `-Dauto-world=`, and `-Dstartup-diagnostic-seconds=N`. `-Dchunk-debug-mode` disables LOD, water, caves, and decorations; selectively restore `lod,water,watergen,waterrender,caves,decorations` with `-Dchunk-debug-enable=`. +- For deterministic startup checks, combine `-Dskip-present`, `-Dauto-world=`, and `-Dstartup-diagnostic-seconds=N`. `-Dchunk-debug-mode` disables water, caves, and decorations; selectively restore `water,watergen,waterrender,caves,decorations` with `-Dchunk-debug-enable=`. ## Benchmarks and generated artifacts @@ -40,7 +38,7 @@ -Dbenchmark-duration=60 -Dbenchmark-output=zig-out/benchmark-low.json ``` Scenarios are `stationary`, `traversal`, `rapid-turn`, and `teleport-eviction`. Prefer the `headless-benchmark` skill for bounded runs. -- Focused CPU-only tools: `devenv shell zig build worldgen-report` and `devenv shell zig build lod-bench`. Pass climate snapshot arguments after `--`, e.g. `devenv shell zig build worldgen-climate-snapshot -- --seed 42 ...`. +- Focused CPU-only tools: `devenv shell zig build worldgen-report` and `devenv shell zig build worldgen-climate-snapshot`. Pass climate snapshot arguments after `--`, e.g. `devenv shell zig build worldgen-climate-snapshot -- --seed 42 ...`. - Building/tests compile GLSL and write tracked `*.spv` files beside sources in `assets/shaders/vulkan/`. After intentional shader-size changes, run `./scripts/update_spirv_baseline.sh`; `docs/shaders/spirv-sizes.json` and shadow runtime SPIR-V parity are test-enforced. - New/changed textures go through `./scripts/process_textures.sh 512`; preserve licensing/attribution for placeholder assets. @@ -48,20 +46,19 @@ - `src/main.zig` and `src/game/app.zig` wire the executable. Reusable session/player/settings/benchmark logic belongs in `modules/game-core`; screens belong in `modules/game-ui`. - `engine-rhi` owns rendering contracts and opaque handles; `engine-graphics` owns the Vulkan backend, render passes, resources, and pipelines. Vulkan is currently the only backend, so interface changes usually require both the RHI vtable/contracts and Vulkan implementation. -- `world-core` owns shared block/chunk/coordinate types; world generation is split across `world-worldgen-*` packages; `world-meshing` owns chunk mesh/storage; `world-lod` owns distant terrain; `world-runtime` coordinates streaming, lighting, GPU meshing, mutation, and rendering; `world-persistence` owns saves/regions. +- `world-core` owns shared block/chunk/coordinate types; world generation is split across `world-worldgen-*` packages; `world-meshing` owns chunk mesh/storage; `world-runtime` coordinates streaming, lighting, GPU meshing, mutation, and rendering; `world-persistence` owns saves/regions. - Never call RHI/SDL from worker jobs. Pin chunks while background jobs hold them and synchronize shared mesh/chunk state with the project mutex abstraction. - Convert global coordinates with `worldToChunk`/`worldToLocal` from `world-core`; chunk coordinates are floor-divided, so ad-hoc truncating division is wrong for negative positions. - GPU-shared structs and shader blocks are ABI contracts. Use explicit layouts (`extern`/packed as appropriate), assert sizes/offsets, and update CPU, GLSL, descriptors, and backend bindings together. ## Graphics hazards -- `ZIGCRAFT_LOD_COMPACT=auto` is qualified only through immutable per-layer descriptors plus expanded fallback for unsupported shoreline topology; `off` remains the explicit supported fallback. `force` is diagnostic only. Do not weaken the saved-world RADV reload, validation, robustness, and Phase 5 qualification requirements based only on generated-world captures. - Dedicated transfer queues cannot upload exclusive graphics buffers safely with semaphore synchronization alone; queue-family release/acquire ownership transfers are required. Until implemented, keep these uploads on the graphics family. - A descriptor set update affects all previously recorded commands that bind that set when execution happens. Terrain/water or direct/indirect streams that need different buffers require immutable/per-layer descriptor sets, not sequential updates to one set. ## Verification and CI -- Match verification to the change, but graphics/RHI/shader/runtime work normally requires: format check, `zig build test`, ReleaseFast build, integration/robustness as relevant, and the appropriate headless graphics skill. CI additionally runs Debug and ReleaseSafe tests, `phase5-gate`, Lavapipe/Weston integration smoke tests, and `phase5-visual-gate`. +- Match verification to the change, but graphics/RHI/shader/runtime work normally requires: format check, `zig build test`, ReleaseFast build, integration/robustness as relevant, and the appropriate headless graphics skill. CI additionally runs Debug and ReleaseSafe tests and Lavapipe/Weston integration smoke tests. - Install the repo pre-push hook with `./scripts/setup-hooks.sh`, but do not treat it as CI parity: it formats only `src/` and omits ReleaseSafe and graphics jobs. ## Git workflow diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4f3641e..82b240ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,7 +104,7 @@ dev <- Staging branch for integrated features | Branch Type | Purpose | Merge Flow | Examples | |-------------|---------|-------------|----------| -| `feature/*` | New features, enhancements | `feature -> dev -> main` | `feature/lod-system` | +| `feature/*` | New features, enhancements | `feature -> dev -> main` | `feature/terrain-improvements` | | `bug/*` | Non-critical bugs | `bug -> dev -> main` | `bug/rendering-artifact` | | `hotfix/*` | Critical bugs (crashes, data loss) | `hotfix -> dev -> main` | `hotfix/crash-on-load` | | `ci/*` | CI/workflow changes | `ci -> dev -> main` | `ci/update-runner` | @@ -113,7 +113,7 @@ dev <- Staging branch for integrated features ### Branch Naming Guidelines - Use **kebab-case** for branch names -- Be descriptive: `feature/lod-system`, `bug/chunk-leak`, `hotfix/save-corruption` +- Be descriptive: `feature/terrain-improvements`, `bug/chunk-leak`, `hotfix/save-corruption` - No strict format required (issue numbers optional) - CI branches: `ci/` prefix for `.github/` changes @@ -155,7 +155,7 @@ devenv shell zig build test Use conventional commits for clear commit messages and PR titles. Allowed types are `feat`, `fix`, `refactor`, `test`, `docs`, `ci`, `chore`, `perf`, `build`, `style`, and `revert`. ``` -feat: add LOD system for distant terrain +feat: add terrain generation improvements fix: resolve chunk mesh memory leak ci: update runner configuration for faster builds refactor: extract lighting calculation to separate module @@ -319,7 +319,6 @@ modules/ world-core/ # Blocks, chunks, coordinates, and light packing world-worldgen/ # Terrain generation, biomes, caves, decorations, generator registry world-meshing/ # Chunk storage, chunk mesh generation, GPU block buffers - world-lod/ # Distant terrain LOD chunks, meshes, scheduler, renderer, manager world-runtime/ # World facade, streamer, renderer, mutation, GPU meshing runtime world-persistence/# Level data, region files, chunk serialization, save manager src/ diff --git a/README.md b/README.md index 58fd188c..98112082 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ |________/|______/ \______/ \______/ |__/ |__/|__/ |__/|__/ |__/ ``` - ZigCraft distant voxel landscape with LOD terrain and forests + Historical ZigCraft terrain landscape screenshot # ⚡ ZigCraft ⚡ @@ -42,7 +42,6 @@ ### 🌍 World Generation - **Biomes & Climate**: Multi-noise system based on temperature and humidity (11+ biomes). - **Infinite Terrain**: Seed-based, deterministic generation with domain warping and 3D caves. -- **Level of Detail (LOD)**: Hierarchical LOD system enabling 100+ chunk render distances using simplified terrain meshes and specialized rendering. - **Greedy Meshing**: Optimized vertex data generation for maximum throughput. ### 🛠️ Engine Core @@ -118,13 +117,11 @@ To bypass in emergencies: `git push --no-verify` - **Shadow/cave lighting capture**: `./scripts/capture_shadow_test.sh screenshots/shadow-test.png` `-Dchunk-debug-mode` strips the overworld down to basic chunks for isolation work: -- LOD off by default - water generation/rendering off by default - caves off by default - decorations/features off by default Re-enable individual systems with `-Dchunk-debug-enable=` using a comma-separated list: -- `lod` - `water` - `watergen` - `waterrender` @@ -134,14 +131,11 @@ Re-enable individual systems with `-Dchunk-debug-enable=` using a comma-separate Examples: ```bash -# LOD only -devenv shell zig build run -Dchunk-debug-mode -Dchunk-debug-enable=lod -Dauto-world=normal - -# LOD plus cave generation -devenv shell zig build run -Dchunk-debug-mode -Dchunk-debug-enable=lod,caves -Dauto-world=normal +# Water plus cave generation +devenv shell zig build run -Dchunk-debug-mode -Dchunk-debug-enable=water,caves -Dauto-world=normal # Headless startup comparison after 5 seconds -devenv shell zig build run -Dchunk-debug-mode -Dchunk-debug-enable=lod,water,caves -Dauto-world=normal -Dstartup-diagnostic-seconds=5 -Dskip-present +devenv shell zig build run -Dchunk-debug-mode -Dchunk-debug-enable=water,caves -Dauto-world=normal -Dstartup-diagnostic-seconds=5 -Dskip-present ``` The shadow/cave lighting capture launches a deterministic low-block test scene, applies a small shadow-focused graphics preset, waits 5 seconds after the target is ready, captures a PNG, and exits. It defaults to a `dug-cave` variant that matches a player-dug dirt/grass cave mouth. Use `ZIGCRAFT_SHADOW_TEST_VARIANT=bend ./scripts/capture_shadow_test.sh screenshots/shadow-bend.png` to check the older bend/deep-black regression. Override the wait with `ZIGCRAFT_SCREENSHOT_DELAY_SECONDS=8 ./scripts/capture_shadow_test.sh screenshots/shadow-test.png`. Screenshot paths are restricted to image extensions from `image/png`, `image/jpeg`, `image/gif`, and `image/webp`; the built-in encoder currently writes PNG. @@ -157,7 +151,6 @@ The shadow/cave lighting capture launches a deterministic low-block test scene, - `modules/world-core`: Blocks, chunks, coordinates, lighting, and shared world types. - `modules/world-worldgen`: Procedural terrain, noise, biomes, caves, decorations, and generator registry. - `modules/world-meshing`: Chunk storage, mesh generation, GPU block buffers, and meshing helpers. -- `modules/world-lod`: Distant terrain LOD data, scheduling, rendering, and management. - `modules/world-runtime`: World facade, streaming, mutation, rendering, and GPU meshing runtime. - `modules/world-persistence`: Level data, chunk serialization, region files, and save manager. - `src/game/`: Application/gameplay state, screens, player, inventory, and session logic. diff --git a/assets/config/presets.json b/assets/config/presets.json index 3387959e..f72dad7d 100644 --- a/assets/config/presets.json +++ b/assets/config/presets.json @@ -25,9 +25,7 @@ "ssao_enabled": false, "clouds_enabled": false, "clouds_3d_enabled": false, - "lod_enabled": true, "render_distance": 6, - "render_distance_preset": 0, "fxaa_enabled": true, "bloom_enabled": false, "bloom_intensity": 0.3 @@ -54,9 +52,7 @@ "volumetric_steps": 8, "volumetric_scattering": 0.7, "ssao_enabled": true, - "lod_enabled": true, "render_distance": 10, - "render_distance_preset": 1, "fxaa_enabled": true, "bloom_enabled": true, "bloom_intensity": 0.25 @@ -83,9 +79,7 @@ "volumetric_steps": 12, "volumetric_scattering": 0.75, "ssao_enabled": true, - "lod_enabled": true, "render_distance": 12, - "render_distance_preset": 2, "fxaa_enabled": true, "bloom_enabled": true, "bloom_intensity": 0.3 @@ -112,9 +106,7 @@ "volumetric_steps": 16, "volumetric_scattering": 0.8, "ssao_enabled": true, - "lod_enabled": true, "render_distance": 14, - "render_distance_preset": 3, "fxaa_enabled": true, "bloom_enabled": true, "bloom_intensity": 0.4 @@ -141,9 +133,7 @@ "volumetric_steps": 16, "volumetric_scattering": 0.8, "ssao_enabled": true, - "lod_enabled": true, "render_distance": 16, - "render_distance_preset": 4, "fxaa_enabled": true, "bloom_enabled": true, "bloom_intensity": 0.45 diff --git a/assets/shaders/vulkan/g_pass.frag b/assets/shaders/vulkan/g_pass.frag index 14d1fa36..23bb6334 100644 --- a/assets/shaders/vulkan/g_pass.frag +++ b/assets/shaders/vulkan/g_pass.frag @@ -9,7 +9,6 @@ layout(location = 9) in vec3 vTangent; layout(location = 10) in vec3 vBitangent; layout(location = 12) in vec4 vClipPosCurrent; layout(location = 13) in vec4 vClipPosPrev; -layout(location = 14) in float vMaskRadius; layout(location = 0) out vec3 outNormal; layout(location = 1) out vec2 outVelocity; @@ -36,40 +35,22 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { vec4 lpv_origin; } global; -const float LOD_CHUNK_SIZE = 16.0; - -bool shouldDiscardLODFragment(float encodedMaskRadius, vec2 cameraRelativeXZ) { - float maskRadius = abs(encodedMaskRadius); - if (maskRadius < 1.0) return false; - - bool readyDiskMask = encodedMaskRadius < 0.0; - vec2 cameraChunkLocal = mod(global.cam_pos.xz, LOD_CHUNK_SIZE); - vec2 chunkDelta = floor((cameraRelativeXZ + cameraChunkLocal) / LOD_CHUNK_SIZE); - float detailRadiusChunks = floor(maskRadius / LOD_CHUNK_SIZE) + (readyDiskMask ? 0.0 : 2.0); - return dot(chunkDelta, chunkDelta) <= detailRadiusChunks * detailRadiusChunks; -} - void main() { - bool isLOD = vTileID < 0 || abs(vMaskRadius) > 0.0; - if (shouldDiscardLODFragment(vMaskRadius, vFragPosWorld.xz)) discard; - vec3 N = normalize(vNormal); - if (!isLOD) { - // Calculate UV coordinates in atlas - vec2 atlasSize = vec2(16.0, 16.0); - vec2 tileSize = 1.0 / atlasSize; - vec2 tilePos = vec2(mod(float(vTileID), atlasSize.x), floor(float(vTileID) / atlasSize.x)); - vec2 tiledUV = fract(vTexCoord); - tiledUV = clamp(tiledUV, 0.001, 0.999); - vec2 uv = (tilePos + tiledUV) * tileSize; + // Calculate UV coordinates in atlas + vec2 atlasSize = vec2(16.0, 16.0); + vec2 tileSize = 1.0 / atlasSize; + vec2 tilePos = vec2(mod(float(vTileID), atlasSize.x), floor(float(vTileID) / atlasSize.x)); + vec2 tiledUV = fract(vTexCoord); + tiledUV = clamp(tiledUV, 0.001, 0.999); + vec2 uv = (tilePos + tiledUV) * tileSize; - if (texture(uTexture, uv).a < 0.1) discard; + if (texture(uTexture, uv).a < 0.1) discard; - if (global.lighting.z > 0.5 && global.pbr_params.x > 1.5) { - vec3 normalMapValue = texture(uNormalMap, uv).rgb * 2.0 - 1.0; - mat3 TBN = mat3(normalize(vTangent), normalize(vBitangent), N); - N = normalize(TBN * normalMapValue); - } + if (global.lighting.z > 0.5 && global.pbr_params.x > 1.5) { + vec3 normalMapValue = texture(uNormalMap, uv).rgb * 2.0 - 1.0; + mat3 TBN = mat3(normalize(vTangent), normalize(vBitangent), N); + N = normalize(TBN * normalMapValue); } // Convert normal from [-1, 1] to [0, 1] for storage in UNORM texture diff --git a/assets/shaders/vulkan/lod_compact_terrain.frag b/assets/shaders/vulkan/lod_compact_terrain.frag deleted file mode 100644 index 9a7b6496..00000000 --- a/assets/shaders/vulkan/lod_compact_terrain.frag +++ /dev/null @@ -1,60 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 vColor; -layout(location = 1) flat in vec3 vNormal; -layout(location = 4) in float vDistance; -layout(location = 5) in float vSkyLight; -layout(location = 6) in vec3 vBlockLight; -layout(location = 7) in vec3 vFragPosWorld; -layout(location = 11) in float vAO; -layout(location = 14) in float vMaskRadius; -layout(location = 16) in float vLODFade; -layout(location = 0) out vec4 outColor; - -layout(set = 0, binding = 0) uniform Global { - mat4 view_proj; - mat4 view_proj_prev; - vec4 cam_pos; - vec4 sun_dir; - vec4 sun_color; - vec4 fog_color; - vec4 reserved0; - vec4 params; - vec4 lighting; - vec4 render_flags; - vec4 shadow_params; - vec4 pbr_params; - vec4 volumetric_params; - vec4 viewport_size; - vec4 lpv_params; - vec4 lpv_origin; -} global; - -const float LOD_CHUNK_SIZE = 16.0; - -bool shouldDiscardLODFragment(float encodedMaskRadius, vec2 cameraRelativeXZ) { - float maskRadius = abs(encodedMaskRadius); - if (maskRadius < 1.0) return false; - - bool readyDiskMask = encodedMaskRadius < 0.0; - vec2 cameraChunkLocal = mod(global.cam_pos.xz, LOD_CHUNK_SIZE); - vec2 chunkDelta = floor((cameraRelativeXZ + cameraChunkLocal) / LOD_CHUNK_SIZE); - float detailRadiusChunks = floor(maskRadius / LOD_CHUNK_SIZE) + (readyDiskMask ? 0.0 : 2.0); - return dot(chunkDelta, chunkDelta) <= detailRadiusChunks * detailRadiusChunks; -} - -void main() { - if (shouldDiscardLODFragment(vMaskRadius, vFragPosWorld.xz)) discard; - vec3 normal = normalize(vNormal); - vec3 light_dir = normalize(global.sun_dir.xyz); - float diffuse = max(dot(normal, light_dir), 0.0); - float block_light = max(vBlockLight.r, max(vBlockLight.g, vBlockLight.b)); - float illumination = clamp(max(vSkyLight * global.lighting.x, block_light) + diffuse * global.params.w * 0.45, 0.18, 1.15); - vec3 color = vColor * illumination * mix(0.72, 1.0, clamp(vAO, 0.0, 1.0)); - if (global.params.z > 0.5) { - float rawFog = clamp(1.0 - exp(-vDistance * global.params.y), 0.0, 1.0); - float fog = rawFog * rawFog * 0.72; - color = mix(color, global.fog_color.rgb, fog); - } - outColor = vec4(color, 1.0); -} diff --git a/assets/shaders/vulkan/lod_compact_terrain.frag.spv b/assets/shaders/vulkan/lod_compact_terrain.frag.spv deleted file mode 100644 index 00a69b72f462dd1144d55c0ff9cdd2f5d3ceb972..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5356 zcmb7`+m~Eb5r+@S^dvDu5+FbpXy|}KBpL%D5Qro*BxE8CWD-;mFGHvM%%qu~K4W)J z$KZtk1`{O_1ysC7<1OCt%5wSO@-OgD@X041eDku@<@cRF)j2CZxo7>dtA16xcJ11= zPiOhEn^q*rWy$JfW%7D57*{0AU=p~|e4dHC1M?ux-lI$H`Khd4Ye5lZ zB)O4!vXvc?8Rbvi(Ql^5W)?fylgOjwB5w_cJjS%It&-cU2YbvcX5ArY<-1GmnMSh% zH;8SmmUbTL7vU#{)Zoh@1)(d(@z^-Xzb@bEY>=;1&S3hR>HNKb4Pp4_8cPo zAn#SkowVIZJ2SJb+MMMb+TMWNJz8sIoVExzjJF!w;*rkKFiPK92k)$(wU!prUZ+_f znujsHH|w;Rb$T=1=2P}n0R!LKd&tkaycGrgNP!_5_U80*ZVf(bk=<|>$Do^?)4yZUVuV>=o0-s=KFsaD{cNdML>sK}ThRAC*BUON#>PEM66{6ccR4k_VjzP#QFB*&XqX#llxhT^UcZqzQmb_ z+#gGv?@jJcCC;}d_tz5V8fVRCeOC6j$JEz>tN5kyem9ZRa%SagkleePVkfk--PUN-zu}~J=X^Q-Nali^o_}t`CE_+ zUfchVk^G)L+l^os`*`o(x7^j}Z(**2%Yb>R-wdK2w+!Wr=hy<5Bqt`~T=(R9GVZze zBOA-Jxu5!E!Flcjh0XiX|6PSmUVrrm3QqgGk*#CdZjY7oeH`6*vEH83?)he!tH5Et z>T|hvQGXIW;%Q$1o2I!Zz$~Me*sj0`TH#!+{NI$ z>b|k4r}2GbuD_F2p4Fap=<|0NW0|XGeh@gTuVVJMF?8?J-^Q};Z(-=p{tlM)#wEJH zcVRc)sgf@5?^f8I&lmc=cb#X_b~h1y z-_u|XFrK!*;d7YT@lRrFKLq5gm-4@;sIpw_|0uG(Jw{yfc??+h`++w1wtn&xRQ(p_ z_XFRvZ@@TJ<_`e*gPg)RA1pX|hcP`%?DL_5b02-($9s{F+@3&o@6c<=`n*gY*7XQ@ z0?3b1Ke;+^ugyTa-#=q}$J#uD{dE+iU=A4HwM`%&KJ&=>_*TP*|6V2X;nPOeX90vy zhAbaBEh0xwuGb#650`B2udn&qgW4iy57}?R1eyB&j{)ys36#(9k-~PesL^qB*UCq% zr;v>wIeZj3^3z{?_?bAgce-&9S_Wv4k>|gHXufuDLJNpK*Yvp~X*7F>&HrE5~ z&lP?3O=N3y4n!Tlg&cL%9(A-PucB*@oWG4M7y5S!J#zUjx<2y3eGggQJ22*XW;tVc z*WU-$KE6{wK=#}hfId~`9|HOKPW`ChDT6@Gi zjH_?-gZ6(AIquIn^8Pupc^@ij`wL__M{vJHmUEBbepPVhJI(j6%KU5Kdi}L~N9OPv zv$lT|VJGl+)t*}e9IL@y_^n_bE96zomjnB4Epz)W}O+2PyKb`6m8EXx03-cacB z5%-PA<`Qu?BFov!=HXf8qQ^EP+haEaV^o>n3gqKA{nmn$cMR^UlK4Li*IrceKaNFVZ)n@IDeFw1i%G%xE z`;(6v-HB}NjpH}sE+A+8xWk>u5l6dg?YFyuHN3z|*X}Af>w6FI4D#;;#?WpK_KaMd z`Cer63VnB>$NvrX> shift) & 0xffffu; - return raw >= 0x8000u ? int(raw) - 65536 : int(raw); -} - -uint sampleIndex(uint apron_x, uint apron_z) { - uvec4 words = tileWords(); - return words.x + apron_z * (words.y + 2u) + apron_x; -} - -uvec4 sampleAtApron(uint apron_x, uint apron_z) { - return samples.sample_words[sampleIndex(apron_x, apron_z)]; -} - -float terrainHeight(uvec4 packed) { - return float(decodeSigned16(packed.x, 0u)) * (1.0 / 8.0); -} - -uint surfaceMaterial(uvec4 packed) { - return (packed.y >> 16u) & 0x7fu; -} - -// Compact tile edge bits are N/E/S/W. A clear bit is an explicit non-seamless -// contract: its apron is a local fallback, and its edge must retain a skirt. -bool hasAuthoritativeApron(uvec4 words, uint edge) { - return (words.w & (1u << edge)) != 0u; -} - -uint skirtTileEdge(uint skirt_edge) { - if (skirt_edge == 0u) return 0u; // north - if (skirt_edge == 1u) return 2u; // south - if (skirt_edge == 2u) return 3u; // west - return 1u; // east -} - -uint sampleColor(uvec4 packed) { - return (packed.z >> 5u) & 0x00ffffffu; -} - -uint skyLight(uvec4 packed) { - return ((packed.z >> 29u) & 0x7u) | ((packed.w & 0x1u) << 3u); -} - -uint blockLight(uvec4 packed) { - return (packed.w >> 1u) & 0x0fu; -} - -uint ambientOcclusion(uvec4 packed) { - return (packed.w >> 5u) & 0x3fu; -} - -vec3 fallbackMaterialColor(uint material) { - if (material == 3u) return vec3(0.32, 0.58, 0.20); // grass - if (material == 2u || material == 51u || material == 52u || material == 53u) return vec3(0.43, 0.29, 0.16); - if (material == 4u || material == 31u) return vec3(0.76, 0.66, 0.38); - if (material == 10u) return vec3(0.42, 0.41, 0.38); - if (material == 12u || material == 47u) return vec3(0.88, 0.92, 0.96); - if (material == 19u) return vec3(0.24, 0.20, 0.14); - return vec3(0.43, 0.45, 0.47); -} - -void main() { - uvec4 words = tileWords(); - vec4 params = tileParams(); - uint vertex_index = uint(gl_VertexIndex); - if (words.y < 2u) { - gl_Position = vec4(2.0, 2.0, 2.0, 1.0); - return; - } - - uint top_vertex_count = words.y * words.y; - bool is_skirt = vertex_index >= top_vertex_count; - uint x; - uint z; - uint skirt_edge = 0u; - if (!is_skirt) { - x = vertex_index % words.y; - z = vertex_index / words.y; - } else { - uint skirt_vertex = vertex_index - top_vertex_count; - skirt_edge = skirt_vertex / words.y; - uint edge_position = skirt_vertex % words.y; - if (skirt_edge >= 4u) { - gl_Position = vec4(2.0, 2.0, 2.0, 1.0); - return; - } - if (skirt_edge == 0u) { x = edge_position; z = 0u; } - else if (skirt_edge == 1u) { x = edge_position; z = words.y - 1u; } - else if (skirt_edge == 2u) { x = 0u; z = edge_position; } - else { x = words.y - 1u; z = edge_position; } - } - uvec4 packed = sampleAtApron(x + 1u, z + 1u); - float height = terrainHeight(packed); - float height_x0 = terrainHeight(sampleAtApron(x, z + 1u)); - float height_x1 = terrainHeight(sampleAtApron(x + 2u, z + 1u)); - float height_z0 = terrainHeight(sampleAtApron(x + 1u, z)); - float height_z1 = terrainHeight(sampleAtApron(x + 1u, z + 2u)); - // Do not discard an authoritative apron. Only explicit fallback edges use - // slope extrapolation, so resident tiles never pretend an invalid apron is - // a seamless normal. - if (x == 0u && !hasAuthoritativeApron(words, 3u)) height_x0 = 2.0 * height - height_x1; - if (x + 1u == words.y && !hasAuthoritativeApron(words, 1u)) height_x1 = 2.0 * height - height_x0; - if (z == 0u && !hasAuthoritativeApron(words, 0u)) height_z0 = 2.0 * height - height_z1; - if (z + 1u == words.y && !hasAuthoritativeApron(words, 2u)) height_z1 = 2.0 * height - height_z0; - - vec3 normal = normalize(vec3( - height_x0 - height_x1, - 2.0 * params.z, - height_z0 - height_z1 - )); - // The static index buffer contains all four skirts. Valid same-level edges - // collapse theirs to a degenerate strip; absent or cross-LOD neighbors keep - // only their own edge skirt, rather than relying on a universal skirt. - if (is_skirt && !hasAuthoritativeApron(words, skirtTileEdge(skirt_edge))) { - height -= params.w; - if (skirt_edge == 0u) normal = vec3(0.0, 0.0, -1.0); - else if (skirt_edge == 1u) normal = vec3(0.0, 0.0, 1.0); - else if (skirt_edge == 2u) normal = vec3(-1.0, 0.0, 0.0); - else normal = vec3(1.0, 0.0, 0.0); - } - vec3 local_pos = vec3(float(x) * params.z, height, float(z) * params.z); - vec4 world_pos = tileModel() * vec4(local_pos, 1.0); - vec4 clip_pos = global.view_proj * world_pos; - - gl_Position = clip_pos; - gl_Position.y = -gl_Position.y; - vClipPosCurrent = vec4(clip_pos.x, -clip_pos.y, clip_pos.z, clip_pos.w); - vec4 previous_clip_pos = global.view_proj_prev * world_pos; - vClipPosPrev = vec4(previous_clip_pos.x, -previous_clip_pos.y, previous_clip_pos.z, previous_clip_pos.w); - - uint color = sampleColor(packed); - vColor = vec3(color & 0xffu, (color >> 8u) & 0xffu, (color >> 16u) & 0xffu) / 255.0; - if (color == 0u) vColor = fallbackMaterialColor(surfaceMaterial(packed)); - if (is_skirt && !hasAuthoritativeApron(words, skirtTileEdge(skirt_edge))) vColor *= 0.82; - vNormal = normal; - vTexCoord = vec2(float(x), float(z)) * params.z; - // Compact samples store semantic materials, not texture-atlas tile IDs. - // The terrain fragment's color-only path is the stable far-distance ABI. - vTileID = -1; - vDistance = length(world_pos.xyz); - vSkyLight = float(skyLight(packed)) / 15.0; - vBlockLight = vec3(float(blockLight(packed)) / 15.0); - vFragPosWorld = world_pos.xyz; - vViewDepth = clip_pos.w; - vAO = float(ambientOcclusion(packed)) / 63.0; - vMaskRadius = params.x; - vCloud = 0.0; - vLODFade = clamp(params.y, 0.0, 1.0); - vTangent = normalize(vec3(1.0, 0.0, (height_x1 - height_x0) / (2.0 * params.z))); - vBitangent = normalize(cross(normal, vTangent)); -} diff --git a/assets/shaders/vulkan/lod_compact_water.frag b/assets/shaders/vulkan/lod_compact_water.frag deleted file mode 100644 index bbbffe5b..00000000 --- a/assets/shaders/vulkan/lod_compact_water.frag +++ /dev/null @@ -1,73 +0,0 @@ -#version 450 - -layout(location = 0) in vec3 vColor; -layout(location = 1) flat in vec3 vNormal; -layout(location = 2) in vec2 vTexCoord; -layout(location = 3) flat in int vTileID; -layout(location = 4) in float vDistance; -layout(location = 5) in float vSkyLight; -layout(location = 6) in vec3 vBlockLight; -layout(location = 7) in vec3 vFragPosWorld; -layout(location = 8) in float vViewDepth; -layout(location = 9) in vec4 vClipPos; -layout(location = 10) in float vMaskRadius; -layout(location = 11) in float vLODFade; - -layout(location = 0) out vec4 outColor; - -// Matches the two-chunk overlap reserved by LODConfig.calculateMaskRadius(). -const float LOD_MASK_BLEND_WIDTH = 32.0; - -layout(set = 0, binding = 0) uniform Global { - mat4 view_proj; - mat4 view_proj_prev; - vec4 cam_pos; - vec4 sun_dir; - vec4 sun_color; - vec4 fog_color; - vec4 reserved0; - vec4 params; - vec4 lighting; - vec4 render_flags; - vec4 shadow_params; - vec4 pbr_params; - vec4 volumetric_params; - vec4 viewport_size; - vec4 lpv_params; - vec4 lpv_origin; -} global; - -void main() { - float lodMaskAlpha = 1.0; - if (abs(vMaskRadius) >= 1.0) { - // A negative radius carries the outer edge of the ready detail disk; - // begin water's translucent handoff two chunks inside that edge. - bool readyDiskMask = vMaskRadius < 0.0; - float maskRadius = abs(vMaskRadius); - if (readyDiskMask) maskRadius = max(maskRadius - LOD_MASK_BLEND_WIDTH, 0.0); - float maskDistance = length(vFragPosWorld.xz); - if (maskDistance < maskRadius) discard; - // Fade the translucent LOD underlay in across the detailed-water - // overlap instead of changing its contribution at a hard circle. - lodMaskAlpha = smoothstep(maskRadius, maskRadius + LOD_MASK_BLEND_WIDTH, maskDistance); - } - // Far water deliberately avoids scene-depth, reflection, SSR, atlas, and - // thickness reads. It uses stable low-frequency waves and atmospheric fog. - float wave = sin(vFragPosWorld.x * 0.012 + global.params.x * 0.55) * - cos(vFragPosWorld.z * 0.010 - global.params.x * 0.43); - vec3 normal = normalize(vec3(wave * 0.08, 1.0, wave * 0.06)); - vec3 light_dir = normalize(-global.sun_dir.xyz); - float diffuse = max(dot(normal, light_dir), 0.0); - float light_level = clamp(max(vSkyLight, max(vBlockLight.r, max(vBlockLight.g, vBlockLight.b))), 0.35, 1.0); - vec3 base = mix(vec3(0.025, 0.16, 0.30), vec3(0.08, 0.38, 0.64), diffuse * 0.45); - base = mix(base, vColor, 0.12) * light_level; - base += global.sun_color.rgb * diffuse * global.params.w * 0.06; - - if (global.params.z > 0.5) { - float rawFog = clamp(1.0 - exp(-vDistance * global.params.y), 0.0, 1.0); - float fog = rawFog * rawFog * 0.65; - base = mix(base, global.fog_color.rgb, fog); - } - - outColor = vec4(base, 0.78 * clamp(vLODFade, 0.0, 1.0) * lodMaskAlpha); -} diff --git a/assets/shaders/vulkan/lod_compact_water.frag.spv b/assets/shaders/vulkan/lod_compact_water.frag.spv deleted file mode 100644 index 5a666caa7e7abf2ece9afc7c981e0a1dd6b5ca39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5940 zcmb7`U6@s66~|97Ga-(M3Tjz22pWYEg_;G-3@|cKql`joVU345XNDtZW>4qL9E2z# zq7O3BvJCBm66~8yv&=%L>ARq>8*jSks_Sn0eCm1n{r1`Gu%m9;&+|X;TK~1)^{#il z>)rdzp0(t~g~E#p=NIM_ephHd7Zql~6yQ42v43Fez>4~0X~nhIt<+;)VRq`M&xM7H z3vG;ItXLV>a6VY74iaz}oB*f5H24KL4SofF4Soye6Vn(ZgEX5A3WXUKXotADTsTZ^U2R2`l?qVbJYb&fp@2@61iq!%ucceO0p;0OC z9-62ncO!SOL;b8>i^o`QXlqYx?tQp8HZ+mcTbvcIPmK?iDmA!fZ0jO7oK%wEvL_~X)LI5l(K>X3MpB&`D^J!c!!2`by$iHczlo$aIaIIg zwb-%GdC1j?Mr%^=7iN4?tBl%__D~PpXm&R0x1ot?7ui_$d}Dnz8Q#@oU2h-Ujv|3lpSJW)G&VF3PWy5A zT5U0=^pmq^x(Img>Ps`- ze(EP*&(~hwgiYC}9cyhm~m(uZ+;4K6cSr6IInH|{_op0Z-Q;{3M9$V!ZhnsQj>vW8IPZnr z${go?kn7EH-UYcqIM2v?uo(D+eq~clb^F;fb^qG!Wp($iemK(^QvF1xTSM*krT#wZ z(~Row%bu_w%~PAQHd0Gq4#(Rl7d-Cm2ldy%_<`!erd#&y8jIe$H)zUCTryAk=! z>CT?m=jO~O>;vd?Us$px^g;B$Ec<=8@!pDT>?=Xo??9gW!eeVg-;RD{W-8Ttk?#U= zE=6SXY1X3I58e&)k8|>Fs2k67@EaVu``8Ost$M0E)_V{7nbW6R`MwvO{9E>W(H}W_ zwEG&WVBY(IbM(^|aSkBgd1R(F?|taR>4|v<(cPc+_x+)6ykBPWRheDg_et2#WO6T~^VNN)L_G8NyBGex&zrWUzq;=j zx$D`5yb@iV9~!!&|3CBk;&XKWTZY|U_Ww#=AIj)`&iJ;%;{|M{)a=G?y$=ORX* zf8eQYK9I9k$`OA7vhknE;#`U>w-A_*wUZ0~%aQZ>yaYY+(dHcMEAJe0dnquEdA*F$ z$GqgP0CMJ~9P7Ls**FKu%YDBB$Sne$oQ7N%aP4J4d*tlAF7S6^Mr=o8d$+F!OTiZ6 z*t4(9IC&rYE9z}**H{nqbAO8&PI%Fwm6SX$nL>j z*B<%38QJ_cgUIhK$nvqqO~`TsAo9Bz`G4iN1zuasy#-lr5JXSjifl}6k>6Hi^Rw@@ z$NX)`=C0j(`;L)|TyIB~3;(wv%iRIO=k3UOpF7dzwu3?HZEwE=IM?@pcJq>dConJT ztB*POUX!+?|7S9c@pBOiBv7qW4e0qt@3N0GHf&Bl?(K=jE3asu*w{4R9)@Tq4$ zHO7cDi7X#JQ<+b`-x_f8k>hUU$kF-Q!*);3=KA`EZBNc-{`$rpes^YD&ZysB;8|PO zDx>zu`TfY&BYNrs$a053^veg4qVhl@3g;XHjK|0Brm>3;gaoE}JJ{wFa;9&*vYA49f(9|Xp*e?Jc7qkkXD zIC&q}w|`^JPas>fSo4#}&XJFI#;1^7XB~JL#N4CE+G6e_$j+A!?onj(jvc z9mw|inT*pn_9++Ze-=5`&-dTw;I%zVoOz6&2Xe=N-<`)8ef;id`vQ>jTcaHNcpUiz z@J@Fyei6vU`d>nh_49Y+%kbKw-@bzET={rEeihl8UjekoyYy?w=K<}}>t9FKcRA3$ z5a?q~jx%bHoWFrA7y1*K9=UuIT_5@2op2;|QAM?-m*LTrfC+^0x$Z~> shift) & 0xffffu; - return raw >= 0x8000u ? int(raw) - 65536 : int(raw); -} - -uint sampleIndex(uint apron_x, uint apron_z) { - uvec4 words = tileWords(); - return words.x + apron_z * (words.y + 2u) + apron_x; -} - -uvec4 sampleAtApron(uint apron_x, uint apron_z) { - return samples.sample_words[sampleIndex(apron_x, apron_z)]; -} - -uint waterCoverage(uvec4 packed) { - return (packed.y >> 8u) & 0xffu; -} - -bool isFullyCoveredWater(uvec4 packed) { - return decodeSigned16(packed.x, 16u) != -32768 && waterCoverage(packed) == 255u; -} - -uint sampleColor(uvec4 packed) { - return (packed.z >> 5u) & 0x00ffffffu; -} - -uint skyLight(uvec4 packed) { - return ((packed.z >> 29u) & 0x7u) | ((packed.w & 0x1u) << 3u); -} - -uint blockLight(uvec4 packed) { - return (packed.w >> 1u) & 0x0fu; -} - -void collapsePrimitiveVertex() { - // All rejected vertices share one clipped point, so fully dry primitives - // are degenerate. Boundary water also collapses, avoiding wet/dry bridge - // triangles; partial-water tiles are intentionally CPU-meshed instead. - gl_Position = vec4(2.0, 2.0, 2.0, 1.0); - vColor = vec3(0.0); - vNormal = vec3(0.0, 1.0, 0.0); - vTexCoord = vec2(0.0); - vTileID = -1; - vDistance = 0.0; - vSkyLight = 0.0; - vBlockLight = vec3(0.0); - vFragPosWorld = vec3(0.0); - vViewDepth = 0.0; - vClipPos = gl_Position; - vMaskRadius = 0.0; - vLODFade = 0.0; -} - -void main() { - uvec4 words = tileWords(); - vec4 params = tileParams(); - if (words.y < 2u) { - collapsePrimitiveVertex(); - return; - } - - uint vertex_index = uint(gl_VertexIndex); - if (vertex_index / words.y >= words.y) { - collapsePrimitiveVertex(); - return; - } - - uint x = vertex_index % words.y; - uint z = vertex_index / words.y; - uvec4 packed = sampleAtApron(x + 1u, z + 1u); - // Compact generation accepts only uniformly wet or uniformly dry tiles. - // Keep this guard as defense against stale/corrupt payloads; mixed shoreline - // topology requires per-cell indices and remains on the expanded path. - if (!isFullyCoveredWater(packed)) { - collapsePrimitiveVertex(); - return; - } - - float water_height = float(decodeSigned16(packed.x, 16u)) * (1.0 / 8.0); - vec4 world_pos = tileModel() * vec4(float(x) * params.z, water_height, float(z) * params.z, 1.0); - vec4 clip_pos = global.view_proj * world_pos; - gl_Position = clip_pos; - gl_Position.y = -gl_Position.y; - - uint color = sampleColor(packed); - vColor = vec3(color & 0xffu, (color >> 8u) & 0xffu, (color >> 16u) & 0xffu) / 255.0; - vNormal = vec3(0.0, 1.0, 0.0); - vTexCoord = vec2(float(x), float(z)) * params.z; - vTileID = -1; - vDistance = length(world_pos.xyz); - vSkyLight = float(skyLight(packed)) / 15.0; - vBlockLight = vec3(float(blockLight(packed)) / 15.0); - vFragPosWorld = world_pos.xyz; - vViewDepth = -clip_pos.w; - vClipPos = vec4(clip_pos.x, -clip_pos.y, clip_pos.z, clip_pos.w); - vMaskRadius = params.x; - vLODFade = clamp(params.y, 0.0, 1.0); -} diff --git a/assets/shaders/vulkan/lod_culling.comp b/assets/shaders/vulkan/lod_culling.comp deleted file mode 100644 index 7b6705e1..00000000 --- a/assets/shaders/vulkan/lod_culling.comp +++ /dev/null @@ -1,90 +0,0 @@ -#version 450 -layout(local_size_x = 64) in; -layout(constant_id = 0) const uint MAX_LOD_LEVELS = 5u; - -struct Candidate { - vec4 min_point; - vec4 max_point; - mat4 model; - vec4 instance_params; - uvec4 compact_words; - vec4 compact_metrics; - uvec4 terrain_command_a; - uvec4 terrain_command_b; - uvec4 water_command_a; - uvec4 water_command_b; - uvec4 lod_and_padding; -}; -struct Instance { mat4 model; vec4 params; }; -struct CompactInstance { mat4 model; vec4 params; uvec4 words; }; -layout(std430, binding = 0) readonly buffer Candidates { Candidate items[]; } candidates; -layout(std430, binding = 1) writeonly buffer TerrainInstances { Instance items[]; } terrain_instances; -layout(std430, binding = 2) writeonly buffer WaterInstances { Instance items[]; } water_instances; -layout(std430, binding = 3) writeonly buffer TerrainCommands { uvec4 items[]; } terrain_commands; -layout(std430, binding = 4) writeonly buffer WaterCommands { uvec4 items[]; } water_commands; -layout(std430, binding = 5) writeonly buffer CompactTerrainInstances { CompactInstance items[]; } compact_terrain_instances; -layout(std430, binding = 6) writeonly buffer CompactWaterInstances { CompactInstance items[]; } compact_water_instances; -// Indexed indirect commands are exactly five packed uints (20 bytes). -layout(std430, binding = 7) writeonly buffer CompactTerrainCommands { uint items[]; } compact_terrain_commands; -layout(std430, binding = 8) writeonly buffer CompactWaterCommands { uint items[]; } compact_water_commands; -layout(std430, binding = 9) buffer Counters { uint values[]; } counters; -layout(std430, binding = 10) writeonly buffer VisibleIds { uint values[]; } visible_ids; -layout(push_constant) uniform Params { vec4 planes[6]; uint candidate_count; float max_distance_blocks; uint max_commands_per_lod; uint padding; } params; - -bool visible(vec3 lo, vec3 hi) { - for (int i = 0; i < 6; ++i) { - vec3 n = params.planes[i].xyz; - vec3 p = vec3(n.x >= 0.0 ? hi.x : lo.x, n.y >= 0.0 ? hi.y : lo.y, n.z >= 0.0 ? hi.z : lo.z); - if (dot(n, p) + params.planes[i].w < 0.0) return false; - } - float dx = lo.x > 0.0 ? lo.x : (hi.x < 0.0 ? hi.x : 0.0); - float dz = lo.z > 0.0 ? lo.z : (hi.z < 0.0 ? hi.z : 0.0); - return params.max_distance_blocks <= 0.0 || dx * dx + dz * dz <= params.max_distance_blocks * params.max_distance_blocks; -} -void emit(bool water, Candidate c, uvec4 command_a, uvec4 command_b) { - if (command_a.x == 0u) return; - uint lod = min(c.lod_and_padding.x, MAX_LOD_LEVELS - 1u); - bool compact = c.lod_and_padding.y != 0u; - uint counter = (compact ? 2u * MAX_LOD_LEVELS : 0u) + (water ? MAX_LOD_LEVELS : 0u) + lod; - uint slot = atomicAdd(counters.values[counter], 1u); - if (slot >= params.max_commands_per_lod) return; - uint out_index = lod * params.max_commands_per_lod + slot; - visible_ids.values[counter * params.max_commands_per_lod + slot] = gl_GlobalInvocationID.x; - mat4 model = c.model; - // CPU-visible terrain is lowered slightly during the full-chunk handoff. - // Apply the same terrain-only bias to GPU-generated instance streams while - // preserving water at its true surface height. - if (!water) model[3][1] -= 0.05; - if (!compact) { - uvec4 command = uvec4(command_a.x, command_a.y, command_a.z, out_index); - Instance instance = Instance(model, c.instance_params); - if (water) { water_instances.items[out_index] = instance; water_commands.items[out_index] = command; } - else { terrain_instances.items[out_index] = instance; terrain_commands.items[out_index] = command; } - return; - } - CompactInstance instance = CompactInstance(model, c.instance_params, c.compact_words); - uint command_offset = out_index * 5u; - if (water) { - compact_water_instances.items[out_index] = instance; - compact_water_commands.items[command_offset + 0u] = command_a.x; - compact_water_commands.items[command_offset + 1u] = command_a.y; - compact_water_commands.items[command_offset + 2u] = command_a.z; - compact_water_commands.items[command_offset + 3u] = command_a.w; - compact_water_commands.items[command_offset + 4u] = out_index; - } else { - compact_terrain_instances.items[out_index] = instance; - compact_terrain_commands.items[command_offset + 0u] = command_a.x; - compact_terrain_commands.items[command_offset + 1u] = command_a.y; - compact_terrain_commands.items[command_offset + 2u] = command_a.z; - compact_terrain_commands.items[command_offset + 3u] = command_a.w; - compact_terrain_commands.items[command_offset + 4u] = out_index; - } -} -void main() { - uint index = gl_GlobalInvocationID.x; - if (index >= params.candidate_count) return; - Candidate c = candidates.items[index]; - if (!visible(c.min_point.xyz, c.max_point.xyz)) return; - emit(false, c, c.terrain_command_a, c.terrain_command_b); - emit(true, c, c.water_command_a, c.water_command_b); -} diff --git a/assets/shaders/vulkan/shadow.vert b/assets/shaders/vulkan/shadow.vert index ca9017a1..ee6d66fa 100644 --- a/assets/shaders/vulkan/shadow.vert +++ b/assets/shaders/vulkan/shadow.vert @@ -15,10 +15,6 @@ layout(push_constant) uniform ShadowModelUniforms { struct InstanceData { mat4 model; - float mask_radius; - float lod_fade; - float _pad1; - float _pad2; }; layout(set = 0, binding = 5) readonly buffer InstanceBuffer { @@ -45,7 +41,7 @@ void main() { uint tile_id_u16 = aPackedMeta & 0xFFFFu; vTexCoord = aTexCoord; - vTileID = (tile_id_u16 == 0xFFFFu) ? -1 : int(tile_id_u16); + vTileID = int(tile_id_u16); mat4 model = (pc.bias_params.y < 0.0) ? instance_buf.instances[gl_InstanceIndex].model : mat4(1.0); gl_Position = pc.mvp * model * vec4(biasedPos, 1.0); } diff --git a/assets/shaders/vulkan/shadow.vert.spv b/assets/shaders/vulkan/shadow.vert.spv index 67146d48b22dbd0d3952d93c64be1691fc4ee5e8..efe10aed931721d85e577afc86afbf92d7477d3c 100644 GIT binary patch delta 1227 zcmYk5Pfrt36vf}nIM9liRAMTrB`CoSc4yoejcgP(QHY7r6(Kk=L};b0h?P2^f?835 zry}lLvm%X&-@v7D>5}hYq8JyR-#CMP$?3iK-2d-QfAsz7?eyHfc4HjVVN&M9YUik5 zH^wY)O>a7xlC-JP&0tc}H*IFd7ToN?8CjfG&#c{Yv)!}*9A=_esg4!LgNkhOYIfVr zI(56_=DHi=ZcUC32jy!)xf(<%ZHj5wf|t$Xb>fK9M6r5xq87-Mzg-byUe0OQx_+1K zhW9m-OyVnINS$WvH?J#9*gg_<&YL~vo@ASFHnKfKTTd90FPZvKWc$1J|9sEXd|c0PQS!0< zp30@4Nb48!c01MOEZR&u=PcPmIu|}AMj&EQSP~Aa*mKF0&@J@v=#tcf!v|>kLXzjf z&rsOQF^B)9^t8ZC*zaQ>wN4t}eo5E;qV!7h#@>q4Kc^7(n-chztA*dEjvBhv5K zg?-s@8cq1Dr#dEC7T{R(r?T!!a(=5KzN+v@_@ygCKOJ{^D4kmz)P?B=&Ah-XaP#<( z#InGGFfVAT(D0Xm*ik{6QJ4mdjE|db^%0X4dM2G1V)^T04{8x^>y>a>Xl?vD=9Byt zMkR4gNldmX5VRr?#FeiJ*M$+q@N72bBzCzsG3T^!Z)1+{a5(;q-bp8)c>MVj`dL+B c-f=zBjUk9Fc;gL$?|?1X5_`RQVgIK07ihVb`Tzg` delta 1643 zcmZ9LNl#N@6o$Xs+d>IqLWn_9OF*KDK^M9+Mq`l#9HOvuS1WBzLuo0cj=%*`92gX? zI1ivZSs6BN{0)Ym;16(T;t&&kpUb_ZdXu;3oc9dhcg}bEyX{*?gB|LqHO4aaCTiX- zHY6i6#+c;~r8TSZsT1_2SUw`Ba1Ka)ZmeA!KKQB$&pk#S1F6#8N@t{sjd=04^yF|VeXxoP``jT(PMnfFStyUD3Imz0N;aDr@yg2KmO{-f({ks0 zck*H-D^qE;X2x9&#VptTq!<@c-=%!AuUO8F=86T?s5P$p)r+aKV{u}*j$%$!^7-!E z=839hn`Jj)e{Kw#jwq^^2Hf^=)7B_D#PWF_Nk##8TnISE0M}i`aVA`E6~_qR&Immw zY$hdZgg(!07!yAf{M2Sa;AAw7>IQE;1AQ}lX6WFX|66V<80$PwX>zBbucq-*WWX`xjb^F@#T zhqww(io@S3eM%CW8aE|57Vd~|(;%FGW|z4ogO7yUl9s?_NWi9JIQ(}dN#-K^b*WR5 zbFLMQS@UjOzYFf+Xv}#ac25=Jel%GRg*jnfSP+QarOHc^wALcD>N%Mr)#2~~ZtzHw z3F7C5*vmeL|FJRKO`Ql1gIIOHL}zy}P5!J*lr-$I^-^8ga^CCZ_R1U*_#Y1l-*rYw zlJA$o@eK=Q;fE%GeoQe%NgnN|IO6dUb5uF3Fzc_&)kg-S9b{gQg}V`DhB?ID0cIg`1B{28Z8~&dVn> zGG6_j!0NCq_>jah=KBJEE;{DhOoYRzOZ~vd`&auW*xR7cqI3!n%xhsM)QKL|JQ3)) z`rxNNALKuiP2Q4nxY%=n7b%7 diff --git a/assets/shaders/vulkan/terrain.frag b/assets/shaders/vulkan/terrain.frag index a856600f..673ec7a7 100644 --- a/assets/shaders/vulkan/terrain.frag +++ b/assets/shaders/vulkan/terrain.frag @@ -14,9 +14,7 @@ layout(location = 10) in vec3 vBitangent; layout(location = 11) in float vAO; layout(location = 12) in vec4 vClipPosCurrent; layout(location = 13) in vec4 vClipPosPrev; -layout(location = 14) in float vMaskRadius; layout(location = 15) in float vCloud; -layout(location = 16) in float vLODFade; layout(location = 0) out vec4 FragColor; @@ -41,20 +39,6 @@ layout(set = 0, binding = 0) uniform GlobalUniforms { // Constants const float PI = 3.14159265359; -const float LOD_CHUNK_SIZE = 16.0; - -bool shouldDiscardLODFragment(float encodedMaskRadius, vec2 cameraRelativeXZ) { - float maskRadius = abs(encodedMaskRadius); - if (maskRadius < 1.0) return false; - - bool readyDiskMask = encodedMaskRadius < 0.0; - vec2 cameraChunkLocal = mod(global.cam_pos.xz, LOD_CHUNK_SIZE); - vec2 chunkDelta = floor((cameraRelativeXZ + cameraChunkLocal) / LOD_CHUNK_SIZE); - // Streaming gives the contiguous ready disk to detail and the outer - // annulus to LOD. Legacy integral masks retain the two-chunk overlap. - float detailRadiusChunks = floor(maskRadius / LOD_CHUNK_SIZE) + (readyDiskMask ? 0.0 : 2.0); - return dot(chunkDelta, chunkDelta) <= detailRadiusChunks * detailRadiusChunks; -} float saturate(float v) { return clamp(v, 0.0, 1.0); @@ -74,15 +58,6 @@ const int DEBUG_OUTDOOR_FACTOR = 10; const int DEBUG_SKYLIGHT = 12; const int DEBUG_AMBIENT_OCCLUSION = 13; -// World-space hash for LOD transition masking. -// Using world-space noise avoids a fixed screen-space dot pattern. -float lodTransitionNoise(vec2 worldXZ) { - vec2 p = floor(worldXZ * 0.25); - p = fract(p * vec2(0.1031, 0.1030)); - p += dot(p, p.yx + 33.33); - return fract((p.x + p.y) * p.x); -} - float skyVisibilityFactor(float skyLight) { return smoothstep(0.05, 0.25, clamp(skyLight, 0.0, 1.0)); } @@ -138,7 +113,6 @@ layout(set = 0, binding = 4) uniform sampler2DArray uShadowMapsRegular; layout(push_constant) uniform ModelUniforms { mat4 model; vec4 color_override; - float mask_radius; } model_data; // Poisson Disk for PCF (16-sample) @@ -278,7 +252,6 @@ float computeShadowFactor(vec3 fragPosWorld, vec3 N, vec3 L, int layer) { // receiver reference moves slightly closer to the light (higher depth) to // avoid self-shadowing on coplanar surfaces. float biasTexels = 0.35 + 0.2 * min(tanTheta, 5.0); - if (vTileID < 0 || abs(vMaskRadius) > 0.0) biasTexels = max(biasTexels, 0.45); float bias = worldTexelSize * biasTexels / depthSpan; float compareDepth = min(currentDepth + bias, 1.0); @@ -523,24 +496,17 @@ void main() { const float TEXTURE_FADE_START = 32.0; const float TEXTURE_FADE_END = 128.0; float viewDistance = length(vFragPosWorld); - bool isLOD = vTileID < 0 || abs(vMaskRadius) > 0.0; float textureDetail = 1.0 - smoothstep(TEXTURE_FADE_START, TEXTURE_FADE_END, viewDistance); - if (isLOD) { - textureDetail = 0.0; - } - - // Full-detail chunks own this area. Dithering the handoff creates a - // camera-following grid of holes at the chunk/LOD boundary. - if (shouldDiscardLODFragment(vMaskRadius, vFragPosWorld.xz)) discard; vec2 tileBase = vec2(mod(float(vTileID), 16.0), floor(float(vTileID) / 16.0)) * (1.0 / 16.0); vec2 tiledUV = fract(vTexCoord); tiledUV = clamp(tiledUV, 0.001, 0.999); vec2 uv = tileBase + tiledUV * (1.0 / 16.0); + bool isCloud = vCloud > 0.5; vec3 N = normalize(vNormal); vec4 normalMapSample = vec4(0.5, 0.5, 1.0, 0.0); - if (global.lighting.z > 0.5 && global.pbr_params.x > 1.5 && vTileID >= 0 && textureDetail > 0.2) { + if (!isCloud && global.lighting.z > 0.5 && global.pbr_params.x > 1.5 && textureDetail > 0.2) { normalMapSample = texture(uNormalMap, uv); vec3 T = normalize(vTangent - N * dot(N, vTangent)); float handedness = dot(cross(N, T), vBitangent) < 0.0 ? -1.0 : 1.0; @@ -552,27 +518,21 @@ void main() { vec3 L = normalize(global.sun_dir.xyz); float skyVisibility = clamp(vSkyLight, 0.0, 1.0); float atmosphericVisibility = skyVisibilityFactor(skyVisibility); - bool isCloud = vCloud > 0.5; float cascadeDistance = max(vViewDepth, 0.0); int layer = selectShadowCascade(vFragPosWorld, cascadeDistance); float shadowFactor = 0.0; - if (!isLOD) { - shadowFactor = computeShadowCascades(vFragPosWorld, N, L, cascadeDistance, layer); - shadowFactor *= 1.0 - smoothstep(shadows.fade_params.x, shadows.fade_params.y, cascadeDistance); - } + shadowFactor = computeShadowCascades(vFragPosWorld, N, L, cascadeDistance, layer); + shadowFactor *= 1.0 - smoothstep(shadows.fade_params.x, shadows.fade_params.y, cascadeDistance); if (isCloud) shadowFactor = 0.0; float totalShadow = shadowFactor * clamp(global.shadow_params.z, 0.0, 1.0); float ssao = mix(1.0, texture(uSSAOMap, gl_FragCoord.xy / global.viewport_size.xy).r, global.pbr_params.w); - if (isLOD) { - ssao = 1.0; - } float ao = mix(1.0, vAO, mix(0.4, 0.05, clamp(viewDistance / AO_FADE_DISTANCE, 0.0, 1.0))); vec3 albedo = vColor; float roughness = 0.85; - if (!isLOD && global.lighting.y > 0.5 && vTileID >= 0) { + if (!isCloud && global.lighting.y > 0.5) { vec4 texColor = texture(uTexture, uv); if (texColor.a < 0.1) discard; outputAlpha = texColor.a; @@ -586,7 +546,7 @@ void main() { color = computeTerrainLighting(albedo, N, V, L, clamp(roughness, 0.05, 1.0), totalShadow, vSkyLight * global.lighting.x, skyVisibility, vBlockLight, ao, ssao, debugDirectKey, debugSkyFill, debugBlockLight, debugOutdoor); if (global.volumetric_params.x > 0.5) { - float shaftDither = lodTransitionNoise(gl_FragCoord.xy + vec2(global.params.x)); + float shaftDither = interleavedGradientNoise(gl_FragCoord.xy + vec2(global.params.x)); if (atmosphericVisibility > 0.01) { vec4 volumetric = computeVolumetric(vec3(0.0), vFragPosWorld, shaftDither); volumetric.rgb *= atmosphericVisibility; diff --git a/assets/shaders/vulkan/terrain.frag.spv b/assets/shaders/vulkan/terrain.frag.spv index d15544a4ee550537b484f86872d66131339c5ba2..0a9588f7b334a674b1964bc0e0ad069ac2f24bfa 100644 GIT binary patch literal 59992 zcmb8Y2bf+}`NexkWxfOZZrJCiUsW}!V_Iil$3C6i9sOOiI&rAWE<-+}W7_oY z&V9SOJ4emJX-X%BH>R-dEseb&%jk|7oj{ZYs};z*dip#2x;r~&bxz)?uVZpoXHWkg zyv{80>{Enc$F}=NglN)6hs78>_nA$P9_n5uanz44#!G_PUYE^Q}5oM!0 zO}(vJ4ba^&yR(nnkyx92UR}HNjP9M;Gr3ut!_|mkEqtaAw4~NneHv|AN8c1@xSd(~ z>_9#HbT&q@tG8zzG{<&>0o%Tv$98r%0@#QSyI{2`_=wK_NmIu+Iykm- zdjHftIy?G0XY@CU*hR~1JW!^s+5|kWPYYIC{f{~tEw3}HYvZk(gJd7Jz^~tk$mLrO zs%N2UBz$T|-{fW}V|u4eCyqfCHHN5Bc3XVQy0#ls*RX1P_@rhHjXpse{oRrL|Fn^w zHmh&+cMN{bF>0&Eg6H*jShWNE|JJ=gwHx}3&hE}h{dEsIW=!gs?DE=V>*l#Itl9;i z|GQ3xRl8be(|>H&jQ)r>UiaQijT)|h5M&E=Wa=t;5e_oK}J?a$C^fAq9@0Jz!au73Er zGZv`a2zKi1oz~gkH+%e~soh46VAzn{^JYjs!QyqP;o_Jh=Yh)zN52aQ5|dcGqDVqjfi9>cu{( zcgDU`JNr9;L^-sYPQD{o2G9MO;PH*Uas2eDoqb)CI=Xl7nBEAo;p+y>sSjSDI*z=a z?_EZZ8#QgBJL14S-De08!eiGst^-`=)g;3GQ7N!t3}) z?=yDCK{Juru-rNLb#zbcoXp@E$GPy#&3WL=&H3QW%?04WeO;is7;Pfgl*FfjxDuc{P?vqWl)UWI zW$^Ura&Y=|1vq`W5h0By0NUKKtGm0Q>U$H~yt-{I9AXj3}-_wTJcQY>Ri46Sa%?s;PQ+sS8jbsm!ogXhBCXj40TW_Qls zsjsuMXGVW#R}Ym9Y!KFQ7d}(!I=(_a_&i*&dZ3{-_D_n=bg$`}G%&@?U|sfJ^1hDQ zi)M?_cgU0df0TF-}~W{*vxo~|P}3E0?%)jFm|et^;{9)9l&HFj-uO>|At}5D4v>7vdCU}mf?zUQ+)Z5)lCIPz32;20DeFKJJ?1%PsVbZLD2^laC)cGmxtcU5nef<+=bRFllBK=`R?S75(wHOXY-sfid)T|=qHfpY z|5_KuMLX@)bLi7Lre`~AwEaBVytdk^AGh$A;bm;UY_Y%E!hh4kUmwKVt3Sy5 zI*;J`*2xyuh~d9z{auYmANa7UjRkO2XCJpV`|$KNv$1X*rxnoWJ=?TZtHR6JN3`%Y zTliWnd_8#Y5l77EWOudxP2uzIEt&fk|Lt1%s1`oDg^z=m_3htcKd^-#(!viN#M`T> z@|Nelu0cL+)lq|Zd(|VKbVT!3kCAkI`_TT^_?|teY+H5iAU>?R2tH{h6W7yUtf67m zCFqTNu*PZ|-`E&k|5uD4xT%)g#!M&&A39*Q|t$G^0TszOS@Ml~23-IPX&|E7o z!hJX3>U1Cd1-!YZwpYIe7w2oE-|uMWQgv*{#O~%E$_~qB*55IG43G8$*Y}0d=GEI) zEdig`?$ByUc-q-lJgCmLYQ`Y{wyGaq_H$MXKemM*H;A`aXTW>79F2F6uE+EoGwA$)qSteMP0BX*8g8rJgHM@qEUh>D{XV?x_lGU~wqNr(psm^nUdFkJ z<#(V^$8`L8W`zdp=%Z;_YnuIo~v%7nq)>chH<96RiFg%7X(Cfd-W>3 zhsT$`4&LH6%CPsLHO{TBo`&k&y^S_wdgr9(T}yL4yoYww*yflG=ccTFPHXgk0eCq_ z3&VTH&bvM=4(Iv9n}hoOS6j6bTJw3aS?9_vb*$RLN3`%YTKHOnczd-GoV~X3L^}Sc z*~H2HFEdDCv`I9__tRFqH|v`y<-Y5S0<|sYrNZ^wd3Fy&92Ww^!53wp5wz>_=G{cy_yJ5 zznkq1tBywN>+G29rEJG;-fq-&J%T>(erH(qBe{JB+pE#=#(M`}^40C^g2p{`L$S>6a2~{`d!uNXJ!(F0KWeYWqYZuz zZ>tVQn?AMq^1m@Q@^Wkj@%E|%&bk{@KjPBT@aibEj!BbdPGfO1_~R#a^z-9E^Fh^l z=*HgP$0G>u0Gn@XoBf=I&UrF}UqPs6Xw?VLGuiRl8hsgBoeFNgk6_;fH}11|32NOJ z!lyQ#>zc2ZD1zQzU5f5_j2)=It-4mNxDH?Rxvrz^kaa(e&YI`l@2GCZ zt`W!g(I$6JoH>PAYre2)#`lBhoG`wyX~s9KdM5hlc`t;l+ux#Q4bp~HKaVeOaD0)L zKKh&0Eb6UaCsDes`UTp&J`Jm0(Z}~Uc~RaNpJCOn(Rrrv5qm6G%C6=$s=fN1`k>!~ z+8V!$&GP4?UB))rc(?YM%oWp^{msnV*?GSM4Xyr-mfr-1%+I{e7}MQ5b8@4+zpu?| zQuxqn7<`XLTy2ft-Dd64d~xagE{Lu99+RtY^A5hz<_PqeO+PQoje6EX+qY3qd*ipe zS^IYMOyNd7D(NW!dyJo~^8B^d4A;ReOPXUgR;d4ysZA z&}ssD^FC)-H5tra)$+b?cr^vRqkmfOjON?r7JFMY6?-{XN5TK^`~LRE?~k+gpZ5~Z zdG5t`-agY-O&?TOTh%v+536Rvn`^&3Pui+kgZ$d6V_W!fE&PNQe$pV`UR?+u{En3O zuW0k$YqnRH!Do&iKWfj$%r(ywe_x%_J;B{N%cwD@x1u-SkMdp;JZsdR=xxI3ho z+Zw;i&Z>7M=ku)UI=4-n~{m-D6bMpp#=05qMq#3wt z<&WsOZ~O~5@A!=I-9dG5KitAUXyG5V@Q+*gzgqYwE&M+%oUOU&Lt8bZg}1fvVJ&?A z7CyX%FEEI=S4+WJV@LFlZN7|k3|4IM83CX7e$lyat5zT6(^jp~!q;ly+rj%fj@hwy zisgA91?TEaII>uUPl}J4BB4( z9GH*ZO&u~`>;!ui;C@15LPyvC=d_DbDjnl^IZg{$!jt$F@U@XaX?QgqHZFT~dH zoiMq>hr*pWZ}VP%XU0eD!7UljCCzvKa!#klw-=?hh6aw5v(2)cytSN~OY}_N?!|LgRAV(4HtXQjph30PuY7S~* z_BR8y#R_fVT3fBq{Ea}r4GV3NS{qqt{w|>3UT8Zp4|BmINFL#N^bcJF)Tj)UBLTe)N9`Rcb>t|4t=ZRwl$pYkEK z-TO&vo!%ayWXZS&Q>-bWfIRW$0BmvQiWnAcv+v{qLu#c$#RCf;qQJd&pi3PnmqI3{%gF6 zUp}w?p^bfY2zJk*?7KOK!i{aQhR?jc*>l6g^Cf$;+DOup%#?MGU~Zg4@iHLiV6{f# zIe^@xHiEW|Ppq~q-2TWdzZ%^BXj9kU`4+3|zXZAdwx$1)hk5t^_MRV zUx|D*(tM4)ty-Df^6De#W5(F_v@JzT_E+1Q;8k_XcH!S>@Tv-bxM|;hIm$lA($JV!X&F9vdajXP>vgZ0fRb$&)o?Z5bCLyQGZU2WTGQpMb45eW(X*`?NcRK^t25 zF9GMj0so~Ty-S?7Rm!iZv8o2fU?sSkDL(6f#~gjmz?%CE{GuHm-#Yfs!cYC%_|fUh zx^Tt}Cf>KX?i`a(stojjcCrjz{wd z-yg0dcU_(d@!Dqjv%uEnTGVcSuFlmdTW0QWfM%f$_|Aki0vw?g6k+>JLBh?d)bzyx=!WSs)OE>J* z$oMbQ;HwSrYv9X%<0o6hem{JZO&%QBXC8pF4-N2#8s%>s>6p9*&Y0LWlSgvLd#l#; zU5_)<_Wnw;PV2CYKF0bsx#zAviDjr0QJvBlJI8uKxc5fd-3wN&xqVp;Y+uw;egydA zwU2&0e0BJ1dq4YF;#&i*k9vI81pjUEzQ+7I)@#96d}-{!czhbJk9x|i4ZiZXPdpmF z4*aOMrVW(&3|t@e_%=5bN)sDG*yTrC0Tp#t6`5gG9V~!l?&-(DQ4|;Q; z%m#3M)Kg|d@C#4uHBe?F_+E>248*oETp#t6*#vy}7iSNQ!KU!H?ijUwV*5N?AN7>k z41Da{Zw&NjbNG5U%^qlH3%EY&DYGTGx@>`gcD91=dcnH`^RqQvAN7>k20Z+(vmOiI z7C!8xpAVGT4z7=Se6|O_@z|~d?Tmzf|M)Qj<2DMek9x|C248vqF$3+4f#0$H$pd3B z7Osza%IpAsVd}iG-VuK8Qb!Jy*$J+Xddln!-td-r=ld@3sr&3QP-a)SKI$p68~Dnd zmmC=Daqz<@FE%g+yTkQSPnkWyJO5$Cz&h9y{_Zt542;2EaDCL*<8FC1^1Vr(Bd&SZ zuxrva=9+O0IH%5$W9}F_Cic;ojlnuSx5txvZd+&Kc^dr6qPvU?e;Lj(DtrVN<5w2_ z!N}Z)_)P5)`?m18Jr@m(!6^88pZ&PO-75}+``n_xybJDgqSt~x@ETy7bYDFO?zzs^ zGw3My)pPOD&;Fc8?xALUa-Zov!hMD>xzFq#xt2c&_gayZem#o5;7e<8-_X2&c?_!lgj7`R$e{B}qCcK;>ZA2$8=fU6}reh0xFTkp>tFS+k9)KX6$ymyJA1NTBR;0wNV z%fP+TDe&8_89UI<9JpFi>RFqKWt|PIs}10c?>5>UkImr5;q~bR@@?R`4>5OqQ`c_T z*!9MQPlrGE?6cdZ559M?5BBld8e2rW-1jhLAAh5fAxwR*HTf9J|GxG$M$5~6S5ub% zbHo3Xfxf-dQAGNR7M9a&4w^Nq?2!70CCvUI+d@Ksrh37@$_1%yDa_7kRJ@Smz zLKaXUIZlpO#&t1l!wbGF-1jop<#UDahCEWY?}o~@{4G)L^VJq$=g9NbShs}pUqer< zzB|%C?azVJy@B?8cjRIDD{5co+wyYXC6(o`f%}}8`u)8ucI&+Z&fIU8_2{ILbA85<2&f1LyzA z@T;%7YT&#*1+I^JS?(-&RUJOC54>F4ThD=48-5p!Og~oe=C`_UfUgJN{`0F0w7(&| zcZsJ4_UTRG|Gx9+0lrnE{Er99@7Un)4Demyoc9Cg`EGEvq{J}}?K6jdaiE{O!|k7a z)fT_;U{>b9IdcGflT9`mn1=)5j*WUbK8H2REjLhZYJ;ycz^6C(&I5dQgC97`^>zx_ zwVU-O_g$k})|c-ZT|aWyqwg9^?z_ft*OTuSJyyZrcY=A>V0#z9HLF{n+}~}~Hsjfc z<%HTijNu~`PVa`V%hYc&%q?r~_ZTJjJB*V1{YA~k z?l%%8_j`zN_pKqMaL?Ic1^0cF-#wIezikLN_gjW=_ZGid2)91JTPV5TD}ly<)bD7oJP zggah-2N3S~`W-;H@vYv%{RSX*bH4#7x!(Yke5V%f@Bgt|e{Tz))x!NQp!D~ffNBQ2xBq?{5WDsHZ9usB6D{2D0!sVS1-C!X7hFHT4=DZpHlXBx zYTq8!JSXP8PL8RR|xlFuR~(@VXwzx_h9iTc5d%2pCfty z>3g|t*|&k!^R*tIAJsm-tBcQ%(bVJfliJ63it+g=n)<>-MOOWc|9LH1hh%x}vAs~( zY+v7nIbq1E7i-(PVEuA$VSl`aXiu9jgS}tY=Ji9qC4D8Ue#!qXCRf+)^+wII-Y5PF z?DNFJcmu0f`JeHQu5Fg_8l;}~UI(is);GYG*@)zQ`fDV8POD@1E!cU{&-%QEsac=* z?0*D%zusHV#h<`x7u4n71gm)upE7>|mu239tDRk!`72n>GU?mjz{bgvtF-ThulM|K zgKb0mg|+r~u)5C)#_V-g&Gs$xKGz022wr<~lYO{>jV)c~y5UiGV{|WY8xGCDl zXg+J09AE4H7udR83)W#d{nCc^wDAepSi}DVw$1eGzhE`{HK*>YCy?c|S*Q05YStla z1N(gQ2>xrp+rerErN8$SYWlmThJ$_nactxZfQ^5L+GZK=JJiMAbNGB@dFx*oY@K)1 z{8M1H5Aj`;+~Z?x>Wh%nye|`vqD|kSFAkoIpU+n1`MCsKTl%sjSj_ntc#k_p{hn2m2fAlYhZ1#&#mF>fL#OOpK0Nrh3kJQ^}7bw1?!{kTDH#hz}BP9 zy+-YGVB6A`^|wBl>20h(?Z&qjxq8~)5M0Ky5nLblYU|w?Y#n1s`f5+vO~F2cC)Uq{ z^;dtGxLiY+)`zXRmDwE#%_ zZSy?T&pM4`B-nN{uA{*Es;Ax2VB6iho{uqLWAlhMw$^Mv+77iw7YDQ*(QLybemm7# zS$5}IE6eVJW-MjdU2Cl@yIZYUb}xGA9F9XXZsRnr=(~fB$rx-i`W|50vwrJS--3Kk zl66^5AJ@}XK6{hY@$3h8og`(= z?~ks(_KWND-~g~OXv_Q`2(}&PU%PSdMXsKCJ_uZ{uY=+GXm_m~0(Or{OozhNlDuZw zzb~NK*7_vtv>tsE%VD+MSoAd(+f{e(%N}Iz{nk_X9S)vL?(dEn*A6uGjO#?On#sc$ z9M`lv30=*0^|jpz)HwsJzqUSd`IgM2_g($q z-N-GYO`l$Jb@5Dc+e|yhz|+nwuspWeVCT4#C%zNFa&5+U zGT3-5cM`dLOWw@54o?C5cOe;v?OCto^-Jvf(3bNd z{kR0py7e=T3(3`N_gTtZ4!*4B?u}Q0uO#hR&EL4UxEicx^02M!0oS4#<2B^gXZh>E z>M4IcSk2^Nd7lfsZ@B@@`t`}Z%Z*@T(w6JdO<;A8jL*$*+g_G@2$g0WZ-Hye_TPup6H-SjPBnCD(3z?k)F$@2ze3kjwSEpZo!mey+_2!LH5Rvp)n@bFF@z+{3l1 z{xy=CYgL@_{RY_aU5vau_a268%YE%P!D5pG4S)`a@Wn{@bZ553AjG$j>n_qYOYhVYA*j%E7!@BaP{01J_S~r&fFT? zcflUUrtLc~ zq-HE)$M0F}zXiWhbJzCoz~$QhJzO94?1z5sHX=~2m3ZYcQHx}5Ll z<*~JcFRg99tCw%dH!sfNg5X=o9S{3$|LlkH8>jIZhwWLfN=bbZvQ0 zxI9?RG>5&}^LPcYb!)Sq{`R4k=Z}@-_^E66w-U9C*UI22#F_rC0$1CBadCfH4eVim zwXI50v%g~N^!Jp1;A5Sxku~9YX7INadD{Cl*cdFg7P)*&uAYwB+TaJst$<$Y zZiTKbYi?_>ngO`xwiR=@=Co}?Qgh8UFgnY#+8)jOX0I>uk#M!NKMI`o%X`bw`jE2s zj{#d&-D{I|jRiX%E0VP5nQjNL*BkA*Chb`JdQH-vdvnLk-ww5Bo^}SSh3``H#Ih?~ zAN6SC!0Nk^Qf_y!=Rkb+fa|AjyE~Dq>F+tYCpc^9DfnJ+$L35rsgLK;-e~GsANzpS zOdgI$?%N!r@#wab_d@%E)sk{gwjY{hy(cpc>(w`FO1o<)b8MbDJOFH**U;A?b*#31 zAi6f|*q>aUG6#WErd)Riqif5$I|Qs|a2eM_;no+QFM!qb$@}!f;Fi_qdOch_hwD|_ z1d^KTRh+szz}B5-fr(%>-~C(9WN_-yHi7g&z}1s{=kK|9Dw?(vNvDv~o_g7y?b@DY zP6HcL`Y$j0?>NNwbg*Njujja2U+2hvI4}Bo-kb?8=k;v3+F2yW>RfUU=R?~$q*qAR zBTkzafOCyMA1v4AoPQDQSQ*cSV7c!f?bk)%5#*N9rq6lg>bbss39MG$3ta+Nf2>|d zmx9$?>&AUK*u%KBT}DzfZgJ|q0^C~nm1yeUuIs)EtmZ!NIHn%+Jda%swjIYrf7>vQ zYso!~L!WC%YQ`Z>9M^%3qr5I%4_8kgZvd;=N8?C6=85A*uM>6ow}EZP*z~sz-7+=r%~@wgwXmhnhE z=85A0uaufcm%9w^6*+< zTMxsv<-GhhIOpZJz;e$^_k~Bno03~bo8`YouI}11Hsdg!*fI~XncJ52Jr1_Kz7LYi zGsbhltz-Non!5f^kjqoocfi(>Yw~x&YJNkJ-&MZ{S9eZ~%X*%ItEZmtgRLk02VmQp z!+CGa&w$nZGiYh+hj7bjOIy!^)yuY?hO1k*^ZFb(^ZH}3hv%aDk4V2DIiBLg^AoT! z=brATV6~G;@%b6pw&L>wSj|aKKVF2Z`wc+K{Ty!Jb5HOR*!uO==6F3%uI_vrv+)^c zY%jOi^s#>T^+2JMOOZ(wc4_a}0>V`tm8V|(#^2W%hW zD=&R56W_mstxw(R%!-X&K{UH<}?as3-^>^UDl0qdh~8OQoR;BpTC3s-XvbIA`;Eedu%@?5c)zQp72 zo7U;OR(;a<#lh}@`dOdfZK|cdCBSOoOM=WyQoio3C(4Mjz zfXlKQ!PWdTFe$q+IAyh`>?Yu{?C0TX{&|^{-3*+v+OtMB2OE=h>yt52FUQ2Ok!K#v z<*o2!Szu$ zZohq2v-}B^AFWQxJDV|JHIs+q!Ra;7XYKaM_>9Gv(x)B3WuJC}tL;d#PrHzN*e7i} zllCTAk2v-00rq`<#(z(^T2kH%?S*E1z8BgJygNzX%#U_swO!k=t+cTZxQumQxY~G< zvF=aqVXWHrBOOYz9&ze92<-0*iS=N(T2f*?1g(tqK)Ak%RlBj;u5H*>+V}#vjCBHB z?J$zDc917lZHJSNAX$&tdc4P)2=@1!#5xJ?ewY+}GP-+YQu@$|ZXeuBt;_QIrVrZf zgYnq5F{BSuz-1r0;A&Gz_TeaU564^Ek)$4y^@vkXH`w2066-X$T2f+BHx}!)4(m!C zz2GvIqv2}PNyajRJh5o&BOODs9&zgF2bbS`&4g!sw6$JqW})emlvve`)pl*ew$jG2 z;4;?Z;A*o;#(DyIV%2s$NzHo1spmwn-;CtCcoICZYHN-4WHfz}605qgrVaBlcKc9{ zyW=m`*FLg}@c8?(_$RSF{U$u~sx9*x`y;j8dou6G{Ck)z^B=}U zTVilL{JTIr79kmff0oBT;FzC3soqFX3wbJyEIi6|m*BS*PQomO5Vrmv#OcUe@^vU|@Qs-~MWu3o+mv#ODuJ(IU>ii?va@wrZ`BAe@*VSK1jz{?4!1g8lZLo2KzXP@o z&&xN-|4y8?*Wll77SBZD4IV|J%X* z*LWs-h+LwN{c){oPaDI)%{JPbe;*y(Hq`ZT{D*_*C*{0e0IX*6F!Q`lJPRV&PJU-z z2yPj5eHO;HFxWBAUhpZnn!cG2wbZ!?*gC@(g*#7{cMSY{Pu29x9I81l&XHqlZh7Zr zacuTG?_!pOtNAV_^R8CTyK|KGm%?WIdEdJXT+R25UbB}Y_i&E2ElW}}4zcw-i+y=; zdA_UwUm87o#maECl}I^XRsma1Tjpd{u)4{^%sFvi)n>c4Z@Z5F2(aUy{;UqSKe^{x z1Fn|jyBW)^iDp^t)-BJT?L4eN@>q_PJ$reu?XO7g9_=2xHsxH?j`;sh3-$<^m`0EvbJSxM!{`6_b#L1Y97(X)S7a%v9;zp%012wXqMMsyU!07 zGjHw_JHl;SALrHns2PX$oxy5Zv%7%ROde*om36Qyg8oBD*5Ufv4XmDZIu5Lsly$m0 zn)Pcp7ROR8W3mTWE$d}Zu$sxk%>B!?wiiUZV!pH5yXI;+SNG9Q(w08#3$~r{>eOc+ z_5NAz3W~}1GdL-Cb z%im;=LQ_wy-QaRuror`5H?Gucp169zwri~V+oo|%C-*QeeR@f1#wAW%M}u4A>O)gc zTrX8;7wTOYUK;`W!=2Ggfh8oeehD^~e+3 zabWeVrQ^YB-iH}S>M>6oCxC6o*z~sz<2Z@j!#MOgk)&oE;>2+>xHXPb(9|;?bHHkj zlX0XT^Tcr~*mjIff7|dpRL^!b>P;-{tyTR`V_E_AiAycJ@nO zx&1Ph%fV`=vbV~w0IL~S{@#Eq!Rpz&uL7%?tkd??tkeDeYOrypjceexk@~I$tL6G| z9oRDJj-_>659Ytdv%U7zeIwZRyjR!nCNTds)`DejAnD`SJBBxd^>KW~8Q)>pHYIuZ zoS*UCxW*EF)jaXt4z~`+MVs93gpK76uyd-e-MZ8)dl&gv zNq5&=|9imtWE}4W8G=jwjAJ|58?s5ND;@%I59B*v?ew0k&~@^UP+#2ZcUzb`%UV|SnB_+nrGcU z0?)eDSMFFiHs1zYf5zr9xIS6OkAu}*$F5t;JONfWxqj5kKp|JM0=22Hz% z^CK_kXJo^|o=EOmcf7Z)vGcQCjh&C}NzTiL3O{>L@X{QL-R{f?u) zP2Svdegd~WZI+Yg8=0SiT}zhHrjKo_Tes)!&%nkQ{sLT|T>D-G>!Y6UNq!Di_j>5Q z?HH+*W3(f>$7oXagE2L>k7H}>IP5@jjCLZ=-taQ@WsKa1e_3$H`_+P5-){`ZbTeO}$A#`bepl4CcHJbS?(s3T+N*!-o1|D%Qf zx8Rm*F{@*Ux`Z1#;|G0wd@4vg9^4k4(x5Ksj?{0^i`|oa>XP*Da_&E=`U-}b#8Td_DJ;?Rb=ip&*HOJ5KUI6UjcxxL@T9D+JimlW0 z&%+2Ob-$DG`}@?ZZtSVo+?_8Ex)$@_awO26zNM zmeHn<`=5GZ`83$}w7D0`E#G)w0$!W)meJ-seumt`d0ZR34oS_q5+|ANdrPod*7??85983b6-muF#Ma}SZVS$wdQFk1jqSm< zVf$WRm1`935M?c-~^dFtO6Y#sO3Yi>WVn%6_eLcSNtx~yAl zS?BryaOT?Uy*%|E1h!uLcp$kvbA2$_xwecpeGVa4x4d)x1#ss2P_R66eHhrewv0CC z?{IPtW7a-_q-MdF6U3IT|dXiv67p|e-c>#jQ?b?S~>n|<@h^RDc=dsej(0% zsD0e05i#>gzBzsHzG#mE?^)ozYV5t#-X!B3PwxGx<)%`%_fO$n1-HymExf1Tmg{Tb zGh6ua1-HGk3vT&yTlhsS{E`-aMGL>Kh2PY|Z)@RqxA6N~_`@yyv4Z>T^K`)-pJxhg zf1YjOFSPI%3vT&e6#Q-QI|bK1|Im5!WBm)a@Ffba|B?mw--g(?=B_Kxy(6jLwc@$s zT+6fOy1}jyZO*$q=iW51=bmM>>Eqf_&-K0+JQp99*r0o}>2PhiH#-`vW*x34d19CW zR?EFvKUmH4#`EGOCebUBkaM{LjaJ5e;W*aAhJ#0hU z38a%q)+4qJ*Y#o~&tLtu>61222A6G|0#{p-lzX!|VD+U*%aBs9dRedaTd(K-sbJeq zKjr$SpXTvB4eVIx>v<~AT%8X7U-xEbpleGVXM!!8d$Y5|Bz1jUQ|EwP3%29=Ef;%k z>!1DiJn-81>!U4y_s{uYZI1ng$?uQdfwSz3^s=HS>{V{bz`}R zTyD(XCteD6t!F$ggF7BR&uB~kF9&P04)-E?>bMeI)^QcwI$THEQpeR`ZFx6;Ef}}P zyLoNbkgWd-a&3v}dhq|6qZ`n*rM?@%sZZOv5bJYq)Mgyc)6HP74dJ)Iy*}jk?=Qpk zQBU2sf>XElrR%!gXSJn|w}aKf@2Gj!&7E+4)bm^2U0~l?Y0Ga7Ujf^mze{VkAMWAm zsr##7weWkuww>QF?gi_kZuz^()#CpkSS@}08rU}d%}3iqq{RJo@B<`$=5Vj$Irt6m z{UmMHe;>J8>i;I#wG#daSfAASEwDc7@%c8``A;7n1?#778xNDK#s6`zYdZW1us(@# zE?6J+^z%t@`l;Qqx9-Ql+N{fS<0)_tDSPmDz-rlpzX$g49M$$+lA7nF*m_)V-v@h6 z=C|}8fYnSM#$x+V!?mUTAA)lodIl^{Kc52|ujQU4m*+j+^WYt+)A}r@-Tgw{*o-l4 z{1{yJ@h5P#Tmyav_OOrIeo9ia9Uy`iLy2X}#o&0yC-`3oD`aQUu zr$4~eOdift+Wi}v?b?TT$p1=m5BA>Z?_f23^!HuwyI^&>=gvRCjzfMUdk?ISx@*@m z?}Lp&TiX2q?3&1LZ6CtbQtwA#*M+}F*v>z}*5eWF<65&n`EKQ3X!>fiPV0&PzrotM zg{lssR_px)?)a%|Py7D?yB>1>{THmJf7Xhc^Xc3=C+6v&jX17(?w-#^NqJt-re^u9 z7w0zZ55Z>p*+1IgYC}ocZ`;A1^V&Qw$?c!_ zsp^U6Q{d~0dKZD~lRt~JC|E7^I<||!)t9E+GNhbGi=$becH8nESWWExujioHd*7T> zOM&g1x#!d}myLtM)JlHzIR{$Gx&aoB2`lx%3$(JTsw{3|n>p8X(xID*J zhL`8qDsVNEM|qCzgJvA|$@_Qzt+0Ada89 z_Ow3{?0n}On*>(VKWjnF`E{b^&GK14&TZO1Lf@Kuoty%8 zePmCX3RcTG*afzXdd|Tk!JdQKJO}0Fv;D!?JoY8!*?zwQ?_b~pYV332fi+&4{2-Ej zKZM-pdhIi4$2`yZ{VjY}3qQ8t)_GjPt>=sueohO&q=jEoaNh%5-@7 z+#64W&qb%3gYNlz(6!||onEk-dxXyyM}s{)f3-~~sX1Qa^7%sDx%V156YM;=&Kx^= zY{!6|7j2HCJhs_j=SiDmD%a-vIUej>X>)w#Tk`w8=kN(&-}Bg(by`**eVt3~8H*Fa z8H@5g`$_29^5>sV2CF$1iQyEuF=UUM16T7oD1ZL>RJdieIfkc`dpL&LP9v!~hT_zH zCfK?Y=UH%{XFT7u>1SWh25Yl#t}A)!I2UXkIcLvz7Z^s z?G|v>wCAbZ7(7R9Cu{gtuxr@5twUdZ^mRYbZk?`y+rX~t@Y})qgx>+SkNL9pwjJ&6^O-|)dDgLc+WrdIGVTrHa=$*D`aKRMWxxJH zfe)*(d*%d^>!pJ{`|>^1mwnm%ffoMt7XGak{&)+YTl2(zFWh#+@2h$4t?q~Gqwd%k zk6Qd61gnKVRP&Vo8eAWB%NxI%{;siafE|zUhvAM%-Z^{|u8(@2Qy&34w|gD5{%Fk;+hcHj)GhDWsm1>Zu<>eh9Obb+3D!USDX_jdkG}&}bIql_@4_vo zE%zwj1FP$2JI;?<>is@gEp_|={4_~DKF@$lpC7{YQ%{|0@qZR4(QelIz_yJE_2v!LIQm$g{?ON*(1If3bzX(!yV@dE);W z+^u;dQX(w58k|V9TYSzXj{3Zo7_)TKsh8* zAK{kQ$9a~Q^W06m9#cr!*QOS@tH4Lr*f~CmUsxU*8O+5o`XrAo0c1nu1){My8zhsa{ev|x14(J zFBSqjhwGBG&%6IH84RDL#+e=BMDctB>cp+9F{4pv^InXH0y?pGoqV zLCX2q4|W`8k>|N?amtnF+EOiig@XIeZq=G++?Ie_ukYiHYe~5M+<~Oc`L+%9QN(Ur zOM}(Q-(Hr1tH)>A+Nb;mZ#gveoEOW3ZEJaw<+alqj*WWO!D?XJ&NX6nu-XWc^X9p>23Xz365pC|$JM@CuUx;> zyB65CQtzk1YF_tK$J%h~u#eUu*Uvg!59`!+*NQl6#W_2U9a%T7i}efcIk!>G(}(ro){}Gkb8xj>Kdnzqe`DGJYz$df8^WC**OfN=XdCK& z3v3@Z2D|RnwHuR~bv}!}DR`5b>;HMMK8bNNuyGtn&rZO;IoLA#u^cMx`7?)GfVG=E zj7=ZMS6jx{IiExFIEm!=o=ARjjUB5~NRIER@Z!DU6%IyHQtoodKJ?{uM4sEGxC$KS;=l9NN>ZxlN zuv!_zu5e>0W8V!;J>|xMEvudwb_W}Ww#2Xp*cgT|F`3^z(bQAdUSPE{hP~m&P{zIw zntIBO2U}J>G3*OA4sF?=_5-^vd@gan*T?mtp7r5cKa=Ef8Y$=B=>d5(LKB?fYsVOadPQhJ&XSeW+TKHuJxBjaOZux5pZhO}iT>l#juK!I1*Z-D+>wjCp z^}oH~`rldeyu&#NJ{SE!^70%x7_KefBOd}*b0R+E-1~xqOU<`i=mnK5h9u-TBs*{c(J;@f=UGtoFonLaqDWCvBPMo6i%$#^<`x*Eo#zWb(wS&q*XT zV-=TcPCc=o3O4q{dK$PfNlp~Wy~nXW9bI4T>E{{XvY%(d)siw#XQ5eMyYu8ep=Lej zl3zeNzvi}eAy}XM9THyz>!WUY_Z792{}TA(np^%7a9RFRxIXHZcOOzq`76Me*WB_~ zg3Iz(!SzwMy!)1#aSCgx!?wi5J ztL-LodFuW$ICb9wmZ$Dp!PadVZI-{0Ts?K)0k$vNZYP(g?z_OL+kH`w_!|wrGChP8Aux0eqmObn~@LcLUki5KR-w)T8=a&b-)}ikG zwqySgcs_EUhqQa%KS=Jmpsl<qzA_%j8!{103Bb1nQw1-HJR6x{MJ6kPku1=s$|7XE6% z_5W?b^?$pCzgP3Dvxni1XZSbaj%oItN8tLX=RW;gVCQuQl6Kdhf)Scd-wZ2Y;;ehsXTdfI#)Y(3dq-TdBFqZExy%AJhhSe%7sR&QJ4w zQ1kcp_S(J?`h4IaB+D3sw!~ncZy|YHLdx~)(gI&r;L8hqMU9R5$^u_i;HwLKO^w~# zuPyZJ3VeNmZz%AM1-_}kH-jCkFO%olb2$Aortk$?_`(G@mc?86N-ccl7QSi=AJM|s zD7bN~)xtL_xb1Jz!nZ7VqaOv=e^d(}+rr1S@B>@;Auar{7CxbcPb|3c99{FQ%?07k zt>>F#EziHXw-DGhuCCp=QOo)ADX{A+d=a?oDtptSaDCLXH!TLv-lW~NZ{5zTwo#nR zPoXUVo=g4-$@;zDUJ^|`&zMVr)l44N>oaEVA(uh74*%V@AsEx=W#QVg&o2kIF7;^3 zgVo)SvzM#@&pu+Fy<|mn{k7W{*Q8p;WfidJq&C;1Jhs)qj-fW!raZRQ!Jf0)T&MEb z)&x7&+FY}8ZSFUp2769xbN$Nwx5wQt)&_geYaF&`y_VN6b*}@q57}2g1Gb!c+W0Kk zIm>vh3)W9PKI?(qPx9Nv=iu&h>M6HA*s|JEZUeAuEj}B9^;5S`p5toq-x%y(mHl-S zxNE`vRhw(9xxY4lliIYluZpg3>iIm_b0PI?R{L3xw$zjB_2y{SrJr%RH>g>kW4#qP zbL+k!Pg~o7Ge=v4<+;Dv7VLX-%V_i5vA&UD`?VHHpY2Ju>mH;{?EW%_lo)mZtA+0f z)<1FV1a_>m#&-s*CFOp37c|@Ue%U&$N8i-5E7<;L&UOP^M%}pF=hWiAC%E+A3%&YZrtO@ z)%14`4h0`v+YTX@yI*Si0(dcUb<4UptEKGW;H(GtX?fC&{3NjZ zNcd#9;~L%xHs0L790ArxJw8*w)*GLxVExqN(*^b#AD<)P`l)A6I0|e$+KkofjGA@0 z2X%wJHwd2w_x>Q~P7hok^|adyww}z_bg-JfX+te__Jcj|vtDMxjk|njI}1%cKF8EP z<(fJcO+7xdYoGGn>2YZ4S!c(Cv(7B9J+>1Ho9*ix+lhtE`1Q?xb5d>dzON5FgQUNG zKbl;7#_D9SYa#P-DqL+&?c*Gr2DV(r^K`I&>Z#`puw$ONJ`=2_Z^m3LV{;aGB~m%Y zXT#Owb58A3j`6u@>KTXgz!^WwYft&}!M0J3{{?XMl)n(1^46t2wmbQFveg#xIXHs<63YT&vkJ9)DzG3VB6A`cy0izJFXe$8*A=ppTfntFV`TKkl1{2ny*_}p9jl8-|53qxMtZsC#;dR2f2S|sf?H?q@xKkv zciiQk^eDQv{8slE*s|&$QtuPw9{$Fo?QzmvlCg@@_6PX*n~eV2^hqpFg3DN*f+v=8 zU3>>!TVnYx*s|)z@-(@Jv1t1q>H8$x6DJnGmo^stwds>segH0Gc?O{ zS+Hf*jpcdr#G>svlA7&_6U&o08B1*DWo*V+j+^6WtSeFO$6)8xIoysu{RC{Rjv%LP=eLlFjYW^M3zku7e zKK9r3sb(A};P*?gTAo8+0jrrjY}>Z75Bv&U{~;Kx!}I1P*U9fFGLDJtg64yV#+Kuad zau4HDe~+YQT;jy_0XXB9G58QoJ!9|@Sj{mouGC|mxc&*Y?MBSxwvo6#25UF2f0KI{ zm-@d*YQ`l_T%UkjM>7To&dIOtoqwV;+oHsgJfJo#T>?^K7^!Z zT;jyl1~#s8pKC``&v*?3t7W`Wk9p#nA8gyks=sX{uHj(q#GQJS);=$Xrap)Lg{)eh|Ecv+Pxgfs z;I``#ZN*x%{p{N-q3Ns5wyi7vD}%N3YK}i|)O^>o3f%sxYj;f4%6pi*ZIojoX>&?@ zZ*o_GzXEm)ze=9>(j%xtznq_|!z~lOM$Ns41y*bFKYi5QD=n`U|Fytsc~|ynuydy# zpLJ@VjN@l&ANBZr7Hk}8cU`!DhFLv6>w)c4d_D))Pd)9g54L`7xgXsS>|FRPpxuAl zGq#Ot+X~q9^Y0l*{TsvACh6mKjjY;)|Jg@v&WSv8;x+aGlE*zH=fwNMdkcJDjh%!0 zNzTuMs+>ZvCegT>IHA{K6J~X$!x$;MR9t!L9Fxf@{CI=D82w96lF)GxG92d<(d? z?894v)m%vKVOxti+)K4>MN)G<#jcHQYOd}Y_Fiy%uxlWEB;57k`qe%Pu8+Fso9jU> z{$tch`38F|*tI>H)UZ*;HKU&MW(TlZ>evx%9l3|w39L_2?%{Su(^q?9QA@qMf~_~_ z(m1fOtVPnc8_9C6S#4t1>w;`_sbf#LcDeiXUSNH)Pwx#@%lWqt*fQ$Y=XzI5-}Y4} zxvxJ(efxo(-#m+Xj_!}9p8Mbfz-lHB`@lBP7-Q$;KyV@_Drvt}Xlb(O|XdBKjzUAOnM?$*G599SQ9_Xv4E$+~SzoU$i?tt)H( zM6jB6vg#!M=d~i&&y(T$sK@6Nu;)VhJ_oFyy8S(#Tup!1^l4!Co$%B3tLt-}&g;(&r1nmQ}Z{ zv&q%c|1W~ovKL(hc3peT*XI3metWqXu03P*C9v8<_+($b1aA2iN!o4mLUQ%Ac^TL> z6@EEbpVWH=SRZxEUrMeP|0}_2xu?Dgtac42qt}>gz#gt2ZC7g}xqigf<2+mscARbF zI&yj1xDo6;YP*44p8ez|ujM@VX}AF+Ge%$lpquc^7Yd{)iPZP&f;QE(qA zf1ku-U^U-BN4uvn$^>tXJ8d3Y{vuL$+)j@8W+SS{y z;oW(w>c6CGJF9AWYF~r&IZ_YlMA8|ghe*F9{fD&Z{8hCy=~deQ7wH|+yQKF>i_w;K z;|3Z*WL0$!4R@lJZFUmvBG_h38JOAEJ+^no#IEVxyY4Y|`{`Yir}p#@t~qIg&1X&8 zaC2NJJ72Xhc~Aeuf$pB}-MVHRws%)|@5~vE?IYw9yQcO`@7lYkuWPV(R?q&QN21*P z)e_`=1Ks;f@9Lk?JJ>tWzxzP%jGi^?s*P!J>_-et@9UesO?Y*CD`6kRvQ5{F9w6ER)f$$W($zh1#Gca!4&8QO zV0w3}yeMVNRkx!y7TB<{8=^fSTfWEGydZyQY3smcp&#h~x{%r62YS_VH6g_3YhKj|4-#CYocq@sMqwo+Equ>VR7_pnXKO5%!t0 z#0yrNfhYA0PMk8X?%>#-X@gUC@9CP}Gh?tWVizs5$xxY&YE$ssJ}p>n{XgocTV8Wk z?}nSV2FX5biQk|Rk;}IlR?kA!cJL`I%T_4c4osa!9K$NAho~;QO1nI_y1zT(*BYaaYA5jA{*J76hX3EX7pV3`pV8CTGjXu#K^JFP zx2tQT&0FhvWVJg!|972?toE?ZmOm%wU|0Xd9^zQ2`drf%OTATTbADK7QfrPzR^$F> z-6N`f&~4Xw+?RaU8h2Ftk8~`!&?owyl6#i#@H;lVaU_;{V_NjHnJoPn!pU zTWyZ44z`W|)6b6T3+QuWS)lUdb0DVoPMGO7vcnGh*W);MEJsum@JSmJ!Ood`MmPEN zfti!1^!LnYo%ah=hobG!GcdJhaQac>CQj+=op_j4(wWwzyB??DvwQczpyOgaz5l14 z1*^l$dZzXcP8o_Sk+sHos&&=%jHvp_TXl}82EeVc=&YuJhsQa8#c9cS_RQ$->1)Dl z?aXd&#^feAabU(iQ+fuwfG8uXndIAZxE!=KaR$y)sN2V7;t;MUq$Pg)zvq%Yp`e6J@*^8Bjq=) zS7s}=6S2==V2$%D;O^e(b)2WD*Kty6%wS*F+(m}HvpN}jeVu4+-6$Q^*U|b1rcdo+ z199&<3*N-P&E8|TA2t)|h2_49UsvCRo^A%;I8K9SZcYbhZq5K_Zq5V`@9P59IcO8O zF7);G_v|rqu$v1(akaAU&gyJ@xSkc)tB+Mzp*3;t(=&ZKcLBAzw}0~g3c0l;`ES#O zA>IPiWuML?FZ*;pJbk(VoIZUAoIYI$9^R+U>LRqkfx)i6*6vdG;bOGmeHd9?($eN$ zr{VVZQt}CX0}~HxdhGaKhSpV&bw_o%N&Kh8k+@{fuRRlcZo}+e{-qX`RW3Z>UpUQ?d2FPd; zjq)(>(%{}3{G7aX|Fi9snf-_DI>07?gl%N?5cv*$0~5OX_U-ST#L2`4IkM69aa_iG z^8p@7UHW?;JEUXmnWe3>wt2rvTjQs3hZW7c-x)Le$9t})?v6&AIM6rXy-@T{@T7ss zM(`WT`v$YM??R_q{1zly_^P}~7gUA^L#J3eUx5w1c-`z8P{G`6F$zBSE zYzu;C)VDn2Q{7NZ3t^i!Vfv6^B>Sr4urO)X(8LazhwAjc*m0URFnw_RjNUKyz|+q~ zzYGdM1ycmehFJ^`zhDt0=14y zYi`HSu*CSOU7l1uLvv|a{^XtlFCHSWx(2klJ_oqV@9Ua2p4+PFE!+IHtzM_CHBjG{ z)^k>uZQ1KJ+p>3iUu+2dMcuC5|Fwo}*Y!Jzo4-uB<9IZB%inSAtWJUtzt108-H+DW zKWp!yn|kWe_c3(N2cLPk8)|94MVs1nWPOK>8(tmNb7*yE8vZ=IUf?yKuX+hSvo+Y< z7tGkZXY$Oxu4a#U58LoIMphr7P3&Xxd%BBuI;)Q`uv5FHWecs_UIcA!TOHM6ZG0Jc z8QZdL_7&RrN^N|VVZ5_iT|T{M64%row#_<*t&GoP&jIkmdZu$bHS`Eu`yGHj z_j!u@PwR4DY3ezojZbXj-EI6Z_`sw|GkVz3orf9lx%aaj)tB1*k8b0~wei_){1kXu z-`Q>UZ@2OD+xP{;IM+~l+cV}>!+bibYliX8>U#OaNv+#U&I8ByCba)Gz7Gs5+fh9@ zjE}5-1)n&RiR&LM*3ii6QS|!0uU>8A>P^)3{{(t{60&iy{82iqKcUT>HP_K`e;Unu z_+1;H=ioDX`-}VK&gw-pp0I{#ucA%p?V3^7R`;!=eor&Y*91F_t=sdR&8*^Wi0v)F zCSp07!)Ii*DBOEJo?MH5byQ2Em+Nb}Hoig|Uk%>cD_ZMmb@=q=C5(IPTJY9B+gWW4 zF3#yX)-l?-^c~qXp|7>ic2@hN4R%f2mRAl#_Zodzv!P$I0&4|1EkG z-%BZ5@7XP%SJBFNUT@=Xw(+-z@e$R3;azq8yj|mh&{ba_JF5A>uCpC$TSv8Ucys++ z4BkJ`x<)Pm?(UHp(}-$SI9qOwR|mK5+eTFD7G>5OR;Huc96tBiw4)l`@YXu-sJ4So zo_r*&xB9(3yzKXmZG4xOKUe;i|E_KJ-G_0Wwc)MvZba3iZAx!F2Zw>VORXP9?b|eX z>%B>5^<{8R|E%45r|sG^i?=?`*|9Bc-`4X}3W)=}Mx#$!l*qiY|Fbq8J9nVo^#7Tb zPksMr8_yO#J2YeTe9LD->Up8?*{1P%5v_On^tv(IehFUo@0B+GdgE`y`oGa;f2)nZ z)A$>N{_nQg-ygY-~Jx4>PD=h(4JY2L0W>rJmTskmKK3U5(DY!?do+ zy!7c-8(IIBF>CwbFTfq!>+~(|t8QrHHxA>S)y?qqyVc&v>K-(obG?*p-^W{*I*yKN zA#Sqg-V=?iR+jrHQa@beoqtEQHhS|&OROWR>F~+yl*V@&m}l{^JgUMwtFz$s`w(9~ zHtn2`#(j3JSmq))F9@c2(YMUS4WGLobyioP4Szq-QC*8RZA$Bhg?en{<=714oz)NE zth;TSM`l`@zq%c*YvRP2Q(4>${!whdZzj`r`}lCR=ZjTm8Hfo%4j> zWL!%ls(ayiPJ38e-Io#7W8l`aBl{+}z85>dI!0Dc!Kc*Edac)96hZH-oX$hw!FuU_-KlOEH@*wt|?gVxtsYjee^fFS=8IS=Av{*wIu_ z(Z}~nd4XS#Pe-*LdhZN=Y^(XmYEwARK|Y3$tsmhVzcIt=>!`LJ#yhK>EjR49sg7#j zM(gH^YM=Za(OcsBN1wXh5!HCK)-{>?Iq>dHd;Z=yYxmX*Rp)mKy!C#St8eQrzpnFG z%eVZzETfI6PC(nIuBWs9&2rX0UHy}}nMbP2oQi!)7dMODt(Wk|=I@%bw&`VAwmhxp zT9n~D{ zbLWcZ5qR&6ZTkjhcGGr8{d?=I7Oi7>1pEK}ouae;O?K9Pb6*}>_BZ&>-G@4=N88$Z zd>9{DJqd4}8|8V_QT=I{Uq|(qHvV)Qf3}VPZ5Z#Y-hvN*x7$&@k2d#xY-jZWeCD`u zWA>^`nO^nRUZ9r&zf*K*#M zg}1+VUI&eHpxE0+R-2&lqHeHWzOC1Ub^V*7&z-xG)t>N~d*=t9R^Z0BFM94P4*=(# zqH}b}usS-b32nT)jZbRhQ`&fM8$Ya#_qFl==^Zti$-scW2Y|rca z(CYnqTXqEQK(a@?XI*XYAl_Fiu8AKGuJ{Uonn3c+xRq#2hT|G`km#j{HWCIcjurD@@S&{I`Odcq_h4Gq;+z(P7?k6 zE@W15;c?vkPJ}(JcN(`s+qT}d;5oAXb|m_q_1pZ`-1$ApEWZiiy=7}`=d0dFn@PRX z`gr^5ha$y3@d38o_(G)kDS_JAQ_wx8Y9seutQxNpTj%6>-^lXtMJv7s*T$c+< z)GRlel=^L3?YKhY6+x|?UT7VSc1fYxo@H+=G;Vup?XE&I4*h;zXe|3$d%DmZX8m3( zG=JAnbFdS$zh|f|R%i=1+RBCI?-%;5UucUo+UP>_Hw*ptMB9ORco@72$s>GJi`LxV z9ki1T@zVZ9435L-+CF0Fy}fhgyL6AtRd_j9(dvHVYroYjT5Eu4nZJyMej^&cjD=d} zFJqyW`O8?SW&Sc2zT;no`fSIske7Y+{kwMS*DjxHA5p(^bj-E+o!E+CrHfy{mlQn)99sp1z8;&C~dLPiot#OJ26^cXaxv@7{aLJwI&A@9X5Q zWx3zl$vw~H-WOSa{N-i+-Xl3~DepIWmd{vv4XI&)B-M4Ko{gKaYZ^Y1^>p7PFI_Frpv09(!PPA3w9?|9jtBo$S zc^hp{@{cl`@>N)+u2b<+Am?r6I`J-*+@$t#;xT5izAM8Cy|$|_iRzrJLQIae*s}Vp zg0JyfHhjs3>to$ZH{4jPdl_Xu)&!Rm6yRGamE3_gl{Wzsx#-ck46^6JL)33B}{ z@3^R~MQ)sF%duDwZW&|pC9HT}l5O&8qSjh{=&U{iPoJ#Qxc!#Eb}efgTafFc-fDMb zHKt9|e-*rTXmsn>x21PhyMc{kZk+Q~d&8|)EvZ%i{M7;f!)9G+zpKrjb|yEvyrs=o z^|krVjiIyhJ&R+iuexJ&B)RLM#kr3HXWaF3TxXLTyXTN&c09Q@b;s-ka_#D==Ol3I zX=(FUr?zS8rdI4rROkMG*!4+DneZ|Is_M9{*JeGr zzV0U3&wI%&uNL1qU=P1WevsTc9hVjPV)r3(wdmUZ|0di01?61B*0*`Xy*E%Zj#1=~ zG+h5*HrTcj=cC}NI&Mt(Gc~V9hrimgZx#L~eCgF6+cNwu_%*LTy5+izr|aWwl0NG3 z`2hUVUY*-`KF>RkW7ayf%SVtr!bifDQr>%qM;_jF%VpRVRs=g2+RGz+Ip$_1h<^5Y z<;I}q7|B;@wABBJnjg1C`mh>Y%@m(?YM;?V{ajaDX_HY4~gKjW)V@=v;jr&UrJ$->l1DGTJ`9117XOZu=zn{#Q-=e;VvKX_tFH ztUc|`M`tEYdVVX%bOUh4R3DQ^a>sN-eDt&ajmSOJtY7Z^w1;bJQ?UDqYb)(-R@;yJ zS?2Y#a5d*P+U9Wg=~COG@zFPaTQ-`$>vG1~-c}^*v<}PYW2~deJty@^Ec<|~>bQC? zMu855doQHjJ#AXU?aSd{`=XZe)4}hrarn=|XTYD{>)|^S-ymEc_4v#Lzq0uBdVM(7 zv*61;Id*6~j)3c;;C} zIR(D+sc#I;&o|)usHe=S;Q6ol+MVIwgpWMtiJ>y5!Szv(&*|W2@7#H)oipG+|I)TY z<8~%oAN7{eO8Y*)ETp#t6`40Hp9X~oW))&GL>0WGT3@(D}qnm7#TT z3H*%x|eXjLdaxd6x0^x8Ex%)EZkND_kfBr=7p=Nw? z-!piG`(B~szDMxL{YD2D*Jt2K>DMCY3qH9j_XDl_l11TaNzu1Qb6-tL`MuHqwdO%X zYkeGi$7jA+bH`y4+_{MVY4EBVJv#NC4p&Qx-&tr|F8Cztr4`q=;A%iP~guAtk7-wl7{;fJ?OAAA>N zALL%keHT>rX)_1Ds`rL5eC6)CZ2tVS^W4BKGzE9Gg zzVyPqKQ-A8`;>kkip{#ir@?*qU|lQY@4F|D)a|<`>kjwbliX*obHL7}=aaF28_u6v z*Wa=5-BilUeK#df`!B#9-n8$#DG%%WcjG%7u3hfCs(0hR)j^xQW&79G8B(cyn8wK05t4spdBh z@$=zZe|p8C_Ai1DEOGD9zIqw_!>bP;;#bw>e?3(GmYTmd#BYUjo(`Rtx53qt630)_ z);j34p?>}pZrt`&Tm0?@vr>o7-}~VkZM4zQJj{VRHtJ=+AFj(SJyhRjBs$p`P{OcNHb~n~IYAokY0p`F%vV zx!*&CJ0E@v5w5@AL4-U1e*X}z-R~Sq?zavlKctOMXybnCQ2P6=L%6x$I+WaR9ZK%E z4khm^xbgX|LuvP0hm!lPL&^Qtq2zw+P;$R@D7oJ{l>GRDJ6?Y05WC~+cMjpk=Qj=| z_Zx?BbH8yY`E_mFZyZXy-#3I?|DW6V-`cp}J(T`_^APTO^P7io&kw(O2sih8hmx;a zaQnY{!L844A4-3}eJHu#J(S#U9>VR<76n)L`-j-g{q~{c`?c{y+PL39l>P&4+;1XE z`#A-7KK&*_`&L|KmIin&6T5GFO&7aoi^s6jdresZ?megPcDH1o_u8={Ts=N3H9o%Y zjnB$x>hW2n@$nsCd`6+EFHBU{yDAuSwI<2(+GG1fVY7XG7v}ELa;r7Ab-?-=fBNP% zM|;{_3+z3!ef4@H-;BOm&)VQ~$knxbeNwZm_nzy5eTG;VZ~Z?BH{NX;n`OLasas|g z`lrCYkGqLBd>&jMuIBs1tMF0V0IvQH63eII#^Uu_ z+fSPIJ_FX~n7l_FsmuIr#@+Te0o%UUcKvO~zIeUWmNqvB+rNza7GT@-J*m$fK5OW6 zLNlIQf}IEbtk3JUn)TW4R$$w(-=oPr?6>;XBsKdjPWkjBHuJ={4cIZ!*LwlEHs@|E z*ty%Iny-E)*$%E|@}O8<&U*%Jn=!-2xINfseCyL@tUHo>7_0UjNNUC^{*X}-tJ(f2 ze0R}KBLi^P&aQB^Q=75c4XozxI4QF`yezW^Tv;w#=8 zY#Z9Y*=YNK)yI(>Gw+YoY~M2bgFi>IZutRV>vmkUS;l)P^_2NMSS{@y1orm`Q?!H8 z4s4QZ$-2J)w{Fj2>#&@DX+wM3I0S60;aym9sr;(R?@pQPh)IS5P=5G+5@2Sf?=ldYowp^e3+m5l!BKI&BeP)u> zj798Nxt5Pmqy8HhTi3#oa5eWd+e{hrw0RWRI*mjB)aSYIMfj$q^x;cjHJ@?6*R=6v zxOHeV)?>&$j8)swBsF6d8>_z+9tS?Q;jXjUVCVjG)H@q~JXjy~%&S_~#aF;57W}JV z*Fo%GYvU)u_5T+2+qaX!`l!1;U0YuVTaPyPO0`qKwxum=@Ec&dP_IGl#&-g_dfNXc zxQypCxISJtt@m`Wb&MtHt374U1fM}ltY?AsSHG3GoZD}K)!lcmrbqVaY`FSnBj#Rb zPlc_njPruVo;be)Pn`N{&p2Gz*gO~Yvrgl<2yD9<*NegWs;Avcz_z{X3cmc6>sEW0PY zbPlgUGj8KFuISf-jma2nGx~L4+p~V_Q$L6NdXjZnP9N9Px#a4Omp(U;)a0{i`$q5- z(jM&6#&Q!_&E#S1#+o_!0lGfVpe3H0;ob)%WzFA$uD|v(n)Bd?U}Mmh`Tr5vcAS6h z#{GSA_002+!R7k86|RqV*UD{R_n5@=6R=v6*CG4&Q#9N1J0t6~9(@zb?Ty`7^feaS zRd?^p9%Syluit9i33l)HcjJuf&(PE}u6KdeOdc86w0k#t#z0@&y@OmmeY_XE1}XPv z_kk_rHC3DEaPEumfoo6M2f%99JBM7J`{JL2m!hn?_WQ}zQuY_%he+AK9|o&=jnn@@ zuv+|o1^y)|_x8UA8|RuNZN_Q2N5I-F=iEF7_8v~#qhNXJ{4H32ZI6@7y@wk`xhKGW zQ)d}%`uv7mUHl}uZCaOm-tXY+k#c|jd$5|L|14+&6jySRY8Jt-E1eVA4 z6xcOm+kYXK$My`^dDZqbxm=rT_^;sf@ma83n|*u^Y@aOmH*)!AyveiN^I-ptGUKp4 z>$SXo>Aya?*Z4cwZ#iBdFYh(}0oNA4e-?h_v)_y8+7kCmVB^lZd>QO~``loi`sHE82`|dfZ%{J_7`t=@~b?avwZ;`9nuJ~ zN5CG=srtesHRn{E^495`#%7*a76bd6PWTewb*MkjTOWn1eHJb8E(teY=Tm#;Gxm=) zcKhS+GJXSRnb?;q?8ascmN8!6hiW$-_rGPp{-&hOccXH#??wG>DdVy{xZJ;1fU7x9 zE0KFRPU${2CrE0>BDO#7ZL6zsUgddX4Y*pKH&VttZLSHnPUFzu`fPJ;au3_oXDyPNZHjHv z^K4zP-xtb()hGGSc*Y{@em%H8>dt}hqSZXtaH~EARx8)~`f&B!TWek6nRoj3! zr4PB6+ZL{t%;hjTs`ytd9dwf?>rD}nfjd(*uB*~H2w#nc|V*vI2fKeF6ZD2=-P4( z9uKyxdgAW_yJn2*5OR5J6T!Kkn*f%_)&sU5mg^>$$2J+9d%a0uxqef~?VEnC_d~(n z2WaaB%f*M08*BWgf^!ew2bQO;0r2uo9sOW=${h|~gk+u5$mOxk0DIllHXSVAjBmG` z!z000kvkst+y2=P<2O#@GY;FcUds=XW|C%+jv%F-qrlp1XBpa)$M!|AWwb4eO&;5q zz}BU0Ic)OSz6`bvZL4FGiw__-_NY*xzKX6b?@+%6Rx{0R?$1sFTemj*c`~_L zp5wj_UV^&SwI5Hemhn0T?6Y0^`wh6-an1GVn_v(7tL;>hn*9}9=jr5r=Wd;@ku%|W z4mtxYPkY}28-wM}BA0K*)zdLM8|=ReYJHZ|uHR|o+ET|kU}KhB?%QyEa=kehtY-3X zti9gkeZ_g``dCk{spo^O!|~7lcL7{q5982BEoHs~wv6{mDRUv*GPY?MebiFsBCut= z-lWXMaLX8@W%N8E*%)Rw z*ZuFJTaSLmWejTW_gMqx@|*+aS=ZkKXI+=q*Q?OAWzAg;R?9hdE!e{~r|lY&nrlv+ zb#Oh{dv&ib?wQ|*tEK%Lz-hm{pS=-XTlW5&z?N0_+GJfn06QMbk+kPI^=7cw8|}F! z-O~7aP0~JxTp!2mI=J@4{3Ebh_>UW&SZ;;uqaN)iVD;NbDfd&b=RkaJhwGVB?&Jyj<^d;M%O?K5}`=JOECaa^3wLU0c@OgJ3n+UHbG8 z-1_443$U6#`CZ~+xMj6@5B5uP57(=#h$I}Y)E1MC>->p3ph*EzBu z&Wpb8Yj1(edHok$?QN3t@h-WC^P%k>(oTFsVm;!t`2jfB`1iqbZO-|>!H$*jdhH`4~yfIK+wL<6z?`_v@w6)H5E-fYmY{smDBVEDN?BW7FR@ zjAME7#G%h}BsJp@Cyo`s?QyJ#rk?Rw39Od!NIm9>V`Z@I7@PjK;adMMebnZ;;(j;^ ztY(a>ffJ+pCrIm(j8U9(Zw;_9=KbxOa5a;M*8t7+p@k-f-SGF-{8qJ#-9SWkMa6w>iYZ5oji4I2)2$~ zlQ#mZ`F%-#pWYa*?wlBx_4uuxdg}QM*m}Y@0o&GW&U<6t6s+do%}iUH!7ZmPZG9H3 zUbf|Tfa=!mylxK8ylw^d@LW{ilC(X^@f0VXt-;2ed%DqJwU?Re_>6(uR(!Srt9gHt zeryX@_gjjT8ww5t4%B{!i ziS_K%_&UxzgUf5su5h(oNS+6~lY2N`+IAy-m1I3)+w?lKC)o9z`@X&4{)XjsUt9XV zH&~mo?LjV29s7W-c@$E-0ckFE2c5E-cUjW;O_{vLP%fxp)*!uJxM=p=83+(T6;S=E2 zo9CQ~V13l{Ow!WTN$GQ((&f!$JnsbvjAsVi zcrs3dV13k6W+vG22%iNm%NzmMM?GV9B-pWa%(Rzdb`;!p^l{7v$km*8`(ez+9-I4L zY11dMd=q`eB`;}7_c$1!PBKkANwdx_)Y{*gAm1}^uX zli_M7kz5<6kbAf`w0)iQU6S>Pt;cou4K>zS_Ks8GYI(Lf4eVh(+P+CTon$@Y#JU66 zSoPPYPue&`jW+W9bS7LaZF~!yHng2ZI-6uY;$DC1wds>KzO6@g1;p=5gAX?Hsw4xA9B}&W9Ax2Jl_MCYv^jY+Epai(6!_q&b_v4 zNIxK1kJx(rUFLdl`J3bS;pOi#H^B8#H}31m)hs`o`fmiQ<({WwhHa&dyTE0v_rTTeCK>B}@ z-@x4ulcGP0?jD(xK0JnQAKXi=%kuiB58Ca6@z}O8qz}IZmwk8=uJ#1UKK!2C!|~Sk zJJO#>)+0_me*pWtOk({bTrDZFs2hv*T8DL|jz5FTSe}Bb{e@&K&yXh;ZBLV)BUz6) z^*jsqx58X&{tD0dXluXL{0&W?q{OOjthQ?#wv{%X2bZz_9j^8Q$yonMo>;a0gQR9X z;?(mZcu}rviS;FTV%63j>&s~RBqdgLV@(_8W$gB$9CycGo;6`E&pJ1k=NvVcUrTNu z%;l#v++4nU!_DP+PB53}-IcjK?v%CS<$0ORC zjpkTJd#lmhH}`CQn|d3~^7?DHZpZXha&7-+HZ~>scWAxtEkg2Gh_nfQgu&dkFz)r= zDR+GQw>HWJlv9w%8wn{7ItYSyp) z36g);Km19sK0a4#{~bvm_53FLdvMyxH*d%|2MqzesBS8+obc9kBH{ z{@PQIdg^%>Y(3%cH9TwheYl#*x;(PBJ_LWzq^!Yz!!4sNbMhas?PyCo{{`D_>aueE zOsQ)gH2t(CcC|6|*)i<^TSxgGz7tJ7KJzs`<$LV;(bR3nxm*BjIrWrV5Zqq(LTKt) zHw%L;tDg1$5io!1G1u`@Xj{ zntJZ>mjSDpJj~qt9B=Df7QuS;Gd6h{+sgQOtVqhawG!CaRw4J?I-Yz5%DE20*KFhd zKQK$X{~yfoEy4c3FvIQt*n+EXU+`7oI}}{I|4&SF$I54=QKa%&X;rv!+i%a&)!}NZ zk@C&v8eq$5bB)_4wbZ#5xU6$+cv)Z&g zwjn8XZVa}ZHtTe3)l%nYz-67Az{@%}gR5;yN}ZntTTYvGIzMXGIh*m?8tiz4j|SV9 z@G)TH2;T;59o`f9Z(MFmvJT7XtKD{-cWq+l+;4-#&bQ~3*ty=3N&3u1gn`m%sj94 zcfUFa!FKYE&%t2JsO#go{{^sPp1oi^SWVx|hg#}91Z5|QQDsbw*A~EOaZG+CcVQrA4=}w9Bb<(sTqgZ zdR8JojI>I_-5dJA<@quISL-L`e3=HeoVLu#;b3)>hnaKYzN*c3ZQphs{~2J%Km8d5 z+n+q|&IGF^d9P==S!kBkZr$>7&t3x`k57ZFv~sZ84&I6AGtp}v*BvFo}C5OS6lk< zEwJs3AZ1+72J7o#Z2G9B%sF7oj3lMZx51X-PhCbIwe;s)uw`6};;cpQb=S2aj+^^D>d4A{rF4$P?tNzAetXGkH7^^%j>~m$ebkLB^_nNH>%q2btoqxgaos@fVO;urpQL77 z;>2|$xIM0$(9{#x55VOZ-VApP)iZ{-fGwxZSksnyV*Mf5Sd3qP<1p4AlY1DeK0hL< z8LK$4-Uc?-TgemKPr&L~OFspx&B4z&QjdA!xE*Xe#-_h*7{{ID9>$^19V9j55GRhG zf!pJ_3r#)aaW`1aaWan7W1cwf(Vt{&`rC%*p?c0k_wP>`6vuibV_Bcvz20;1etgaI z+w;%iN0a7|%C+_&Tw8vJ`~{dl^>swsLnQYU>(iF;eHg5kcNLF-?UQ<*)qV-)Po1>) z6Pt1W3argG<-Z2=ryjd*fOh+Vnpa{KPJ@G;8Scey^l zZTMzQ`;WsNJNu=t+$E*J>-65^4`Aa= z8-Ik`M(X<$SS{CwKZ7l!o_7s@0rRJ>PkZWq8f<&squSOpVE)wSrDdKX>EqZthR=fa zaeT!YU-#q9NFJXdWqdbju;aQZ$+7+{xnsLI`E%4!j`i~mPkb-Ht;2EACim|q7|Y+m z&Z)X~>r%7qKgnMvz0`31UjgfraeNhQOkUHRtJmTBctm@n(UiT$zXo`dq^~yX=%&4Y zkiSK;&GGbTHvVrnn*Q^`cO=jC^Iu@?9*(8F9Lud+ruzRi;#fLXTav_%;O7DUkQ;rz(U`Pr^zVt;JSiQ~OZgPosk8|-|HB{^5ylRH;xAECZ-t`;e{ zjnm*@8RX zD-_)NRw}sVR%y8Xa&KD>Zu^dbb;+}rEDv@sQP*zUYL<0fuLxGtX1j82qp+eP|3b8OaW;~TZ{tqX3sv2A>ZHojXMKcL{o_l1Jn{vid|KC$4o*IjV^4{PHy3U2u? z6FgYlAJT{tk2dNpcVG6}7EPT94%Xid{#}rPmVg|Mb_UPh$BLxQu0ecw#B<-8Mki zmRL3fTUOmzK24rjv~5iK49WJy#^O4%kH(_EHhmJyCg3ubP2q{9yx-moU0Y)LEZDN@ z?m43fWDBtRBjC-+)4qDyzH!+8DEzhr8)L>zUXGjN7vHVGj;+3)Kl04y7_jfvN0XQH zxs5)g+?#F-Rx?`FL9 z*QQU}*acj+u`66{kEV@1z-dF8D)Z^wT`P`-2?|ech|%nXAu(U1M`d<$6C5U0don z2yEH>UUV>6A9d?>PgB$1cWmRq?z`ID-{jgxp>=`lHG}OCusnZfWde8&d@Q5Q^6r1? ziKPc@d)nL!<(7AyOalAP#4_4kD^tilTr1iqlhm9mapLL)8<*D=v3C7z=P>e|yM2w# z+;`~xt}n;uiQNTCxgp*_&Qw8b&@rsX8Eic=QriQ0d`*V+;A#bE#*%E zt6Bb=OTu?9r<@%z6DnEdgxflPa|2Eb&DS1 zGiGt(x&&-o&YxJjevXf0B{z@%Wnld?{+EN*%JEk#$KSC^`76NApY4frA6oCvt&r>g zPvs``!TX|dq~LuDyl;cOm)ehHocoh|e`>k!QMdO|;a3&hGS{^6>kDqVo7(s-ZTu$% zx4j1nZutk>_^;adqiy_&HvXqJ{&X9Eu8qIg#$RpY3yhdM9>%w5!F~2wy5Nq_as}6a zg*Lug8(+QPtKz>_!AFC~6kPlM1-Jf#+xWzS>)&1QE5KJa+;!!-cQyTWt$6M@*Yd2n zYr(D&ZO*$q=iYVTHLzJmn?9}`^<3}25B^{GW;dW~%e~o+U^VM-J;@Wp55Q`wI>k-?A z>v~U;=db?S^hq0cg3C6323H$L%DvfLVD)EcY+v%!t6tV?{nqQbe>d2+(@(j+>8E*o z?*Tg&`g)$qGgtS5|JS|QedyX!$NgZ-=H6@$SRZwLTvI;>yB2K6^II+v zUF#W-$Kj60=H%Ma|KEbOS%-U(Jas$?F6;Om+&WxG+ET~w!P@d}{*Pc>>UZ*nupebnl*>Z$uBuv+-b zVB5}b7_Wf!QMdey0o%Y`-&x^TUm0O_DbMzoZ!( zZN`y$%u~;TV9%@Yh2Ykk@n0COk9vGQ0`^?Vd@lmmPu(`XhO5PYF|g-<_~LMVGM`I; z^-)iMJ_^p7(C#|5Ztn-QS=T7ECBfHIcdl0-gRA9w^>MI=alA7ZwwjR&TWx(!V z`R-s@xSGksSZv?>4{d3Gd2p`T-iyf7&lSPOYdP;r{`)@M2G?yc&^W{hcL z6>!^)N6v#kxb zUUQ#S)+PTW$-_0aUSl^;{GS3_N6yFf!D_~D%<^?e)@9vd%Wg>iY0}0Gcb+~2F6U_z zxSGksc}lxmquH)~7-LTIdgXoOHe!-K`ukpdTd=y^eP%4!ameq9+ky2_ckNncd$2KR zOS?OOT@(3Ta!0sY>fH(Ky4b7P&vph|k4Lm!8qNOX8=qa#^wnma))W8Tz}op!9YFNP zzdOY7Q`esM_W-*da{lcJR?|OgMa}tiZk-eJ^lvY)S_m)8=v>|>WSwI zV9$lrJ07l2{;udDV71ii*ml9ypD{+t<~*8!W_{Xi%jX3(@kDaZL2)-Y=Tr~azL|SY zO(vg0@^B1$8@qX8JQQpl;fH~ZIpNL0l1pU zqddnhM>7ulbS3#^q?}{l1*_>}Tb?i91FOp&$E(1OQO>ce!TPA@9J>Z=4BFD)Yr*9? zb{$+T^fiPV0&POqnTfS+!3#{fI;l1EJU=PnZ7l7sXb%yOK`@b z{7vFl=-P4*{cEtAW04qs12=~3agV~)HfR6MzfbWP+_Ksn!{3s7IELCDC#gAx;?(^l z*t!$v@8CZBc}>%%pMCv3Set!wUCC3&AHmjm9nSa9dQP1A;BG|gL*+0)WHR~8foG*jRaeD=>=D6wOntlzQHSPH*kL?X`*0krQ zJhr#MS<{}Ua%1ouwVkZte}P@Y)@>d7>Z7mwfp+V34ZH((U5CF5)+hWuuzk$mcX%I6 zSL$!cw7I6;S7ZO6v9H(EZGD!pZQIfAKA$->muDTDr|l2HmT_+om-}@O^?P)YvR_Xq z@WckYXLgfZFO$f#FaMYNvM-yD=orrDYvT*I@x|Ks5)Du63~$|T_&g2Iz16&MebgNr z<57!$2UsnOx@W*5_{R&aHJj z7TOZaN5E>~i!?m3Eeh91-SUo|TKpFW8?QFUQ6Aez!TN_U3D!5~@yEbwuDP_g6x?#! za*y(Hu)2P>&{ zjql!Y$HhHp3%Id|ZwWX4>_J<>^-<5*Z4J)YY0n;HU$mvJG2pW9ZQzba>fRQvk9z7J z3r^kIQ@7)yE%Uhp*!G-z$3!06PGHAco8u$*H;IvGJA*w>Eu&2z$5=h(cLUp=HpfpM z+aBQ5?O4iF_nu(uwv0B*J0I#PzZY07d~dMrXYG#z>!a@4lV|NYU$aOaGf3HE1{>_N z!%UKE>2isoG-vi;6Q_uayL15?5 zzhk1^In*a}d~jo57ENE@zxzCHn_qz2u0EdYYU9E7L7QVD&zSg(|3#9=k))iDM}ZxO zFOlcDZUW`XbFHV1A69VR*-dMB#%&_pdj0LfxVqu?({D+&Ip4OSK8DzBYZ6$kd=8ro zSC7w>#;5$gtrtx_V|FOmwwzk6h?<}xwrQRdJ zYD<$+$B|&`u#eUu*Uvg!4@Wh2*NQl6#W_2cWhcD{}wxo(al&mR3{>d3lr zT^w6*=i~T>rw>QNttaR7F>tlDNyck^YWf?~abRP}x|$7leq2}DEMpt$em89&PXN2_ z)wLUwnsu&3;je&CY`Fek1?!U-zXmpr{ps0k>?eUOqaUYZr9FQ?^JK7glZUbCt=)6rmQ|nKtmkiojYC`NIv0E*F_h=`d1&g1;e4=K z8N&s&eR%BOL2K!)7%qfcRy{FX1U3$BiQ!`KiNvrE6O;M91Wi3LTnbhzW4Nrg50CwF zw3gnA;R?8A)f2;&VB^r1{pq`4*TtG7_j`R@AL>~juJuz$9w(7<{+(RluY+A5-yqNV zcO!M={4>9$;I64#+xVjecm4gojX%@IpD(!ezgTd~zf^GBd!^v|zg}?t-zd2LZxvks zcM7imy9L+({f6fq&NXn~VO>pLo+H=7wdLFG>%eO6x9@Q7eIM-Mdl_xllhhnvvGcPX zzH;@ncM~}6m22t;=-M*pH-j_h+PsFi7dc? z_RHJAt{K<=FUfxjxBj0rKC{8MgVnQ#-2qlJdDzapq}=P=iEh36Ik|`PsQv&+ z&3O~2od?0TllUJ3I~R%n7hts{|DKWYJWR5z_Qdl@qx;?`ZJFnLf?tA-&vm1(aTx2b z$rGzSzaps_tGHZq>WTd^u(2oB$HA`SB=;W2`nTx%YEM6(0GIuI60Vk%dHNli<+VFc z?h|U(^GEW(kpA3o+jB-Hho?s zS5Mt?V*5APzJ&h=Z2RH=1zRTTj%8Mt(NA0UuzAq4hn4pm^P+3Z^UDaZ zb*Ov4?bvsM-EVvz((ZZRLGHPrt-L;-g^$Onq+B1rS>V$Oe0qV;Xt39pGfAFr-y+Ym z&-}EbE&L+|SN~|iy#_7W#+NF%{!6#<XHTYj~IYhSzI+Sh60 z>lIx8jSH^-=r+Dx!?Vs7fIFVy3&I`K>^lp=^-<4#`odu6bvu%F*Pr98o_ZDoJAYYU zi^J!@ld?vZK)0^-NQ;6kr>}K7SK3qO$H1-wZO)TCwvU62L7Q_X*XFsf4A`}y&H0n( z`=DjPJ=m?Ip1`)yH~VbK29!>R|gGz6RKM^1f+JxIXH+=U5BuxO?BK-FdNY*PFJ~wJz9s z(B|5cyAOGOe-i9_8OvzX$Mvh8K7I=9Scb0;HvZh7ZvfUuJ#B6Xww~-Q8-dmIwGG$2 zTKqo^c5cEy126l(30xob^nX)u`mf#oC#KEd`|xf+pVawTaM{M@aOX8+yaikz^|Y}i zIBjSz+t>ckT&y1pDu=tLyKcp%(w0z-r+;gFV+BKi9=BaDCJh+pgfHxvnH7w%yS6*Pi-z z2ODGdpgq9)seA2^=i1>ooI~-8Bk_mGp(tz!~-`CL5(t}Xj~FW9=&qa6xX zcR$Wvau_`Oh(OVb3MvqI~?p7YIAMMW19i?oYm$!mB%&{ z>{x4a&C0d8-y8wI_YV_ylozNzP{;7ia_&(|71>(Q2aa=ktY&ARk6F82mC>vOD60cURA z7vyQ{RB-0VJwl%Qn{R@BZ*CcFo;%ie2H1YBM$+eWlI^+&X%oA@e2bJA&H<~1e;cfS z;y4%VSZ9r&2UbhU{qp%}w(b40by|Z(}{C7E;dj4OwSAf;Bzg!76PVX_l4wY z`a1{LgRgCD*OANJFSUIiyga$OW!;HT58xdVC&ge9CvHzd%#ZI(rzLb!K_(u{~1QY+v8lep%RzU*GIEziMpW_uUJ= zkEFkSzlU6V#_HE#*FxswF}T{JjgNEiIM{L-&)UntI0JPvDH7<+Z2$pTV|Kj{jfa>M8#eIOVNNd&cqU#^%1Q zU-oVL<9(_2^#2)fc}_hGFVCsJ!Y?FceEtU3M?G~s2QJU4=i&OPXMSD)+m^O`Z~k|% zx<~5z2fU2upYSrC7vW_*FTwRuPaQ9V%XnUa>!+T0UIp8hw#4%qSlw~WIKSR-_fzB5 zC+)leE@OQYUdH+syo~j2xIXHs<6q!1)_36gsVCNV!M3F>vAzdZ_kJ(2z7NluFm8Ra zCO&9vt_l6hHE|`H_N>1T!Iz?yYy976>hbwc<5RBj|DvhK$Boc>%5^-CCnhN|&kIh> zme(HJh{9(3`lde}jm`e(H;3GNE7$mU$+bJiWK}2sIe_xozSr=K|43|}YvJ?3%l%+} zxIXGWf6DXx39POsd0a!v^XIh<_8NX2$!EOpllx4!D)~az*YM-XS1$N6@UKq(JZgOcF&zN zs@A`!y%yZI^|8OMPc`G1jo&(8wLFKe3sy6E*tTtDANVA?{^c|1dT{mJcYg}3mXv49 z_0g3H3m&RfA?Y={YkD5*K_JI&u_Eaf^FM1uD@+0 zuCZY4#z-?1-kGG1v*LHi!C*EA^NsuARZQZLIp+M&jB9 ztlhYFBlj>a^<7D7#wAW%yMx=~+5=5JFqG zF7a*Ej$f`s6 zkDAXo*%uCj+pb5nzDBeC?Aue(^wnnD))oJLuy(pO^!GOh;PzKtyJMnO-oxBvqa2e- zn^4+&lN$Q$TPA$4;oidntC{>qA9eRi%d5qI7FaFs%8md# zck1yus`1G#!p9QoZ zSJ;kkY|CQP&wpni^`8JglBAE8>cp0-8JmJ;OSu3K=>JO*GGO!JQJ>u zy62ngK`s8@0;}cU#W@@7+TMnw%`&bT^_(~7fYnmRx53ttd$@DK`XuEZ?mRSowI>#} z)O!KgdUKv#2sW10NZP(bvYcyHo7nYwV6%qD!fH`{us>OX~YB*!j(~*lh6k(A0Aud=*&D`#dA$9YwLP&)>e5B{e5(8mbs2xo-w@hWJ_5F` ztodJp)v_jj1@>Bz>*ufG`l!d}H(<|&^!-t=e(Lu3VRAM7UDJ<)-FL!&3qP%~lT}af zAIoH3p9JfpZXDL}JFxX=vyI2d)m)42y?+3!m2>n*xO(RDPhjWL=kn6;&*<9H=f8k0 zt8QDrCs#}Vp9ZUCFM0;-y7ro{Z4P;UdwCYFJ!AD(u-al2$iDhFxaF54X}8U%$ko&4 z^I+Fh_zPftQt#iv`lws}IdZl5{{yU+d+L9J)%(uw`fspW%Kr!K-jVYE1?!`p zGA`8H;aSu3xNu4O+J-i@)HyHMzGW?qfUCvc0XF8WfljzS>RDSO|6k4Zmct+n15xx7 zXc5o?J7gg&ojDQ{31MH` z)eYFq0OPH%pB)73%#-uksiFY#5c?)>NUH9B@>QpU-KiEnMyk9hgI#b#YQ mk6(22=zZuPosU+wzs$Oo9WS$2_nviziRaJm+s 0.9) { vTangent = vec3(1.0, 0.0, 0.0); diff --git a/assets/shaders/vulkan/terrain.vert.spv b/assets/shaders/vulkan/terrain.vert.spv index 2b7fe0b6ca6be47e5c4482a55ee7ba5391183675..2b49f12afa59a1f96cdf85b10adb1794dc6e519d 100644 GIT binary patch literal 9292 zcmbW5d7PDH6~|wg1ym43QB*{bM3hBPa6?>xL<$uH_4BcEyv*Dgt_*XhJ2O`?6-+HH zE6w(-%*?d1EL+q{%?wH`&1^F(p?x=7TuSZxeeZjQ$I1Tb)#G!1zjK~*&a=GdzGKty zsUxy%ShiC(GFvz-%b#7c;V>CoYd$VrzH<5OzW$Ec^XAXdVdtzV_tR%gHZj+m`R}?? zrAJUhu0(D?Zb9xqHX&P(hmdW^Gsts@35Fvr$Vg-qvNJLk8HbEVCLmLh{gEdA8P2qO z(Oa@+{@I?lg?dMUJId|Vj`HGaceOTsVD19^npjJgwIeOr2x<7i2 zEbA?`uPt}z+muZQSKIq@eWtea8oV3PyS|~%Q$Jt5Ia{QEtl|V&1YZMg@2>Pp zSw^ntJJO_EtVUWjIt~d5poAdN;hY&&A>TT|+&vf3itU_9TC-qp21Lo5Bjb@@KktK9?Z z%Kf!U`=EI+-)QaZUvIV6-_}<-pX)jp(|4)6w>~&2YWMw<_dT&=c>eE(>&nka+>%`j zF3xa;o9{VZpZm0}DA!Ie*ZLcG$C&ay+%1m0(qF0e0j9>Z_wsBVq45r zSyE|S;X#|{scoRUd(dV-u&4Q0kG-w#Y_cvMsIl$#iPpT|1)mpR_}|Ca=uI7Ya{}Vu zocG33!H74*=LeMATeJHai+PQPRm|JMSSlFjJpkT5uO)jB++XP~w^cgY2Id|{u9m$2 zmR95q?xZq}MD7v%`qr+`uM;XvQ}!rhi5FS>R`k`~)%LZ8huk*pizJtO3t^hGC*j(= zs{^#o9NXjT$-3k6I)Q8mQ?+Ry=7wK8~8&f}_R_fvm&#GUsdYq@*)&*{LQK_s?Z-B>{8;`=e>I;vS;x- zU{5%a$bo(5#}479f_=h|(3#wEIoF)`bX1}=e4>}9v4V?OOR1sp`_qVHefD+lzx73r zpHRdMynTLC-tV;=%icc)v4+6=q5Fva4Y1eDD6qYI65?ZSdrs_)x=*YpaJru0q8@9O zb8pgl;-T|iynl^8Zf;|3MBHa%xfgO*C0ujCU6*j?)9==Vv!~15lW^7{_fW#w&*dIZ zID5I=6A5PzmvezppZh82-44#aEjKygMi<lq1^pSxpl&e5+a;nd~aUvu5U=>2d#V!tKDu0_15z{VACDX_7{ z?y>$6Z(D)me%hnb`)7}GPn@f+En=&PBevhIVr%y9`VAX%)wkQ4^PVM+I_y{CsKZ_* z_P&_gvlM$4V*8ZXyCe3ySnRzK+ndDho!IYXv3E#p9};_ic4Y;wVG1={yZU&1%sm^{gZDDYVY=N`AAPe!6H@!1J>uS4sBIr~d%9SD9i!Med~W6EEoXgVd$#-R`mLuI@zMWvjKdIl{l)fhIqm(7 z&NY|V4|;2g8ZRucYnaJRa@>G;79q~N7_2SkU0PuGTKnaU#tr-y^kIl|tV?`l!A;7> z<$c>&igW%SG4H#W>-zj1Fpt=ND5w2Bg>IgS;EynRU%Y#hSnu_W=9bf@IPas3p0Rf0 zif<@5?Kc*BtoNn@8%KZbw-j95_uIhM6?S`N+@o)x4@10P+Qpj+F7TEi*xu=!i2pG9 z3omTPkAZ!MWBxXD{jL9LMsvsci0!p<`it$aQ6FJ*eOh4`8!YU@(2X5-vHe%he9b8O zwjstBw-lV%{qz4XM1HY7II#8Dg9B@~2M5+}4-Tx|9-Lx(usHh2zTnQz&SPV}_GInx z9=r_fJ76DD!f*c}eusgzhp%7qp0hp3$CyvU_*-I(3gbD5?~C|6#`6<=A^JrLz8w9E zA=tm6W4`ul6YTu^60G0m1Z%%P!P*}ff*(xq4D?5a;71c|ysZh=|H%Yv_j}wt$8*D1 zGqxl4-LsJrvXFfk$=HEtyPe&#=a&m^A>&i*WEZ2)v-oT4L^dMsuex!(WBy@nA2gpm zZ~(E7+B>!Bx0aFrZIIheC7(Uw+=63ZZcBbP>(M~1V*SGYdi1ax*Ekm-^7?zW zVs+1T1M&>@x)$r+$Y`vK8Q*}oFXoC^mlS-&66=2{<7G(fqyF}zHx~RU?5OYen+i_e zhod0t_Ik9Z^;lQrelxoDi;eYG#6IqRd0uZTIEF#)?FDC^@Vlzu%)5e{V4YVZ&evbN z@6Xl5)%Fg=-1@j~@4uYuwx_=rvCpUb^uB_ReRx0Ec@gsi==#VT(|1TNVtx?am}eug z{tuzcN6ZhSr!lVs>mzSW-^1&W@c$UPdCYMm6HDKUr`w?^ED(p0VZhGq>;Lrx9zJg+$!X6nyyHTKJ^7KMN-xxj%<47jr(3Zrt0D zi2DU}`SAH7x<2W<{w1({+=tuIt;hJz*X}y}9n}`G?nHN=({+3qEFV5!DSXm(d=*YU z)^QiQT+F!}-MHyG?g7h(&)3lPN!M{NSU%SAb#&`7{ym6x*Kr4y@me|(B&o}5#xvG^5c-+;*S0ZUH)?Bd53?D$c;zL<8O*w_Jd2}s2L9lCrP`}bh^i2Vn2xit14!D;M2f#uRY|1(%Vjr|v}e8heNUC!7mIBoCN zUlG@1UhSU6Rz~?)+uzXB{C@|_rDyREuzZ^TpJ4gO{}j5M`OWhrqnvrH&(^^te}GmLWK|2(?)TibJtVr|nHn}#*MLACj#C)VbjZ$UjS}n908Wo z*Y8rXzGq_F0sRWb_&wbbET{N53`a&0*?Ybdgxn%TUt{`hD{oAFMx%TGwfUVZ*5;m! zLEl=~{N@$Myj{@UL+APZD~>vMMNjM84J@blI9R81{Z`fch_ha<&TTt5+WCr3lmAZMRGWs1Kd8VOTTbkzpIQhsk9bIlAem56qdLUSSKHM@S z-lc;IKYjHvr+**F`%Pty)?-~!!%TG7I*U26)>&ZrsA0BtB-T0y-N&_RI~X|}F}8Zd zo{R4K(%6T<$w%x%(dE+h%>&E3HrE*In_u|pt558ye5}>Ke_X3IT90)_Jx8Eh&uvJo z^+-7RsOKnjxwM`IVELi@eKedleU+$3{(seDUFlhPCeLvrk3jre%{w{@@!1hMoHHE3 zxD(=fMl)KwePfq`kN&=E!KFQLH#qs|fxDy2*#jfa9_Yr8-;O=e<Yj`)pT?d7mXFx`q01RN?!#1c*JEDo&$B;r>0XWj`=ooh zGuXWx%jn;{2QW@Y+-LC&#+gI#oFRDL5Nv;Se(dRi=;pES*#8egIO^!0?IL&5U;TeCRs`eEqCh~I|8(XGi|sO^&C+vo^%@4EJw zdnCH?V(wAsF;`p6U4ZU2R0vgZ&5@Zz#AqiPDAq@#If>Nn;RX1fSU2S!DA-E7_ z7*SN*R}_~S6x_#skEpoezB2(TdxOZ zA2p{aW)<^_xy7Zkit;(2mgWQOG6xoH`j{F$83%M705SdT#{>VYd0^~5{QOGgKW07N#`x_cITMsWoO_wz?h0+1gR8rs6BknZFhFm zTBTEI8)S?{F;N@Y-l*$4P#gnpk4%;NvD%*5Q|}{7Ay7dG{>qqTF+C# z8>yys!FY3&-X_6ASmb?nXrmpXE;)u(j97WK-EDL_&3Z#yFOTEiQJdUe?bPbc=}EFw zic96pQLWdeYSu7RTn4v(d~&KbKGIlSvb{d#efIR(^R>@Bul?z3f30@CI$E=eJvrP3 z)rnetS;n~zawpO{4^O9UQUgs{({|d2s!84_uSxEqYLb7bn&kUylKab=hKe0cjH_;&9@WSF<31m2RU_5<@p^+@(Hr+Qv@sUf=UQ>!nq3E% z&#Qf157)o$#@_&cdArl954SA~X0Uw6Y8k%C*j95w%3SXNuWhxr)v`_JIOTo1&Bo=` ziB8)!a^J~2-!nYMMz^=#+MM%|+K%c3`Q5v6&iCZ>c(vZ_!1Z!F-*V+R(1Wj_kG9AD z4?hIoX-qac-A3K_DRR$2pQv?ezATYrE?lcQcG*;Od<+%7SNC4%odzq=sgAa4W91x$ zZEtLom(}WRMw`;j5CCHdUmuXx-&t+d)~-D4H5_*GUB?fXKGoqy=fXy3s<+3? zDeuD`WXYRT&GvX%pS^3Cz7&AswwY_21=Rcw{j&E8g@vALewrdzEUo9CZrxsUbi zqU{<@udAjzOuOgePLM7D(<9BalHdN z#d(j^5@X!m;5~7b;vVo+v(>0J>(%LHr_fiWymxBD<$=Dj!2v0EAAXbDuPC<@D$GD} zKW&W{Ui$;++gk0B?a4#VyQ{WRa)Y}NX0WI**(0s?G*?B5z1zCkxV-1OGxb)=JFmN? z@yu22cBdCpDeu7AUvPuvo!H&NK-RA&Z>78+yECiOtI_+iyLtN+y=r=#_h+}SM(D0zp!;3su%GxXCecQc|jlH@tF~#BUoOu6s&)!tl;62>k$U%1gp1a3; zxw~Q0>NV+pu+}r+YTG8Wb*q$jcXvx|e9R4#`tvUDu55DN_Sm)b=l$m%UB}$U{pi`T zj=X_A8}{$xjsp9HAE^tu6&Ytr(uDo3 z*83HweV{!59EW{-7W%~e1kUFtxTq)MoS!^S+#lCJhvteo+==cS$~hP2e_6)G9KI#v z>`%YEwt^?6`C#>%Rq$sXh6?V@Q`M^=X`IjV{wOPy2wC0uOuKo-{-+7x=C| zd~d>$|EJ*HyL;H{pz`jGCjY|0S{w}u1?;`xP zhp%5k?i1fpA9MZ&#{174P1>g;&X?FT;F=73o?qLCugkFS{Ph`rBl-<}_@+Mmz6=}h zw_@~fdq;+~-+jwcN4^)L>%SF=b$k)Jy#DTearnO&U4O?J{x3n7*Z(?N zvAXYt_up@sDc9b0cD>Ez9r#MbeU#Vws>DaFV*TBBuSWFG`+iO0qgHX$;Td4;YiVDH z7`qj*Ztt4cBl4Tr37*|=NSwTnd%`u2v1rf7;+SHN--Pbmip}*FmzT@n`q@C=ljvkxfO}=e*j%Ra{eQFp7VoXedNvQ87LS2A49hv`}jERCyAr40laF!y0Nr}b zZ@hNLaXYOx$3H~-PsooEZFeMmI%~BT`adJ{5qa%DOkBkN1bkPr-AyZw*ndIa50N+a zPFlH${a5sRkuTAgb^HvG`zfOTJ&6nde*^1(Yx4hhbh(8{)j&vVZP%SY}3bUAYmv)b;hL9pYoU+u2N1Bs8Z4Z-LA&jHKjYcUrrpZC8PSU&pS z8(q%+?Z@9_a`s~_`=GnG1MdsI9(+xD5B3A=BX7LlA9CS;0J{5Io8Ka0ZA;(|LHGMY zo8KW~ZSMJpp}Ws-p`Ycn4+qQn4)~jHA-a#h>9iezEJD2N)y?VeNxwh5JN4J5PxP@E z-9Da8jw?R50(u+3){ zoQEfX`B&bT+Rj8j88NTlcG`_~UY>~V-gBPCXM*{MwP!QNZw&bu*K%~ZW%{SNKMO4X ziL@8bMweTGxEEK^`nVUhtwc5;?l*P&@w@UIB>Gv4Zf$u#>)_<0pY`Z+j`8NSSDplx zcg*XNI5VD{{PfkwKG)F7yN9jOdaNsI*of{}^LzSSuzb|830*G6dLFuuW7Rf{{0(Am z^~ilby5q}pZ-$eP+*{D)^6^~&mUnE9F~)ac^3zwJm{a)}tKZ;`)f%nGx}u(^pj*#v zNUZC{aPm>lCFpW_Jx>M8_s{p!;I!$hL_PBVUp>~9uZ3&!M|R|Ch<`6|kNTU!XK&7xj|U_^?y-Xsm!I_q!^y{4e+atV0wnSrif;b6BOit?m*0^O2g~QV z9|4w++=rvf<++al=eZvVmdo$RMPT_n_hPVo(xkuyeUTt$%kpn)VpPc@{6FJ+=>@(1%az!=Bs5$DBR}-F`fGme4*HVX2EJBHG*^ z_9-9x^LTW*{C#;GSYCf?5{LhZ==$g9$K%2B`dhO&_WCL4=7>AzRCH_dEYx;2`)@Ao zX^4AWd&HiOZoY_p0(!)1i`X;Jjn{4s&XZi^ar|=ecbc=%<$QlUN6)79@odqy0+DmQ z)FaO-bjM;J&b>HntI_jX&jClR+M?Dq=#Eo+#I8j*?`P6EzYbl_`ex2wba`Vvqx>D@ zygeB)=aZ0FFS*FQ0o__-4mYAlUD~3qbJ5MMJz_VZJEo{>7+o%}>pZZ0e_iLpnNxf0 z3pvlBm|NTEYcslY5O@o^eFwe(-8GE&@IrKB^wZ|}JjX6V9G~;6kMF0P@8_QZ(wZhM diff --git a/assets/shaders/vulkan/terrain_debug.frag b/assets/shaders/vulkan/terrain_debug.frag index 1eaad492..5ffab690 100644 --- a/assets/shaders/vulkan/terrain_debug.frag +++ b/assets/shaders/vulkan/terrain_debug.frag @@ -14,7 +14,6 @@ layout(location = 10) in vec3 vBitangent; layout(location = 11) in float vAO; layout(location = 12) in vec4 vClipPosCurrent; layout(location = 13) in vec4 vClipPosPrev; -layout(location = 14) in float vMaskRadius; layout(location = 0) out vec4 FragColor; diff --git a/assets/shaders/vulkan/water.frag b/assets/shaders/vulkan/water.frag index b5ac4ed3..ab40dae1 100644 --- a/assets/shaders/vulkan/water.frag +++ b/assets/shaders/vulkan/water.frag @@ -10,8 +10,6 @@ layout(location = 6) in vec3 vBlockLight; layout(location = 7) in vec3 vFragPosWorld; layout(location = 8) in float vViewDepth; layout(location = 9) in vec4 vClipPos; -layout(location = 10) in float vMaskRadius; -layout(location = 11) in float vLODFade; layout(location = 0) out vec4 FragColor; @@ -47,8 +45,6 @@ const vec3 WATER_SHALLOW = vec3(0.20, 0.58, 0.86); const vec3 WATER_MID = vec3(0.08, 0.34, 0.70); const vec3 WATER_DEEP = vec3(0.02, 0.12, 0.42); const float WATER_MAX_DEPTH = 14.0; -// Matches the two-chunk overlap reserved by LODConfig.calculateMaskRadius(). -const float LOD_MASK_BLEND_WIDTH = 32.0; const float WAVE_AMPLITUDE = 0.5; const float WAVE_FREQUENCY = 1.5; @@ -74,13 +70,6 @@ float hash21(vec2 p) { return fract(p.x * p.y); } -float lodTransitionNoise(vec2 worldXZ) { - vec2 p = floor(worldXZ * 0.25); - p = fract(p * vec2(0.1031, 0.1030)); - p += dot(p, p.yx + 33.33); - return fract((p.x + p.y) * p.x); -} - float valueNoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); @@ -131,20 +120,6 @@ vec2 atlasUV(int tileID, vec2 texCoord) { } void main() { - bool isLOD = vTileID < 0 || abs(vMaskRadius) > 0.0; - float lodMaskAlpha = 1.0; - if (abs(vMaskRadius) >= 1.0) { - // A negative radius carries the outer edge of the ready detail disk; - // begin water's translucent handoff two chunks inside that edge. - bool readyDiskMask = vMaskRadius < 0.0; - float maskRadius = abs(vMaskRadius); - if (readyDiskMask) maskRadius = max(maskRadius - LOD_MASK_BLEND_WIDTH, 0.0); - float maskDistance = length(vFragPosWorld.xz); - if (maskDistance < maskRadius) discard; - // Fade the translucent LOD underlay in across the detailed-water - // overlap instead of changing its contribution at a hard circle. - lodMaskAlpha = smoothstep(maskRadius, maskRadius + LOD_MASK_BLEND_WIDTH, maskDistance); - } float time = global.params.x; vec3 base_normal = normalize(vNormal); @@ -188,10 +163,6 @@ void main() { float distance_water_mass = smoothstep(96.0, 520.0, vDistance); float water_mass = max(depth_factor, distance_water_mass); float edge_depth = 1.0 - smoothstep(0.0, 2.5, water_depth); - if (isLOD) { - water_mass = max(water_mass, 0.82); - edge_depth *= 0.25; - } vec2 uv = atlasUV(vTileID, vTexCoord + vec2(time * 0.025, time * 0.012)); vec2 uv2 = atlasUV(vTileID, vTexCoord * 0.5 + vec2(-time * 0.018, time * 0.020)); @@ -201,11 +172,11 @@ void main() { vec3 depth_color = mix(WATER_SHALLOW, mix(WATER_MID, WATER_DEEP, depth_factor), depth_factor); vec3 tint = mix(vec3(1.0), clamp(vColor * 1.35, 0.0, 1.0), 0.25); - float texture_mix = isLOD ? 0.18 : 0.42; + float texture_mix = 0.42; vec3 waterColor = mix(depth_color, water_tex * depth_color * 1.8, texture_mix) * tint; float tile_grid = max(smoothstep(0.030, 0.0, fract(vTexCoord.x)), smoothstep(0.030, 0.0, fract(vTexCoord.y))); - waterColor *= 1.0 - tile_grid * (isLOD ? 0.012 : 0.045); + waterColor *= 1.0 - tile_grid * 0.045; waterColor = mix(waterColor, WATER_SHALLOW * 1.18, edge_depth * 0.18); waterColor = mix(waterColor, mix(WATER_MID, WATER_DEEP, water_mass), water_mass * 0.34); @@ -234,8 +205,7 @@ void main() { float alpha = mix(0.66, 0.94, water_mass); alpha = mix(alpha, 0.90, fresnel * 0.20); alpha = mix(alpha, 0.56, edge_depth * 0.18); - if (isLOD) alpha = max(alpha, 0.93); alpha = clamp(alpha, 0.56, 0.96); - FragColor = vec4(waterColor, alpha * lodMaskAlpha); + FragColor = vec4(waterColor, alpha); } diff --git a/assets/shaders/vulkan/water.frag.spv b/assets/shaders/vulkan/water.frag.spv index 2ddc2be5f36c3ace2a09ac24558e82dca4fc82e4..fa29363721eefa5fce617e6799d4a410d513a7bb 100644 GIT binary patch literal 19156 zcmb80d4N@A`Nt2;f(!1rZ=j~Q;ew`!Y$Jjwt_WIb;|1=(=rA+R41-JNn7L#sn)_1j zmZqqgOKO5xmIY<0(NC>#*WC9N`+Yv=p6Bv1zyA5%?&JIYexK)k-u1la+%r_ucgucR z)+bvvTPa()Cd)r-WqnaHwEDapKXKB;A)Q^*hwQfd&IYWWRRWD!BU>}8rPOAXnp-Kd znK(MJo3Yoh&#{&IX4xv(0obM3b(m?^6QCMe&a#EX*P*64>*!y9e3MGE+FQz# z+GmzKnx~ao4lK0~?rs`7ocJ30n`PaYv1?OyPMg`%Jngt~9p%o}a?9YRU54|`{dKgP zWz*U^8@pzfyGprMldVNLjuF=JzxBa0OPw=!-G%(SR`Rch)}F`KXB$y=ms)0*4{B@f zEH97Quo_dBZ3=FhI;-N$>(ykNRQ>(3Eh_$l+B#;HTJosqUrjc!8dsYQ0(UjfDpSz2 zRkOjAEzPavQb+Si<+0`Vu9=6GyUQJ&!wT$_ymr*ph7d^U?@+-z_&CO(0(slX>w>Qg_Hva&w4*;(NBQb%c4;X8+NYL08O zxxIWB!lxC!OM3Y(gWGZYcU3RnZSc<7v+}lY2eaLBd?&c0tt-d(fh+y4%@%@}_o+5} z20p!`xv6VRCI5@T8!F>@6YfGfwznx`UET+$dA?YIbD{JhmPxYD{lIDLS}X9HY+Ykd zti*1zf{*p@#mB;3+v$_1=<6T3?Q+UeyZZ`0`>~VNU zxv8Z*t*gjWpFOo)Yi?_;R5$i%>mF7K#(`3wJ%iRct=w8}%unXVj?$bw@3Y2E(vctP zX#KLKy<#iper4TXz?aU?OTGBZEAaa4RddbmXev!BS}x|ydvx~{He#jOyf1H{%_((p zDOBzr_WM1wN{>m=m0y;5A3srV?(8bHlDJZ7R;jZy&*Qzndu-t>+FMr5ormRdeK;7( z(`QsW<{qs_tK2JU^XG@|!z(u-=hbJ4?!%Yggj~x3Xz6^cw*s%pHpuhsO6}Tg%N2aw zCwuX2;N0WJRQ6C^HrTqf*EPGt%QVliVRy#*6rpDmDco~zAniRX{n>`8R?|F||zV%vWX&iUTZ!Z}>2TbsRw zR%&UVS;}*92jGU`aaBx3HXb4FE&DOQ;!0inQ@QWRy%(&BrP`J{_(VOcdXI@1dGC5^ z)*FpQ99_({odiDrA|^BE)Zc|TKIkZATp?TJM5UZ%D*(Y%kTy_;y> zztp@rM;qR^)YeQi?^$XC6V3aS+RlmQ{Yh)?H95Zq7jEh;o$Gw(^} z(I?&vOp$Z%Npi=m-`^5)$0qko9@L93&t03JyZR%)=Wy!xtkrLy%_mR&o{6^S{Ob1{ zle>QIH-FDWZn3_aBG$7>PLbF3?656TRQVQQKj!xOH{OrjYf$dKkbAAk-4k*fiuiSC z1vmNq^;BtdJ&>^jvGs{}4(0j>!X1;Err$Z$U&&LOxmVPTGv0AVjzRRwIWt~eGiCs# zG1eOdUzgGtb?aKO!KI(OWo}#2V#BlvNqeE(z@#A+JVw`)Xis|-6;oR zezq_8*TIe{_%NM(f{z5-hTMLP!LqDpe(;709~FE&{Mj`gA02!`?$4_J19Sg`s{hE` ze{GdF!UxRzxFPBv1Al&_m-g9#F*%+RX3Q~I#595Djo-am=Tvx>jjrbG$$3`g=TJE9(}tDym-;X>iB;Fzw(L==SR-F z;l`*(onKaBMn%j$a5dx9%zrOtU47Pb-S5Zz1fLJ~S{y(R7Zkc{xClGrjXtBJy+6T+ z4jn!w_@Cj67u~iX+V?CqMm=&r4NmvPbMQA0-E&0bSPGxJ@T0whzXJbo*;7^iI{d|( zHm=6M1z%;!4ORXj{IL5stIpde@S#ILsPbj-qvkFg8ToglGt6ssO#8vx);@i&@K1ox zdHB?7{e$g%)>9qxkrh6|YyD`j^W@yQ&fkWs$v5MziT6&m(DnU)&HZu=_1rt=8(#2I zq1kWObSm6EW&E_lXW!&A;Cyqx_2u3p)Pj4DkjFfn1X;XjQFWeAhp)0^%X}Vf?>u;% zf#-wmpJR*qSLE?GRM+tu_~J$VtMhtY?%%TNzaGBrQFBMy-wbSzQ{WQeDZ*K)jT8NLx&En z)*A))j4)1L)Ef=voT={TG4K(qEEyT?HNf4o#_0=ve{ci&!W^&HkIxzDd>shqEUb>_ zFnE^rRQvfYcvI6WReuNk%Uh1G`a9uEcH61y?}C5w(rMLk&4ydYIDIj$IbhDyYMwLU zv@_Cvy3c(6iGI2tE{FRp665fBDEwYCcjooatoGw^_`LC@YW}6Uf9u?DzE|KrTSb4~ zhClkz;A;JM;7e|3uksJ_`b|~-DSX?b)~NDj@VN_Tk8+>>2kbTOxnh6i3Tn|_KS|b8 zy-rrj`GQejr&or17W+~D6ve)pYjsNB&$-v+&WZ1OQto@5l=}`RxX((yy9sW-Qxa~z zo`mZ^E8+Tmf0Net9Zt%9hm-OvdvV|4gx~)74kx(zeOD7)|APtF@B5nY%jYNDc;C&W ze&5RkcRbJZ;!At+7kY8usigINr;_qldvV{Jq<-I-q}+EUDPPu$_hAEs-|_ekBzXS3 zn{fT>_2RzcNaKCa5!`s+a|Cz(e9w_`-)W@WcN!`8eMZWCml53got$vz9IZ_-#4V(cMb9}oasK3Ovg&tRLtKG zBe?E7bIOHpBv+}|q+FkMo54P-^e^rU$AbB0`xZXyc-^Xx;p(;@$AQ)SO%iRiz}4?3 zW&!#vu$s^EpOB*s?B{dZ)z^xtd1i>CKkZ<1pIY?qc(7XZL;e|cqON7s?E?Fp6?JEW z)naVjV7~d-^qJE$NIi0#05-?L%#Qc(6TxblAErDzpE2q?8BM>>a^~{9Qj0nHF4+0j z=h-D6R?PFM;4R2u9eu`l{;5Y_P6Im^!M_JK#`ayi?}OERc08@n&JbhzV%%qf`R3!+ zZ(BW->XG|wuv+jRfSsF*$?G0E2W*Ube_B`jA((IeH$MHYwYfcq^|@bscAbm4FMXyp z|Iw5eVCn~RXK*YR7Mi*r3+Fn<@FN5@_ocD6wQ8lzM9hK8rRjhAA{GXG)A8>-e=S!*H6G|XB2aLEm+NG z`2Ng?d2RsnMA3}`# z`gXXQc6Oo71DjW0?EgE!>Jz9Fb?+>=ntRfGccB@p&;H*?sb-%%PksSbOV5+L;p(Ro z<9z%Q+(4-w_3i;jJ@d+qb6oDJ`@kJooCo)V)x!TPa1Z8QF#Z8BU*21Bjsf(W_g+eU zQSaAaHGL0J%7+zWeHh%I`s(^0q*Sx+Bb2|v9xb?Y^INbnN793%DSw9nx?z7r9>tzlOQLt@m%Rnz64^$|Lqo zu=Rt#1vVzGueZU*s9XOHO0|f82doz7=DT3Nc|Y~VxoM2PsP{ft&2z*)eh60k0E@Hk zBQW2559l{n0~Y>|3;!-?;j@nYe2-FpwD$%0b1dq93Fezw_fx=Um@(G`4~ z-BK?0dER*MefD#G@J^J*=rhLO1nM!C4Z)5j&YF$DYF-P`_l@C>FZT5&aJ3lkW?(hn zkwl%%!Red~gsWXao*3^SxOMf#c((xC2YoT#t-vu}f0M`^i@!&VkMV8|cD%;uGsfRE z>gMvi-xln<8@Y3|9oRXEGi7_QG3s$aVXd}gYO15<`C9g|JT90C+2mhz6Y9lhZH%iXRLGN?==19 z^i0?X>~$A8$HLXZBImwnk#iKd0W&u8j02~6#>3UZBF}zkk;nI7<~7#7`I}LHjCUgV z04$!l4+5(lh(&)7274}rg?l+*OCV>x2Eb1MO7X98IY#w9n zx4&)mizic#!D0+YfE`!xBf+jg@NX1;uP@i`n{Z>)W86o9)qT%qJ=g7Mux&-ZzYSIk zi+(qvMZfKfxr~kWj{)2NXukxuj=JNSLaF9+->oE?2KEd|f8$PvtNVMwzLmkYxeBJw zzNj}9x@YlH%r!GMa?b#pJAD?N30IHY&EUwbFLEDS=xJ_a=^tE_K%^jg0X zt}p6c1vXc-b2V7aXA1B0UiUwSTTfra{RC{A`s~jolxmUpTCiH24cCLO!_vOo09TKg z8;h9q{J05CJz{PyV*HzbwDnUo^*Aej24?8_v!?a+hwqleXZyy6@7Ba;|BZEQ*HG$< z_I?gli@ClH{NLvKc65C)XY;^n*U-6W^A33Q)x5^I*PQpe3ZM5nK%8g41gjZmeSK;Xe=oR$js(9CelFZSV7~j|#;C`8rC)*VyYH3soA(|{ z^~m!exF^Z`5Zst}uk>rMG3qfV4};C?S+C#wO!(#(KL1_5asDo`-3P$>+$ZOwJpy)3 z{PzR)WvR4w}dCvfyXyInUHQzUjV~kIM{k<5!HJ*m6Mf-mPNBikDy#!rfTo-=_TUR}v6aEM6 zdTfm8k9+qs;084Pah*I1HrDGzzx%})=j2Ja{^;{Rz-qz&S@7t~zu?BGhxR;JeJK|8 zUI3@}%NOBl*VB>Mt1rRrm;2Zp#@MIlDD|1kYwKmO>l%CaRj~8+N)hAUeGRN0d-rv) zn&!uLgwel$Lu@DRt8akS!s34RCYp8qtzkdRYi#T%{r=4$*55MPco*y(#5%kOk6dqq z-@%MA*IShOqwf1)wcsCu(|htqaJ6Arnzy8SoCD{B?_bae<-Gln9y9|rE`mO7F_u&t|u6N|>3pZE17ps9A z<0rJ*LbHv~>I#jjtaIfZXg!+sjn{8(``}mKGJ3fu=HHE)0PK7@R`be@i@Xzaygctg zaJ2)m$Z;^(9FEZ(a^uY5emDfv{}t;mj=f@^{M(G5H8A&zYqVy99kcsp9m?p_B(Qq$ z!wVkg`DD2Bryem=z~+sZBf!R}$C`cv>^%D%px=Lk8Q1nV!R9q4=JQ)%`=ZaW$Mu7^3E4Jc!NzFp|H<$O)W)Jm9pKy4ak-@K;uJJ04)k37@CYQf6|kN0ge z;7x@&2J_CujMEp-Pt9O+>5E*)g3TSdjsqK~MXnaGar&ZvYOzmdfz9DwGq3C4im69T z8`zlidTEEN$66c@ww`)iBOTzFAN{U}TDl&ttDkf|HiWw#8&i4?xkj@w&zayS^x`Kc z-2HlH!mWQ+!u6k%aQ$-=uK&V>>%S!7`Y%hk{;LWeYts#P9I;pCfSrq#Fnz9tV^WVg zCxX*DC&6R>^jXJot4E#hfYpMZQt;Tf--R2a9xb^3AM;~IXKOCC0y+a%y{>N zTI9PLY(91Umr<&j&-wT<<{Sk73D_~jb$AWf`5lHu%(Y^Oqo0Ja~|#*N_KZQO*W9&OwVjyB9^4r3z6Pr>FGiA6g<1FJ`lTfmXSKAY1R&j@|# z8L=tc&$?Kg*_(hpBQ~RqXO-K){StnA!tVgznegqw_a)qGcwxa^8*`nDiOn;iz~;EO zz}CMX^SSv~l=jE+W7hLQ%7-vN`hQKS-_kkRj`Cs59CvX&ETDV@Q=5Ske89$Hm zam@LMKK%h~pDv`nef%S4jC!0kiwk`brR^B^7fk&L%y!J77XA7wIPLqBaJ30otdE-W z?3_DSmezNkpTeBy9@>-ttX;_Sm4zu z2VwSSOUk%+Jp+z?>b`q1;nsPx7k@k9#=qZ-f01zOdts&d`}N{$CEWOR6K?)Zdhx9b z9`o@mF@vz!OaB0?J%>fiKf%VtJA;3L)uPVxV6~-K+;d(4n@gYT;ykHE&X>Sx&X?h8 zk@Hos+ACPpc@1oxi1{~I?R6~LeFJP=eYWd*sEJ*Rw=na$SKq+~Cj4En{fxc*9@rRl z>$_HJaczD8-V{@hK79mM{}79IJ_ZlO;=1`1to8{OF`t3$Yqb42*f@3Dc3-GP+h2lh zTRqzT3aq{ii|hVBVEd%cJnlcWXtz(F<#XUU(HHJK#eCMl)y_qWzSY95r!VrJM!`>!7TUm2{P&fO~TfyCL4{aY2D_OCx&E$!cGaO>%dIbR*Dp3eCi z@N~}Cgd3-BpS`x!qR(rC?X!CHc^$BNI_Cr6jzyn&GD@|WtM$O?nAV4coX+{iaO2c%*K1!b`oAgI{;Nm-H&esXIo}+f_HQ6O?cX4{{Zo(rZ2`8P zzL@hZ!RqOpZv{{1{A+OI)a|q9lUnq78?b#=k3J6utEY3mE!?r_Gmqz>TI`$c!0!Fv z+ru*~ey{ETHby<#83I<{5sP{|fzv!Y!)+__>;g7MJ@V`dRyR+)+Zzfte*@jn?{5pw z`Q5;L^WXEvc%M>>`;^zfV9d`}Sp25{T7tJu@HSxYS=&;^81^8aF~RqQ8yEc@1~*1M zV)g>3b%w)jGyHqQ%@KS=5g+q15^juov@;599({3MjRxD6zIfL%25g`7IUn+v56|(P zFh4tB&WF!~I~Le`;1J9?*qJi!+hfV$eA{1h>jE^;b6Y`Z(9A)|H)!Ze~fzy*m1|Te+1Y#^|r^YfqYdYAis-?jGI_3U%*$xt(JDUENJ%w%dLi16Ix|fyS(o4asULwFRY?Hj1nf zM>E!oU5+iq)*hH;TVRv1ld;FKXR%47nt`oKPIJ&1Pz^oEviqr5hnnWDqo0HE?O$5Z z(OTZWV_vzlWlpJeR;goDPxH9(#MjX8EbGCH9YWbPXI^W|ocU8b%Ux~d)=|x4$Meno zb+nshbK1KayXTd=OSx8)twuSOk=F6QwZQXAUGugbOa5&u`PW42$YblXbt!vFtqaS0 zx3_eam&L48jj78v05>1GpyJHy)nw~e{mW$|D*nCOI~SB%^Qh=wO*X6=SDOt7cegAk zQ_!;&vr&|-Ep6peXUp;BDdmptdHa=n%AH;1LyIw1(hyslZ9;65A$i@}Y*R{WQsA%2 zHlu8Y*0JVWgG=45rLF@H8r3p(d@=6YY&3dzOKW+?l*-(Uk(ZC%y}iA&37F>@OX(&s z=XR7G^IE{Q*>2!;j`x5sWRZ+p2yVf|$5J*I_ykIQ>Zemy)~7Z*1Kd&SEG;N}XHg!R zuLIfd`ye!ffLcHI75(a(1SyldftyzLvoY_}ZW4DM|2&hZ`KO22Ef`@zfl zRGU2mZ|ZDm?w(x9|6=fl%6MLfyO56UP0Co8cfo0%&%VOBPzDgoB-!WXz-jDiU*R>` z8pa-1iCzCIKGweb zyHM)0`Dm@}O{$YyJLZ+Bn(IfSca}>{$4zPJny)w4>#}b212-%dUy~?7Q%3t?fsaS`Tb%Y37RR%IH}|w?92C9Iq6tES9{u97cMAwbEsC`)%I4>&-`_? zcXl^+apojBR|dCs^i(TFTdNd$duPjB?+)(Ui)d?ZzDDx8>@xU~rLJ;gTYduOG2Sc5iLzVEi-2vAyzsyoa`Mk2Wqt&!)QU6L@F2xwSl}yU0_Y)v|#Lt);!K zQr*}(^o9FXf^neKXM@nX=9JsYjrpCTv9okco_9rK_t%l%WYCt&R_zyCxery=cL=_8 zeunnrYkY;*XKR^jVP|t`PSJ8PXFfCZ9K?pJG@JKjJ+xy=-E8^F-OPRuN2~Oh6y5pd zm-lfq_2L|9tW@H-@vN%LpGA6-Lq=_MHFqAC$BjiRH_fef%)K}ft#a?G%b#a@4p?>* zbnW)SpN@alS9nb}JFmYj`D?R7zv8RS4(rDchjWjeT-h^qS)+AnuX|xXwO_i92 zh-raWhD*(2F5A&6b5uErY`fQbEp6SEdbRm;Sx>RUoR72hSDI>E&~j`(AB!S)5_d(O zr?uQTx3i^kF4txEpcVVBT+!;YhtN1P*`AI0aTfi3483dqaaGZ_eLn2jfBtbZJ@<++ z{1q*)*x1T*PHWyDpCNl1THEK$uSB{&Z=p@k>(plNgS$G)J^~P5pM5Il>{eUY8qYtq z*-9Mh?DwhdoUpdPCY*D-p_TKsQnxnS46Rh$*o>J3_m)&lMPAQm*&c42Q%g&@2T}An*C6FJkh-8sXd=)-tW}j zPBiCD&6{|%;r&f*)kO2&rZz0myq~FUlW4A!+N4DD9;UVrnrr0!YdG9b@DUZ7ubKC* zSZ}fGNs-6;TS)F$tm_#*ydPhdt2RHU^+!I>?bPqttlvJGPoDZcLv7Fb)bDRFx$7mL zTyTn+;x_Fjim8jA9B}db)K#Wr#*l1Ri3rh z1X*_&wifl>8*=@_;EqF0)9*a$ujHxCuOl_%jCbB5$8dV-JQ=U988ej980!s(uR&>y zx^*_9)GxQraJ(B+rg_xYpzaow!>|E)S(|N1XgKb~_LRdgKU)@j2e4xb zzOzm~!6$%iLvBANV_DXFU+{(spBQ`^{Ml6=niPCS?$4_JS-F2k)qhCtzp~03;X@aH z*bw!PfWNTrOS^Bym>f?DGv){^Vw%B=r)^)Yb0R#;CRKCx<~*zNb1IxUs^mNu?l{y# zyRhKupiB;Fzx=Xw?u(qa!i`anI=59~ zCPvIJ;cCXIng4doy85i=y5EWU34R~g>u)GM>??HFa0zzWYXc@ldw+zF8#jJ(@WbUX_0VKWx$c6C(d;I>WqH$21+@KID{L!aoCk%)KX8>(93H zS#NdBhgA4(uH#`~*RckH~i*#+m=! z!l7nLUvCrUwg%rDn9^zclU+6#(2H&`-$5IJG;1h-}uI5$99t+=X(9#Lf-f?jE zsB!v2KMCAGzA(q@^>H$KI$x*4Ia{maITy~kS?%XV@aE>1tNt6|pI?7;)xQ|Nbi1vq z{+r+*y>wD_TsOn5W1PMiS09+X6JlTZJQe-&x_koevrP2c=d190jl7!IKfT&+4Hwbk zX{Bnv)`d4WZ<_nfHw^BxTC_h3{=f&Ls`WRAFTJXx%17t*o2z_V_~wVLQsukAPZ-)( z<-5Tb-M?_6*X2Yo-|BTQpHyfuUY`fk@$6}ZthajI?3MGri9671hDz4TNBLtE$6_w; z4ZgFCvwLOq`3cv5bi(y__v5~MOzZn@F}Q2sd#aTCPAcWTmkRE)o$sT9+n(>Ef}8J_ zgzNX6ROH0Se!}(pZYARN`%We0zEerL?^EQvas7HW9l?HT#BSlL z?t_=WYCb<-M~vE$aP^NkS(+&Qe2Pzf4yNY0AU3aO2n<-x23t= zGbe$2vDq-6gH8skX@1y>)%89FP1$)h&hdHHt4Ci?1*-)=4QwA`PEQ9LqwXA>N$JNq z(02x=<{XGWr$w<^DZu$U56m|kiBqU z@0`Uxv7AiJODHeJF2eln;QNGV<9mhA9NzQf`kd!0!2Xu-+L3=B%s1Pk@LA_F1og>` z!|VPBU^U-SL>oVZtH*r)2(0FMJD)!W`*A+?U5TkVhvMkZPr&9rvFP7bV72In-0??U z%cy$|xEG7M*MikzY(E9_&BvzCoL5t-M~>^k=7@dwGq9TG$9;Dm^WdJm0ZsqUF>~EW zsTOl`6WICIx0q7Cb1~01gGZ3VI{J*cj#54P@(ZwY5&TxLF}Cko{SvHp8|EHUyB%yj zeKGDkz$feRP1GazufS@-?*cnF7m?R}`D?H->Tzw~4d$C)+xp|$z7wp^{c;~= zALhQi7c>9)l)u5$XS46^_x**Y?#Cjoas3Y!9gJ6B;cKl1gj>~o*p_GdsrF?O zgsaEB@mIjm<~w4p!dt{&`roM>}7ux%d9+&g#6 z!;MjoIa>j&uKBSoq51tiqd)Qw2K!zp@~;GT4kQ1{aAVZ>rA@E*Rlw?g%;|3;HFM}+ z9jvC$-$Qb-zlCNLb^IOV`=_1R1J+p+Jc`nIW321%NBvQE z4A^|Zw+0&%W8MZFV>VX5dHmg}FY1j0SLUknOt&38`e6UJg&Pxdzdg7C-8l34yH(A6 zeds%Yzd-_@qjm!O@i|I;M@-G*XR!T>zK;h--}UW+Mc?&Bz1_gJANQsS`Y_*5 zM&HzY28(trt?%{R0G?cM*L!#HA{vf-d%)GEBr#L_#Y~5*O~c~sm;tt~zBrTi1lyiI z+xIu-OmHLZ+1I_ma`7xmjFCXA2q zego`yjnQY!Y)W-=d2c!b+*okus04OS;`}=jY>axGuXDianjg}zGb;HH2k4LdWpJ9m z8EzjU|6H&!xn4XM&V#FKe$xC+X!_0X{rf1eZ3dqYHfA5zT>k=aYhs=e>TPJ|9aH46 zp0UnR3#EQ@x}T2+562?s31GFb$oWmQ$k_uv4l_3Ld<&fB`8HfFEb^R)7J0hB<~7#7 zwNvVk@ty)c8H?Wsr-9W@#iGBbgFS=7LO%n&0X;0*KNH>dJ)6yEePg5Ecfh+Q7WK|T zi+-O3HjlCPyO&bG{q7_0IbhGX>zQHa_&m7Ux!AFobV70Iq-}lgBeD=j$#zy;>fgNYGe>vDX>W*g- zrJB#OK9_%AjHSQZe*jmH_gX&$+vXrlpM9~r`U;h}c9=U%4 zj@_S<~BC&D_0kOp9AcN@s95bN_{b}*MQaJeZ*c1PS^aWaJ6Ze|E3-1=XG%F z>W};7_233#^u>Jt4D6i7+Ws7@W^By2TEyP~Ha_}y3wSIR?fwF+<{pf_a4T3nEcU`} zXt5W}doyNiw5vbb{UtbU_jdSbEZVpOtQHn++=&)#^ng82jE(;P3T&R}|6O3UZY*;D z8mt}`x$j1c-1g75jE(;5kN)2au1)xTV1Ii=8^3|~B^GVmj}~p1!+OTr#yyn!qm2i^ zF-Kmz55kR!vHlkP5XoY!zk{pYmh|KIaP_e0#~;w5AGTv|V`HrPqn(HK6CCf?9)Vv3 zzZgq@KQBSo7yWq@Y+d!x{s>lIh}qUdlxoq|pTLh}(bkh-+j@dBJrkaS>x+6%gUuD~ z{28p~Gm6g*o)1gm*3%bpe*xR3KKt_+rCQ{D2CNom$aCOlv9vFLgR4i(-;0>^ocRZu zdc-_m#Q675(bfxS>T#z06U@-_-xk)_AHEk8pY0nPzLyf8{WsRJ{gqN*wD&KtTFmv! z;Quw(|3=prbM_ywTID@W<+s`^@aU^~jd8Cz@2?d;@14eZExw9HpY%tcUI*JJxqJQ% zu-8DGXK#YljI+KzwTOQkd^H^j{to;exO>2S@4}5ykN1M_f$h8R)byM8ElTys^8wiR z2Ejjs8x!x;J^~w~9_#Qi*u35o^m{)F-zSC7|E|ioQJC$%PpQv+avzmH1G^^vEtPHi z`~Gt@^?3ID0<5O_u}xvD>z9!Kb-n%r}kG(q*tfu*~9a2>L zw=ufy#C>%Wuv%E$&o)J~uD@OEhk1>S{iNUD>aqToF$Y_Kor741E#Z;NzZ>yyMT{|* z|CXyi>TU&A3qA&%-jlb6t92y#w}D4~>-)DS#zlVpk#8(G%|8x4mRzx3+rib&P4aIK zkNnp6Z&{3s{QAw`$6W0IuD(+N`^n#_fOo`p!t!@2l+mZ1!S*Tk$9S;qxj*#zw=&U| zzWf~trFqO3d3FU?@>K2<6X51?|Le2g{+&(a(r;b&+GMbMEpj!$(|Ot*KA&VUPgCH= zsK-0YJ;3Ji6WY{5vmc>ND>Ry8A6DK^Pe-%9@%qhgKPOS@`+|||g!%V8zC##{`SCZ) zj>IibxgzGAtVHSG_Ne*$!tu%dH-YW?asT_$@cXwH-m~N}FWZ968~3(xV8>#dKKp1J z>RzX|u_LCIo~=6-dc^Ef#H7zX$g3}pf7Sw1lwMEmQI4J$2GV+ z*j(;~N!Vnp0gH2W3RwSYn6b9w+^EN%mn zj@f;)I%V`}f3SM+0}3AZn*-s_pL)a`1U7HP91J!_J=XMVVCUKA6#a)_aZMcxHm@-; zpI-;t7k!RRo{nuTxSusJ$2OF5P0aDEO&RlZc%j>t^K~SqR>Ir^YI89A<~60?c{Z1N znGVT&-Z^^hN*FVxKGk zo5Q_kUe~`3Q;(Q-urcZN(g9bGwKy7VJ@vRoI>9kN`dtsTbUj>GKk0g`19v^vqx2kd zjTT~_Gr^DT$4^MO`}OpMTmOuN>pv^u`WGc!{{;!xe{sU~Uz%|JR}?(frU&jgVy_$n zb}p93^tl#}Nj>Tu2Tto850ClNXC23_9(BG6Rtx^Eg2%r7HryEXh&ic<@%h>Pb~2{c zi^X0#1$<&*UZ2LDim4kP`A-AehCch}I;cfkXM!E4KG#6*ztOtSzJqx^s_S>n)S~X$ zVC%+SJ`b#RE@r%IrWX0m2b)h_ziX@(`MwKI^IZg2yAU(}VoJ5hcL~^h>iXRiYLV}I z;56UmaJ9=Y*=satXgN;WvSAPWYDKI}+|Se1E}R z8*`n5iOn;kz~;EUz}CML^ZD{ul=jE+L)P=xly_r(^xs3N-_kkRg7RL>9Jg>i^ilo> zQ@aoI8CZT7W-jZBt-Au{1DJDQ|LnIs_Su8rbusmr|KEZgN1WHc0~@CvF~0{pZm+j` z%0FOg#>Ske8NZnFA!ap8JLk@orS+ZXr!eQam-gg;F0`j9t*=ilT}S6U+FuHGUfi4F*qiRP z5tyHiu-Kc!3cNDqaLoRUq>OvlGvL^#?z5y7gm~oxqf`L zgd4v`!p*;aKfY2F}Q=dg(RJJ^_br|}Q4TGV*~toA$>_nd!%&85$E zah}v7=S$!;=fB`;k@MeRwU@D|^B=HvBIZ@F+ACPJ`x@A~`fS(rP!qcrZ(!zgufBy1 zOZeMh`x$%r9k4O#)_1Mc;@W%jefj{b{yrA%dZpwm%2kwtBSvC0P9nEUx?ig6)$&^SJ-iqTK-lmd%0Z#6Y<76!TdFS33tS z`c@0Kp1#Oa2Ttd%9&Vhv?YeK(qW{Z-?Z0~Te+965I(LKM!-%sT`?n%I?cZRyTH3#r z;MUU@bG|ZIJ)QGa;OU&N3O7#OK6`DcMW2U&?X!CHd3CUQI_E>-jzyn&GD@|Wt2M#t znAU=)V_F;Tc+_Lg*8y8kU*uUAoX+`raO2c%*K1!b`o96#{;Nm-H&nyYIo}AL_HP(G z?cZ>?{Zo(rjR0FuU(ESPuzEV@8^hB%-vn-)x_$P1Qj0!s2DZ=Y(dSWM^>ofRhdUO1 z=J6aQQGJSS`-VJ;Bop3vDm>jKX|gwyl|PzIi{*VLjub4gI#^^UW-ca9U?SxLVBT{$Mrh#P9n9z+L z0R>kO_YDx2;EIZffMkp)N?dT?qDCVcjS7geoul}DuWxl;M*i~URBru#wcL7j@2fO) z?Qr0ZSvDoxIh&e2m9?MUvmIbExZZqSvTXUX!$-#|haWlb2t9Vqx^hQ-cFXq7^=|%N zRjv&Sn#fy_8<4fg?Z{oo1hN6yh-^YOBM%~5kgdo=$Ro(3$YV$se|BJ9&qh62H-EP6 z(L-(1z?Eu$qf%Yes5hF2jL$s|zb@*|vVNo|+mZUh=hQ}XRk@x!<+g6#Bg;n0{X^A? zzFpZN;70#gt{Sqqu2H0&B+>1rt zXO%YAUh0ywfBjZoYP71&X01{+Z%=k!_?9Z=v9ermwi0gX@aR~1xWBqMXWO~ueImDY zT>!4(D{t%&>odj5`)rHt%~pZKcj5TJK$XMn<&V6Nd9A^BGk2rFl~#-o=wsi#1B3Na zq*^*$snSht-5KrMZqLiW&(w!&;4f@6>y^HS+vTy|t8xKf5ALtmMx?Cgo!}+)#)@*` z5UgL`r&X(7SQ=?I?2+dn?>TgMjMY}Vx7xga{pD4q5%PPMa^C*Y@!?XX)`V;4_HN4M z?|~kC5Bfl3@PGJT_-1vq+H6%T-ml2b$H<**mc2ZYV=7#|Hn?)EHav(5->&;v=*=pf zZI%Y=<-xp1VcQAY=*n`X!P};EoqT>DBP*I6LoeUH@8$Q@YShP9RmYmO{*Jk`-!9te z-$-CP!Z{Jzv#&TVn+0hIII`8+Hq#<;t;i|fhO zfyZj~YN=K!jn6$QuWJIX+?NlmQ>qLGYrPk4bm-#zdZ6aq{j4kMjsBs;>bC)|yijtP zHwmUYpTn&}?VIpvb!zI#=X2|f>P3qhjV2>9&+WV}Z^w1#^SgBh#V%c(`)oB&r@QSq zXM1b;(8cZD+nlq$wOviU`Rs3<)GQCu`?LM_@v(mYJ!qZ9&PJ>u@B!#PV(075$ee_oi!%`)_m0`+-s(Q_K7otx6I|32 zarUMdC!QR)egE1s(sMD_8pJ-E-(JXFTj07AcVmHbKmBekaL!A)i2`RGat{+~RTw z77}%d&o5wmT}JGy`Btz-uV=!p1iPQM8mm2#(>BDaZGUuUuvp$1D>jCo9{G98S)bS$ zYoC38>ls0O^nVTO6hvNsu`^dr`xvXS?j`oetu;lBuS?i>IFy^@x*G8;LX5i{tS#cM zOxRv)znay&f!~Zi1u@3D#Lj-Xnb{usoHdtX%>N_q`z~UAKj*glh@D?@+TW9O_t_8p zL00dJcW)-|>-=(WIcS$w z+QKe&KFS&YCsy~1dnn$LIP=+`t&^}b(wL~{kxAGY8Fu5Hkz#*k+DE@}AGs%BoVDhv zv0i|9AI0AJOA6RKe^~)vjegA}e0>4ye_H{Y*Wb^PU;CW}EPwYTysm)fpx;}-#`{~? zxD&Y%D_Hvx=hFE|8JW*}c(*EuwmW#+1+3M?&1Zd_P7Jd8JcYlu0b~tgf7H$68TlV! z&I$K(MvNoQ8E1<&{f1auh_=(&?_Aak6URbqPu^SpmqXrfV;4|^Sii8pwqQ4}c`ild z_4jPV>YmGLfB|iG_ez0+o^8@Jm$eYt|KrZ}mM3-}q zn^-@B+<>@;{^A=E@6^q#A4S6Myfu$K`B<{8PWa=Ai?~mKi*e?b)6c#Aer`dm#orW> z_mhbapHC&9;@+QzlaIYWgDw{_pF=mVvpVwLiY^~MpGViHjD*h@(Bt7P5@0Nu1JqP`-$TdjJ*uN!C-e(HpteZyWsjPob+(K4;&AB!4=G5n5#QUP{p@g;B zlYb!gN!ud{M=k#Z7i;+!SWfYYT8w=ZtUbQ<$I-7x%1G4l1iF08kSEdQjDL(({BN=z zgZNv;`lca1J0VBowbDcOUPp z-^vS+DacC^ZJxh-%E$dV99`}(B<{}((dG5GCUN+`2wnfVNcg`PU0#1{7KiO9baTXa zbTqm(btBrYNOSiX^m&N(h&>kFd=dK+^oZ3KvB#ksuiYB#lU(HS{pI2}>G9}t^AUfO zp2+IsZ&Gb1Aab6UdgNJz?z^~$y%&dVF?zAqmw}^JZBgq<=)R}+h+Tqi-h1f90@jle zIqSP6&7YSi-q@vxvvex!D-d&PkMoj?+{@6dHF|godeo&Y>N*wO+}b1dG<4r7>gq$6 zE7o;7SblO{uY@zF_P7^v{!WeFy2f71(d|LtGtk{T@R{hIVSI;Yp&O&0Hs8-Vb~fVs N*mF>%R61rjLGQCvPBD=(M1cZMs&-09BDb;*)4 zi=tfGthBTZ!*(mPEVmZI5)CukG~4&EEZ2;_-}k;}c$oOBw~o*G{m$~7XL-+ihqkc? zjmxqz*@SF-w%?d6e|FEt!enrr`M6;5(#1y%4pojgdiIez?3%UZe){Z|?UU>6{C8Ek z)-PxvHz2nlw<8;oO~`}DqsSKI3FJxSY2-O%9LXjiyCAzElaalUsYo0D8;j39=p9)* z|JycK2Q^LvSE}9hN_AenuiiLxc-9H{wNZbTbt4_wIO+_a)9Q^?<-Xi0x3#mLEE_0y zudG(|ZOaY?*Sm*u{V;78wRjglaB)jNTKy9B_H3^HjaqLBN9%btcnQ_?o!wvSVYWeV zCl-01RoWOksY{M!W!+X@syC~RMy*oS*2?3$SCt1>mKxK_~`_jgz4=WJ_Ed7o`_wtSs4&Fg&nI$vMCQtByN#kL%tcxj+qnU!#!gWOS! z?!yI)HELkUYg);;qnhM>(wgLURFnLUYLXvalic<-b!MyJqK3J{Jv~(}r)!n>v0nGg zGjbp630GP^+@p{8$2;BISBg}N`YTm#MQhzxFveOupEcr9XSNnD-B;(j3U2hd8-G3g zh4n^XrK@gPFzxyKR8H_ZWBY0YQs%k|yr8eXyqsJ**D3GQtW_^84K(VWk@rsC{T|^l zR-3K!*5;mfmsgbr$nV{ibH4|N`%9Ht1Fn_Z{g%tufgXGZdQZLgfA~)LMs={-XjUt} zPmy~Z`aro+_GO72RU0 z-hLx*U$85BhU?*a^D`26W}nY-sjJ#Jr`i~5-6!MaeYkHNZ*8bn@6T(I_snv>)<3kg zRmzCXGs8BIYcappy4E8$&raKLU*Cw$UT2T?@!r|1n?{e^v(9V-T&uTM^5)v|ZO?mp zxnNw|y%_QhXLN1%ZHsF)+?;REzXRohG4B5D;ySX;;GtSywN$H=hG!j<*YzM=xhrqb zOR6+HYke4QaOK7M^+3(JM;Mp))w@>~tbTrP%5xr1opj~G>SV2>=S;Z&g4!`IP=PNQlEjiroacH z`-uG}Oh9H9*xx+U5Fc~dTU>{8tNXyUdO;q2RTk0qQvTki3MvscS`0b&mKQ*Khi*`MX6 zC!9T5?#P6*7t5WTaQ0!jE;!H39y|l_3B2u0+w%3c&a7jk4_lLuHM$?9%Fo>~IAio{ zOE`5o_t&}Xuig*$LcFoS<`Hiyu(`zcJ^drzUM7zFxTU}h__yW1soeu(?P>Z(Zhvcw zBe(rbY>meH8$4pwx7phB-{#_|!=5INI_zg+?}u~y`(5na5ZlAV-V3q)OYHfJ?OkH` zY7Z*3#u?OPZR%6-G52)DXFAy2iu_)9ntM93H#YZIZ2Szc^T_Exv&c0WeQSaBoq}Z9 zW%I&r50DRReUlLLA6(e1%~}p&G`GBXW`WcA!{K})pTDEW6j;C6NS3WWNxS&O9B0{{ z?7$rK;1}im>N~CDJam0Mm#O^sRg7}tX`GHdJ~;iX?KO<^~q1(5`=ICd1-{q`d zY@e2MkF0k9@zMXajAIaa{l)feIqm+jVyttCFJiPt>xz7@M^E#w2D|>SUykmc8-FFE z@$SiC+)l@<5zjY7zjuMPIgj|-0(*Yq>ln=w_`~R95Mx}2_=bX;mQBukySWr&{vUDP zPZH}|Z$g|$yuRSH-(2X19{vUMjhxWLKV+r2M z<9zO)zemjx*uEXudhOdOwr>YE-oBk;`*w<3|2C6l{z~^8dqU z>^pWvg6)@UM&Y#yz7qYa1Yd`K{V2RH!TN7Vu=C!VVD0xMSo{5>@a6lZPXp&QeNM9dq}jaiKtBi1is z-h^(9cQ0bzjBd>3h%sXQV!dxc*Y0}VC->#8h`j#ZO>yLV8@m3>kT}P;qs!~>-4%!b zJJ9uao#FpZbb0;PGK$r`*Hw5}5Y8C76y?ZaBe>(U3 z3O;HTM;)`#jlGuf{fM#45bHjV@dJqb>D&Z+-v-Gr5##mO?z*mFUD`f@IJZ95Y44D8-8VCS z3R#b&d;IBwkA3(I*tp2~S#*8m&AEkxz#rpXE-pcqDBTzWgKj-{A;xQW9k(-TGp>{Id&ova+r0%oo;7-x zzK={q%|fK1N7Yxd1LQml#AFOqCbdiU@q(U5hC{xqW|WC3;!R3 z^}na^{|UO>G$eBT6kUEYGEkiD&(P)95a-$d9Fd!XIM2fc7xVlA{3sGRA48W*bN&)6 zA31-8E@#fj^J{eTM7_U3mz#=2?%$%zr@4OzmXF-ON0&=;{{ful{v%i}-HShg<RnG9_Q8W9zDV+@7nZv9Pus${yVxo_ln})`~zJddE>V*%7yg~#u_HXCUhV*gh+ z=T_*k==M|nwdoUcw4*!6rQ|3vc7Wv~R-DF~!&rSg(ao>#a|PCR9{PA}_PXf#JAvgC z9|x-Bee++?`8ys$ZZ4v)IsNS^Z%%zCqI-X}`8!ms%`@5+-E-6CZ&Gp8;=co>wd@X- zQ+yn(#aMs8YLDKt2l_f}WhCm@6HY#Q+7xs-$YF{?2H_X-#Eqzi08ZuqjPzeb}#sN7xyT* z$&ArE_C%MD_hm11x%6G!8*Kh~&-X!>OW*T-!SZSD{lM~(dw+DfH1`4EH1`X@a_M_M z9W0;bo&lDR+y|n|nLFO|gV0@%^J@PO`y-d`rTyC{-OF9U?&Ty#|L!=1@leEl7SCin zY!p6n6h3+s_Kq7LXY)dI=kZ;zC%g#ZsI!+L+B|>fl#hFP1iIYeNZh9vqs!}WP2%uB z3SIwMNcg`5U0#1{7KiN^baTY}c`Uj$wIkXtFTO9wp?gQQN9^(F=8M>uqDQQ@h&=(_ zc;iQ2-cj`Dh3InDx4!6WuRxbKb`jz?V?5(45p!yf^OB3)i_xt$_HYS$ z)TJ%zIt|_2+9URKbk`Jhb)n0pb)5m0A6?g(aOTt=_d?EZK 4096) { - std.log.err("-Dbenchmark-lod-memory-budget-mb must be between 0 and 4096, got {}", .{benchmark_lod_memory_budget_mb}); - b.invalid_user_input = true; - } - options.addOption(u32, "benchmark_lod_memory_budget_mb", benchmark_lod_memory_budget_mb); - const benchmark_require_gpu_candidates = b.option(u32, "benchmark-require-gpu-candidates", "Benchmark warmup readiness target for renderable LOD regions and, when GPU culling is enabled, current GPU candidates; 0 disables the extra gate") orelse 0; - options.addOption(u32, "benchmark_require_gpu_candidates", benchmark_require_gpu_candidates); const benchmark_world = b.option([]const u8, "benchmark-world", "Benchmark auto-world generator (normal, overworld, overworld-v2, flat, test)") orelse "normal"; options.addOption([]const u8, "benchmark_world", benchmark_world); - const benchmark_fixture = b.option([]const u8, "benchmark-fixture", "Deterministic Phase 5 fixture to apply during benchmark without screenshot capture (lod-handoff)") orelse ""; - if (std.mem.eql(u8, benchmark_fixture, "gpu-culling-scale") and (benchmark_horizon_distance < 4096 or benchmark_require_gpu_candidates < 1024 or benchmark_lod_memory_budget_mb == 0 or benchmark_lod_memory_budget_mb > 2048)) { - std.log.err("-Dbenchmark-fixture=gpu-culling-scale requires horizon >=4096, readiness target >=1024, and a nonzero LOD memory budget no larger than 2048 MiB", .{}); - b.invalid_user_input = true; - } - options.addOption([]const u8, "benchmark_fixture", benchmark_fixture); options.addOption([]const u8, "benchmark_build_mode", @tagName(optimize)); const sanitize = b.option([]const u8, "sanitize", "Sanitizer profile for test builds (none, address)") orelse "none"; @@ -1139,7 +1022,6 @@ fn defineBuildOptions(b: *std.Build, optimize: std.builtin.OptimizeMode) BuildOp return .{ .options = options, .engine_ui_options = engine_ui_options, - .world_lod_options = world_lod_options, .worldgen_overworld_options = worldgen_overworld_options, .world_runtime_options = world_runtime_options, .engine_graphics_options = engine_graphics_options, @@ -1160,12 +1042,7 @@ fn defineBuildOptions(b: *std.Build, optimize: std.builtin.OptimizeMode) BuildOp .benchmark_duration = benchmark_duration, .benchmark_output = benchmark_output, .benchmark_render_distance = benchmark_render_distance, - .benchmark_horizon_distance = benchmark_horizon_distance, - .benchmark_lod_memory_budget_mb = benchmark_lod_memory_budget_mb, - .benchmark_require_gpu_candidates = benchmark_require_gpu_candidates, - .benchmark_gpu_culling = benchmark_gpu_culling, .benchmark_world = benchmark_world, - .benchmark_fixture = benchmark_fixture, .sanitize_c = sanitize_c, }; } @@ -1248,7 +1125,6 @@ fn addProjectModuleImports( module.addImport("world-core", modules.world_core); module.addImport("world-worldgen", modules.world_worldgen); module.addImport("world-meshing", modules.world_meshing); - module.addImport("world-lod", modules.world_lod); module.addImport("world-runtime", modules.world_runtime); module.addImport("world-persistence", modules.world_persistence); } diff --git a/devenv.nix b/devenv.nix index edcb48d5..1489b5ca 100644 --- a/devenv.nix +++ b/devenv.nix @@ -329,7 +329,7 @@ let rmluiBridge = pkgs.llvmPackages.libcxxStdenv.mkDerivation { pname = "zigcraft-rmlui-bridge"; version = "6.2"; - src = ./.; + src = ./libs/rmlui_bridge; nativeBuildInputs = [ pkgs.pkg-config ]; buildInputs = [ pkgs.sdl3 pkgs.freetype rmlui ]; @@ -337,8 +337,8 @@ let buildPhase = '' runHook preBuild - $CXX -std=c++17 -DRMLUI_STATIC_LIB -O2 -fPIC -I$src/libs/rmlui_bridge -I${rmlui}/include $(pkg-config --cflags sdl3) \ - -c $src/libs/rmlui_bridge/zigcraft_rmlui.cpp -o zigcraft_rmlui.o + $CXX -std=c++17 -DRMLUI_STATIC_LIB -O2 -fPIC -I$src -I${rmlui}/include $(pkg-config --cflags sdl3) \ + -c $src/zigcraft_rmlui.cpp -o zigcraft_rmlui.o ar rcs libzigcraft_rmlui_bridge.a zigcraft_rmlui.o runHook postBuild ''; @@ -346,7 +346,7 @@ let installPhase = '' runHook preInstall mkdir -p $out/include/zigcraft $out/lib/pkgconfig - cp $src/libs/rmlui_bridge/zigcraft_rmlui.h $out/include/zigcraft/ + cp $src/zigcraft_rmlui.h $out/include/zigcraft/ cp libzigcraft_rmlui_bridge.a $out/lib/ cat > $out/lib/pkgconfig/zigcraft-rmlui-bridge.pc < -f duration=60 +devenv shell zig build benchmark -Doptimize=ReleaseFast \ + -Dbenchmark-preset=low -Dbenchmark-scenario=traversal \ + -Dbenchmark-duration=60 -Dbenchmark-output=zig-out/benchmark-low.json ``` -After the run finishes, download the complete `benchmark-results` artifact and -run `assemble`; do not copy selected auto payloads into a baseline. PR and push -runs are intentionally short canaries. Scheduled/manual runs are longer -acceptance captures. Both are bounded, and neither lets warmup contaminate the -steady-state metrics; startup evidence is retained separately. A canary baseline -is regenerated from canary captures and compared only to canaries. Acceptance -captures are retained as longer evidence and are never numerically compared to -the 5-second canary: compatibility rejects a duration mismatch. - -## Reproducible scenarios - -`-Dbenchmark-scenario` selects a deterministic, bounded camera path. The -default is `traversal`; all four scenarios are captured and validated by CI. - -| Scenario | Path | Intended signal | -| --- | --- | --- | -| `stationary` | Holds the camera at `(8, 100, 8)`. | Steady-state LOD and renderer cost. | -| `traversal` | The original 60-second, smoothly interpolated six-waypoint loop. | Streaming and normal movement; default. | -| `rapid-turn` | Holds position at `(8, 100, 8)` while cycling its view every second. | Frustum, visibility, and coverage churn without streaming movement. | -| `teleport-eviction` | Jumps every four seconds between five fixed positions within ±1,536 blocks. | Streaming, cache, deferred-destruction, and eviction pressure. | - -Default benchmarks start the `overworld` generator with fixed seed `12345`, run -headless at 1920×1080, and record the scenario, seed, and build metadata in -the result JSON. The `build` object records the Zig optimize mode, headless -status, and fixed resolution, so artifacts can be compared only when their -configuration is compatible. - -Use `run_benchmark.sh` for evidence captures: it supplies the bounded external -timeout, all scenarios and both modes, evidence mode, and provenance labels. -Direct `zig build benchmark` runs are diagnostic only unless the same evidence -environment and labels are supplied. `-Dbenchmark-horizon-distance=N` (or -`run_benchmark.sh --benchmark-horizon-distance N`) changes only the coarsest -LOD horizon; it never changes near `render_distance`. `0` preserves the selected -preset horizon. Results record the effective horizon both at top level and as -`build.horizon_distance`, and compatibility rejects a different or unrecorded -horizon. Use the same preset, render-distance override, and horizon override -for before/after comparison. Unknown scenario values fail at build configuration -time. - -Large-horizon diagnostic captures can raise **only** the LOD admission budget -with `-Dbenchmark-lod-memory-budget-mb=N` or -`run_benchmark.sh --benchmark-lod-memory-budget-mb N`. `0` is the default and -uses the selected preset unchanged; values above 4096 MiB are rejected. The -requested value is recorded in `build.benchmark_lod_memory_budget_mb`, so it is -never silently compared with a different capture budget. This does not alter -production settings or normal canary/acceptance captures. - -`--benchmark-require-gpu-candidates N` is a matched-pair warmup gate. Both CPU -and GPU sources wait for at least `N` current renderable LOD regions, one second -of settled queues, and the existing minimum warmup. A GPU-culling-on source also -waits for its current candidate gauge to reach `N` and a compact submission. -The common region gauge keeps the CPU source comparable instead of inventing a -GPU-only counter for it. `0` preserves the existing warmup behavior. Targets -and readiness evidence are recorded in `build`, `completion`, and -`startup_evidence`. - -## Dedicated CPU-vs-GPU culling baseline - -`gpu-culling-baseline.json` is a separate schema-v3 artifact, not an inferred -claim from the compact matrix. It retains both full high-or-extreme/traversal source -artifacts and a derived comparison. Its sources must use the exact -`gpu-culling-scale` fixture: a bounded 32×32 lattice of **1,024** real, -non-overlapping LOD4 regions with valid compact tiles uploaded through the -production compact pool. It spans negative and positive region coordinates -across the 4,096-chunk horizon while retaining manager-owned region/mesh maps -and CPU hierarchy authority. Capture the matched 60-second pair on the same -hardware/build, with the same provenance labels and compact mode: +For a bounded matrix, use the wrapper. It applies an external timeout and +validates every emitted artifact: ```bash -# CPU culling source: GPU culling explicitly disabled. -scripts/run_benchmark.sh --duration 60 --presets extreme --scenarios traversal \ - --compact-modes auto --gpu-culling off --gpu-culling-threshold 128 \ - --benchmark-world flat --benchmark-fixture gpu-culling-scale \ - --benchmark-horizon-distance 4096 --benchmark-lod-memory-budget-mb 2048 \ - --benchmark-require-gpu-candidates 1024 \ - --output-dir gpu-culling-results/cpu --per-preset-timeout 900 - -# GPU culling source: compute culling and its delayed validator explicitly enabled. -scripts/run_benchmark.sh --duration 60 --presets extreme --scenarios traversal \ - --compact-modes auto --gpu-culling on --gpu-culling-threshold 128 \ - --benchmark-world flat --benchmark-fixture gpu-culling-scale \ - --benchmark-horizon-distance 4096 --benchmark-lod-memory-budget-mb 2048 \ - --benchmark-require-gpu-candidates 1024 \ - --output-dir gpu-culling-results/gpu --per-preset-timeout 900 - -python3 scripts/benchmark_baseline.py assemble-gpu-culling --overwrite \ - --output docs/benchmarks/gpu-culling-baseline.json \ - gpu-culling-results/cpu/auto/extreme/traversal.json \ - gpu-culling-results/gpu/auto/extreme/traversal.json -python3 scripts/benchmark_baseline.py validate-gpu-culling \ - docs/benchmarks/gpu-culling-baseline.json -``` - -The gate requires the exact `gpu-culling-scale` fixture (and rejects it from -normal compact baselines), matched seed/build/headless/resolution/hardware/toolchain, -recorded horizon, CPU requested `off`, GPU requested `on`, candidates sampled -at or above the documented **1,024-region** readiness target (not just the -culling threshold), the matched bounded memory budget and resident-region -readiness target, nonzero GPU draw submissions, and completed -delayed validation. It accepts `high` or `extreme`, but requires traversal for -at least 60 seconds and `build.horizon_distance >= 4096`; this prevents the -small default workload from pretending to prove a GPU-culling win. The CPU -source uses production visibility/direct-or-MDI fallback; the GPU source uses -production compute plus indirect draws over the identical fixture and camera -path. It also -requires zero mismatch/overflow, finite p50/p95 plus positive average/p99 culling -timestamps, at least 1% improvement in measured CPU-frame p95, and no more than -1% regression in CPU-frame p99 or total-GPU p99. LOD-only CPU categories remain -in the source artifacts for attribution; the acceptance policy uses complete -CPU frame duration because indirect submission intentionally trades candidate -preparation for fewer CPU draw submissions. -Ratios are evaluated on the raw -artifact numbers without rounding tolerances. The derived report also retains -the measured draw-call delta for review. The checked artifact is ready and -retains both full 60-second source captures. - -`run_benchmark.sh` defaults to `--gpu-culling off`, preserving existing compact -baseline commands. `--gpu-culling on` honestly sets -`ZIGCRAFT_LOD_GPU_CULLING=1`, validation on, and the requested threshold; -`off` sets both culling and validation to zero. - -The fixture has fixed 1,024-region cardinality and compact source density, so -its memory remains bounded by the documented benchmark-only 2,048 MiB admission -budget rather than normal-world horizon fill. Warmup waits for at least 1,024 -common renderable regions (and 1,024 GPU candidates in GPU mode), with the -existing bounded timeout. Start with a bounded smoke capture, inspect JSON/logs -for timeout or pressure evidence, then run the paired 60-second capture only on -hardware with enough headroom. It does not change default compact-matrix -workloads, defaults, or baselines. - -The scheduled benchmark workflow retains this bounded source pair as -`gpu-culling-results`; manual workflow dispatches can request the same capture -with `capture_gpu_culling=true`. Do not promote CI/Lavapipe sources to the -AMD/RADV baseline: provenance matching intentionally rejects that substitution. - -## LOD telemetry JSON - -Benchmark results include an opt-in `lod` object when LOD profiling is enabled. -`lod.cpu_categories.manager_lock_wait` and -`lod.cpu_categories.manager_lock_hold` report cumulative CPU timing deltas as -`total_ms` and per-sampled-frame `avg_ms`, just like the other CPU categories. - -`lod.memory_bytes` reports LOD accounting gauges as `avg_bytes`, `max_bytes`, -and `last_bytes`; these are current known allocations, not GPU-driver memory -measurements. The gauge fields are: - -- `pending_cpu_upload_bytes` -- `deferred_deletion_gpu_bytes` and `deferred_deletion_cpu_bytes` -- `pool_gpu_capacity_bytes`, `pool_gpu_allocated_bytes`, - `pool_gpu_slack_bytes`, and `pool_cpu_shadow_bytes` -- `compact_pool_capacity_bytes`, `compact_pool_allocated_bytes`, - `compact_pool_free_bytes`, and `compact_pool_retired_bytes` -- `direct_mesh_gpu_bytes` and `known_memory_bytes` - -`deferred_deletion_bytes` remains as a compatibility alias of the GPU deferred -deletion gauge. In contrast, `upload_total_bytes`, visibility totals, and -pressure counts are cumulative counter deltas over sampled frames; they must -not be interpreted as allocation gauges. - -The sampled `lod` output also includes total far-expanded and compact upload -bytes, plus worker far-expanded construction and compact-encode time. The -paired acceptance calculation uses the separate cumulative `startup_evidence` -snapshot after the fixed 10-second minimum warmup and one fully settled second; -it never derives far representation evidence from the near pool. - -Memory accounting uses an exact **zero-byte tolerance** for allocations owned -by the LOD system. `known_memory_bytes` is the checked sum of source data, -pending CPU vertices, CPU shadows, direct buffers, deferred CPU/GPU resources, -and allocated pool capacities (including slack). Unit tests compare that sum to -the component capacities exactly. GPU-driver-private allocation overhead is not -observable through the RHI and is therefore explicitly outside this total; the -telemetry must not be described as whole-process or driver VRAM usage. - -## CPU frame percentile evidence - -The checked 60-second matched `off`/`auto` acceptance matrix records complete -CPU frame p95 and p99 rather than inferring CPU improvement from average FPS. -For example, medium traversal measures p95 `5.2879402637481645` ms to -`5.0434266328811646` ms and p99 `6.667467823028565` ms to -`6.187350249290465` ms; medium rapid-turn measures p95 -`3.6425690650939946` ms to `3.4344258308410645` ms and p99 -`3.8853954601287843` ms to `3.8102028369903564` ms. High stationary, -traversal, and teleport/eviction also improve both percentiles. These are -integrated pipeline comparisons, not claims that compact representation alone -or any single Phase 1 optimization owns the full delta; rows that regress remain -present in the artifact rather than being hidden. - -## Phase 5 production gate - -Run the lightweight policy gate with: - -```bash -devenv shell zig build phase5-gate +scripts/run_benchmark.sh --duration 5 --presets low,medium,high \ + --scenarios stationary,traversal,rapid-turn,teleport-eviction \ + --benchmark-world overworld --output-dir benchmark-results ``` -It verifies that the build accepts exactly the four scenarios above (and rejects -an unbounded name), that the runtime parser recognizes the same bounded set, -that LOD3/LOD4 retain the CPU heightfield fallback when an optional mesh path -is selected, and that the tables in this document and -[`../lod-quality-controls.md`](../lod-quality-controls.md) match the runtime -benchmark SLO and LOD-preset budgets. +Results are written as `//.json`. The supported +scenarios are `stationary`, `traversal`, `rapid-turn`, and +`teleport-eviction`. -The compact visual smoke gate is intentionally a separate graphics target. It -captures deterministic production-flat seam/water pairs, a static LOD handoff, -three bounded compact-auto motion scenes, and the persisted saved-world reload, -writing PNGs, per-capture logs, and `manifest.json` to -`zig-out/phase5-visual-smoke/`. It rejects missing, black, or visually empty -captures; requires non-zero compact residency and compact submissions in auto -captures; and requires zero-overflow, zero-mismatch completed GPU validation -for the MDI motion/handoff paths. This deliberately avoids a pixel-exact golden; -override its broad health thresholds only for diagnosed platform differences with -`PHASE5_VISUAL_MIN_NON_BLACK`, `PHASE5_VISUAL_MIN_LUMA_RANGE`, -`PHASE5_VISUAL_MIN_LUMA_BINS`, and -`PHASE5_VISUAL_MAX_NMAE`. - -Run only this slower graphics check with: +To assemble a matching benchmark baseline from a complete low/medium/high +matrix: ```bash -devenv shell zig build phase5-visual-gate +python3 scripts/benchmark_baseline.py assemble --overwrite \ + --output docs/benchmarks/baseline.json benchmark-results/*/*.json ``` -## Automated motion captures - -`phase5-visual-gate` runs the following production compact-auto scenes in a -bounded session update script. The screenshot is taken only after the scripted -motion marker and settled-readiness marker are emitted; it is not a set of -static scene labels. - -| Automated scene | Session motion | Capture evidence | -| --- | --- | --- | -| `lod-handoff-traversal` | Traverses 256 blocks across LOD rings. | Completed traversal, compact residency/submission, GPU culling, delayed validation, settled capture. | -| `fog-rapid-turn` | Holds position while making two deterministic horizon rotations. | Completed turn/yaw change, compact residency/submission, GPU culling, delayed validation, settled capture. | -| `teleport-handoff` | Teleports to a fixed distant pose, then waits for full-detail/LOD handoff to settle. | Completed teleport/displacement, compact residency/submission, GPU culling, delayed validation, settled capture. | - -## Optional manual visual inspection matrix - -Use this matrix for targeted manual inspection when a LOD rendering change -needs deeper before/after evidence than the automated terminal motion captures. -The benchmark runner is headless; no routine screenshot set is required. - -| Visual concern | Scenario | Capture points | Inspect | -| --- | --- | --- | --- | -| Terrain seams | `stationary` | After readiness; after full-detail chunks settle. | Cracks, gaps, normal discontinuities, and material changes at LOD boundaries. | -| LOD transitions | `traversal` | Before, during, and after each terrain-ring handoff. | Popping, holes, duplicate terrain, and transition timing. | -| Water | `traversal` | Near water at an LOD boundary and after the boundary moves. | Shoreline cracks, water/terrain overlap, and missing distant water. | -| Fog | `rapid-turn` | Each cardinal-view turn, especially toward the horizon. | Fog discontinuities, horizon banding, and terrain visible through fog. | -| Full-detail handoff | `teleport-eviction` | Immediately after a jump and after detailed chunks replace LOD. | Stale regions, incorrect ownership, and gaps while uploads/evictions overlap. | - -Record the generated JSON filename beside each optional manual capture set. - -## Tolerance Policy - -`scripts/compare_benchmarks.sh` applies an explicit tolerance per metric: - -- Warning threshold: 5% regression. -- Failure threshold: 10% regression. -- Lower `fps.p1` is a regression. -- Higher `gpu_ms.total.avg` is a regression. -- Higher `draw_calls_avg` is a regression. - -The p1 FPS metric is the primary user-visible smoothness guard. GPU time and draw calls catch rendering-cost regressions even when FPS is noisy on virtualized runners. - -## Absolute SLOs - -The benchmark harness also enforces absolute service-level objectives before regression comparison. These thresholds are intentionally separate from `baseline.json`: a run can fail even if there is no baseline drift when the absolute floor or ceiling is breached. The LOD preset's CPU-source/mesh RAM cap and persistent-store cap are defined in [`../lod-quality-controls.md`](../lod-quality-controls.md); the GPU limits below cover measured renderer resource memory. - -| Preset | p1 FPS min | Max frame ms | Draw calls avg max | Vertices avg max | GPU memory max | -| --- | ---: | ---: | ---: | ---: | ---: | -| low | 12 | 260 | 700 | 3,500,000 | 2,200 MB | -| medium | 8 | 260 | 2,600 | 6,000,000 | 2,400 MB | -| high | 6 | 260 | 3,600 | 8,500,000 | 2,800 MB | -| ultra | 4 | 260 | 4,500 | 12,000,000 | 3,400 MB | -| extreme | 3 | 260 | 5,500 | 16,000,000 | 4,096 MB | - -The FPS floors are Lavapipe CI canary values, not player-facing hardware targets. The 260 ms spike guard allows the current Lavapipe startup spike in 5-second canary runs while still failing large stalls. Draw-call, vertex, and measured GPU resource memory ceilings are deliberately loose enough for runner variance but strict enough to fail large accidental rendering explosions. +`scripts/compare_benchmarks.sh` compares a result with the selected baseline +preset and scenario. It warns at a 5% regression and fails at 10% for p1 FPS, +average total GPU time, and average draw calls. -Benchmark history is pushed to Bencher when the `BENCHER_API_TOKEN` and `BENCHER_PROJECT` secrets are configured. Pull-request benchmark runs are gated by either relevant file changes (`src/**`, `modules/**`, `assets/shaders/**`, or `build.zig`) or the `run-benchmark` label. +The harness enforces absolute preset limits for p1 FPS, worst frame time, draw +calls, vertices, and GPU resource memory before writing a successful result. diff --git a/docs/benchmarks/acceptance-baseline.json b/docs/benchmarks/acceptance-baseline.json deleted file mode 100644 index fc476abf..00000000 --- a/docs/benchmarks/acceptance-baseline.json +++ /dev/null @@ -1,16105 +0,0 @@ -{ - "schema_version": 3, - "artifact_type": "benchmark-baseline", - "status": "ready", - "generated": true, - "required_presets": ["low", "medium", "high"], - "required_scenarios": ["stationary", "traversal", "rapid-turn", "teleport-eviction"], - "provenance": { - "world_seed": 12345, - "build.mode": "ReleaseFast", - "build.world": "overworld", - "build.fixture": "none", - "build.headless": true, - "build.resolution": [1920, 1080], - "provenance.gpu_adapter": "AMD Radeon RX 5700 XT", - "provenance.gpu_driver": "Mesa RADV 25.2.6", - "provenance.runner": "local NixOS x86_64", - "provenance.zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "capture_duration_s": 60, - "results": { - "low": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 68472, - "duration_s": 60, - "fps": { - "min": 271.9266052246094, - "avg": 1141.1873631009398, - "max": 2646.412841796875, - "p1": 726.7301165771485, - "p5": 822.9315032958984, - "p50": 1082.8827514648438, - "p95": 1670.5491149902346, - "p99": 1882.0639160156238 - }, - "frame_ms": { - "min": 0.37786999344825745, - "avg": 0.8762802957112209, - "max": 3.6774628162384033, - "p1": 0.5313316142559052, - "p5": 0.5986055612564087, - "p50": 0.923460990190506, - "p95": 1.2151679337024688, - "p99": 1.3760265767574307 - }, - "max_frame_ms": 3.6774628162384033, - "cpu_ms_avg": 0.8762802957112209, - "gpu_ms": { - "shadow_avg": 0.00036080689622872825, - "opaque_avg": 0.45590516591806085, - "lod_terrain_avg": 0.06212655327014407, - "lod_water_avg": 0.007747434279868088, - "lod_culling_avg": 0, - "total_avg": 0.6601311537155427, - "total": { - "min": 0.6551600098609924, - "avg": 0.6601311537155427, - "max": 0.6972000002861023, - "p1": 0.6573200225830078, - "p5": 0.6580400466918945, - "p50": 0.6599600315093994, - "p95": 0.6626800298690796, - "p99": 0.664400041103363 - }, - "lod_compact_terrain": { - "min": 0.061159998178482056, - "avg": 0.0619537861107973, - "max": 0.06759999692440033, - "p1": 0.06143999844789505, - "p5": 0.061560001224279404, - "p50": 0.061879999935626984, - "p95": 0.06247999891638756, - "p99": 0.06272000074386597 - }, - "lod_compact_water": { - "min": 0.006839999929070473, - "avg": 0.007581496664786669, - "max": 0.01396000012755394, - "p1": 0.007160000037401915, - "p5": 0.007280000019818544, - "p50": 0.0075599998235702515, - "p95": 0.007960000075399876, - "p99": 0.00827999971807003 - } - }, - "draw_calls_avg": 493, - "vertices_avg": 716820, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 14826, - "frame_ms": 3.6774628162384033, - "gpu_total_ms": 0.6620000600814819, - "gpu_lod_terrain_ms": 0.06239999830722809, - "gpu_lod_water_ms": 0.0075599998235702515, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45631998777389526, - "lod_cpu_ms": 0.36381400000010444, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.3281769999998687, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 21, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 68472, - "cpu_frame_ms": { - "min": 0.013205000199377537, - "avg": 0.10576299825716533, - "max": 0.629364013671875, - "p1": 0.013535999692976475, - "p5": 0.013876000419259071, - "p50": 0.15540749579668045, - "p95": 0.2341350436210632, - "p99": 0.26818298101425153 - }, - "gpu_frame_ms": { - "min": 0.06868000328540802, - "avg": 0.06987398752035406, - "max": 0.07671999931335449, - "p1": 0.06920000165700912, - "p5": 0.06936000287532806, - "p50": 0.06983999907970428, - "p95": 0.07051999866962433, - "p99": 0.0708799958229065 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 5869.038146, - "avg_ms": 0.08571442554620867 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.8450670000000002, - "avg_ms": 4.1550809089846946e-5 - }, - "upload_prep": { - "total_ms": 6.129153, - "avg_ms": 8.951327549947424e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1029.403503, - "avg_ms": 0.015033933622502629 - }, - "coverage": { - "total_ms": 827.517702, - "avg_ms": 0.012085490448650543 - }, - "eviction": { - "total_ms": 60.853539000000005, - "avg_ms": 0.0008887361111111112 - }, - "manager_lock_wait": { - "total_ms": 34.112373, - "avg_ms": 0.0004981944882579741 - }, - "manager_lock_hold": { - "total_ms": 10633.6023, - "avg_ms": 0.15529854977216964 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1574856, - "rejected_total": 1437912, - "coverage_total": 1574856, - "levels": [ - { - "candidates": 1095552, - "accepted": 410832, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 684720, - "rejected_chunk_coverage": 0, - "coverage_checks": 410832 - }, - { - "candidates": 273888, - "accepted": 136944, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 136944, - "rejected_chunk_coverage": 0, - "coverage_checks": 136944 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1643328, - "accepted": 1027080, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 616248, - "rejected_chunk_coverage": 0, - "coverage_checks": 1027080 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 2391870 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000015258789062, - "upload_total_bytes": 62389824, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 804.624581, - "worker_mesh_construction_total_ms": 82.612996, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.379283, - "compact_submissions": 337710, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000328063964844, - "requested_duration_s": 60, - "sampled_frame_count": 68472, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 74191, - "duration_s": 60, - "fps": { - "min": 193.31912231445312, - "avg": 1236.510554705512, - "max": 13155.64453125, - "p1": 366.63747558593747, - "p5": 452.1833038330078, - "p50": 1571.89208984375, - "p95": 9600.0615234375, - "p99": 11820.470703125 - }, - "frame_ms": { - "min": 0.0760129988193512, - "avg": 0.8087274275132738, - "max": 5.172794342041016, - "p1": 0.08459900319576263, - "p5": 0.10416600108146667, - "p50": 0.6361759901046753, - "p95": 2.2114925384521484, - "p99": 2.7274898529052733 - }, - "max_frame_ms": 5.172794342041016, - "cpu_ms_avg": 0.8087274275132738, - "gpu_ms": { - "shadow_avg": 0.00023941973664197267, - "opaque_avg": 0.3787937900554475, - "lod_terrain_avg": 0.04091909474182167, - "lod_water_avg": 0.02854568289233632, - "lod_culling_avg": 0, - "total_avg": 0.5140627603089917, - "total": { - "min": 0, - "avg": 0.5140627603089917, - "max": 2.025320053100586, - "p1": 0, - "p5": 0, - "p50": 0.496319979429245, - "p95": 1.6265800595283508, - "p99": 1.8081998825073242 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.04080559689898507, - "max": 0.06747999787330627, - "p1": 0, - "p5": 0, - "p50": 0.06136000156402588, - "p95": 0.06239999830722809, - "p99": 0.06276000291109085 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.028436661055362276, - "max": 0.16651999950408936, - "p1": 0, - "p5": 0, - "p50": 0.00875999964773655, - "p95": 0.09291999787092209, - "p99": 0.12756000459194183 - } - }, - "draw_calls_avg": 349.28864687091425, - "vertices_avg": 773148.710382661, - "chunks_rendered_avg": 127.75811082206737, - "worst_frame": { - "frame_index": 29199, - "frame_ms": 5.172794342041016, - "gpu_total_ms": 1.8091599941253662, - "gpu_lod_terrain_ms": 0.061560001224279404, - "gpu_lod_water_ms": 0.04907999932765961, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5959999561309814, - "lod_cpu_ms": 0.011882000000412063, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.010199000000000069, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 5, - "lod_rejected_count": 23, - "lod_coverage_count": 5, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 74191, - "cpu_frame_ms": { - "min": 0.000661000027321279, - "avg": 0.11860171668888984, - "max": 3.4054009914398193, - "p1": 0.0008820000221021473, - "p5": 0.0011220000451430678, - "p50": 0.0939359962940216, - "p95": 0.3020624965429306, - "p99": 0.41558298766613017 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06946477763080319, - "max": 0.22803999483585358, - "p1": 0, - "p5": 0, - "p50": 0.07103999704122543, - "p95": 0.15503999590873718, - "p99": 0.188960000872612 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8002.714489, - "avg_ms": 0.10786637852300145 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.9937129999999996, - "avg_ms": 4.035143076653502e-5 - }, - "upload_prep": { - "total_ms": 6.791329, - "avg_ms": 9.153844805973771e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 543.9349290000001, - "avg_ms": 0.007331548691889854 - }, - "coverage": { - "total_ms": 447.07241500000003, - "avg_ms": 0.006025965615775499 - }, - "eviction": { - "total_ms": 55.280616, - "avg_ms": 0.0007451121564610263 - }, - "manager_lock_wait": { - "total_ms": 31.599758, - "avg_ms": 0.0004259244113167365 - }, - "manager_lock_hold": { - "total_ms": 7282.318192000001, - "avg_ms": 0.09815635578439434 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 5023.008397244949, - "max_bytes": 4295232, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 5023.008397244949, - "max_bytes": 4295232, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 7411439.501179389, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 110029072.49882062, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 172369942.62729982, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184554399.00839725, - "max_bytes": 188844608, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 625217, - "rejected_total": 734067, - "coverage_total": 625222, - "levels": [ - { - "candidates": 25599, - "accepted": 6516, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 19079, - "rejected_chunk_coverage": 4, - "coverage_checks": 6520 - }, - { - "candidates": 156893, - "accepted": 48863, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 108029, - "rejected_chunk_coverage": 1, - "coverage_checks": 48864 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1176792, - "accepted": 569838, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 606954, - "rejected_chunk_coverage": 0, - "coverage_checks": 569838 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1534626 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000370979309082, - "upload_total_bytes": 62520384, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 777.63043, - "worker_mesh_construction_total_ms": 87.56293, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.291616, - "compact_submissions": 394950, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000335693359375, - "requested_duration_s": 60, - "sampled_frame_count": 74191, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 56517, - "duration_s": 60, - "fps": { - "min": 11.457269668579102, - "avg": 941.9449824670724, - "max": 2785.794677734375, - "p1": 137.70696716308595, - "p5": 242.62127075195312, - "p50": 1474.9827880859375, - "p95": 2547.2126953125, - "p99": 2665.7380273437493 - }, - "frame_ms": { - "min": 0.3589639961719513, - "avg": 1.0616331299741883, - "max": 87.28083038330078, - "p1": 0.3751306438446045, - "p5": 0.3925859868526459, - "p50": 0.6779740452766418, - "p95": 4.1216503143310526, - "p99": 7.261796607971186 - }, - "max_frame_ms": 87.28083038330078, - "cpu_ms_avg": 1.0616331299741883, - "gpu_ms": { - "shadow_avg": 0.0003607921502856257, - "opaque_avg": 0.2925049857138018, - "lod_terrain_avg": 0.06179864537858393, - "lod_water_avg": 0.02367230532807716, - "lod_culling_avg": 0, - "total_avg": 0.49457648092177897, - "total": { - "min": 0.3463599681854248, - "avg": 0.49457648092177897, - "max": 0.7743600010871887, - "p1": 0.35227999091148376, - "p5": 0.3598000109195709, - "p50": 0.4405600130558014, - "p95": 0.7437600493431091, - "p99": 0.747920036315918 - }, - "lod_compact_terrain": { - "min": 0.060600001364946365, - "avg": 0.06161213508842931, - "max": 0.06712000072002411, - "p1": 0.06088000163435936, - "p5": 0.061039999127388, - "p50": 0.061560001224279404, - "p95": 0.0623599998652935, - "p99": 0.06276000291109085 - }, - "lod_compact_water": { - "min": 0.004000000189989805, - "avg": 0.023508448796326516, - "max": 0.08856000006198883, - "p1": 0.004120000172406435, - "p5": 0.00419999985024333, - "p50": 0.00571999978274107, - "p95": 0.07671999931335449, - "p99": 0.08436000347137451 - } - }, - "draw_calls_avg": 485.86193534688675, - "vertices_avg": 717240, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 30058, - "frame_ms": 87.28083038330078, - "gpu_total_ms": 0.5791600346565247, - "gpu_lod_terrain_ms": 0.0623599998652935, - "gpu_lod_water_ms": 0.063680000603199, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.37591999769210815, - "lod_cpu_ms": 0.2906270000000859, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.24005200000010518, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 22, - "lod_rejected_count": 22, - "lod_coverage_count": 22, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 56517, - "cpu_frame_ms": { - "min": 0.01001999992877245, - "avg": 0.13931004663265487, - "max": 10.867037773132324, - "p1": 0.01135099958628416, - "p5": 0.012502999976277351, - "p50": 0.16273599863052368, - "p95": 0.2802797973155975, - "p99": 0.3535144007205961 - }, - "gpu_frame_ms": { - "min": 0.06515999883413315, - "avg": 0.08547095075145, - "max": 0.15107999742031097, - "p1": 0.06560000032186508, - "p5": 0.06576000154018402, - "p50": 0.06763999909162521, - "p95": 0.13911999762058258, - "p99": 0.14667999744415283 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 6348.938274, - "avg_ms": 0.11233678847072562 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.3929690000000003, - "avg_ms": 4.234069395049278e-5 - }, - "upload_prep": { - "total_ms": 7.4733480000000005, - "avg_ms": 0.00013223185944052232 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1151.212912, - "avg_ms": 0.02036932094767946 - }, - "coverage": { - "total_ms": 925.547762, - "avg_ms": 0.01637644889148398 - }, - "eviction": { - "total_ms": 64.41428400000001, - "avg_ms": 0.0011397328945273106 - }, - "manager_lock_wait": { - "total_ms": 32.793074000000004, - "avg_ms": 0.0005802338057575597 - }, - "manager_lock_hold": { - "total_ms": 10072.736300999999, - "avg_ms": 0.1782248934126015 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1104652, - "rejected_total": 1382096, - "coverage_total": 1104652, - "levels": [ - { - "candidates": 904272, - "accepted": 337821, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 566451, - "rejected_chunk_coverage": 0, - "coverage_checks": 337821 - }, - { - "candidates": 226068, - "accepted": 120787, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 105281, - "rejected_chunk_coverage": 0, - "coverage_checks": 120787 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1356408, - "accepted": 646044, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 710364, - "rejected_chunk_coverage": 0, - "coverage_checks": 646044 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1612278 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000676155090332, - "upload_total_bytes": 63255360, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 829.376579, - "worker_mesh_construction_total_ms": 108.077499, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.37628, - "compact_submissions": 320190, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00021743774414, - "requested_duration_s": 60, - "sampled_frame_count": 56517, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 96010, - "duration_s": 60, - "fps": { - "min": 99.01277923583984, - "avg": 1600.1444326931228, - "max": 13213.181640625, - "p1": 751.370962524414, - "p5": 930.0608276367188, - "p50": 1695.3634643554688, - "p95": 2824.411865234375, - "p99": 9245.151855468757 - }, - "frame_ms": { - "min": 0.07568199932575226, - "avg": 0.6249435860717586, - "max": 10.099706649780273, - "p1": 0.10816480569541453, - "p5": 0.3540560007095337, - "p50": 0.5898439884185791, - "p95": 1.0751984655857088, - "p99": 1.3309005463123338 - }, - "max_frame_ms": 10.099706649780273, - "cpu_ms_avg": 0.6249435860717586, - "gpu_ms": { - "shadow_avg": 0.00032815362723378024, - "opaque_avg": 0.2512480778615185, - "lod_terrain_avg": 0.05612606144230939, - "lod_water_avg": 0.021801367344047303, - "lod_culling_avg": 0, - "total_avg": 0.4343696973433334, - "total": { - "min": 0, - "avg": 0.4343696973433334, - "max": 0.6787600517272949, - "p1": 0, - "p5": 0, - "p50": 0.3806000053882599, - "p95": 0.66156005859375, - "p99": 0.6635599732398987 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.055965006143691244, - "max": 0.06747999787330627, - "p1": 0, - "p5": 0, - "p50": 0.06168000027537346, - "p95": 0.0623599998652935, - "p99": 0.06267999857664108 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.021652833656344357, - "max": 0.07528000324964523, - "p1": 0, - "p5": 0, - "p50": 0.00875999964773655, - "p95": 0.07072000205516815, - "p99": 0.07100000232458115 - } - }, - "draw_calls_avg": 437.7715342151859, - "vertices_avg": 661555.1592542443, - "chunks_rendered_avg": 112.99410478075201, - "worst_frame": { - "frame_index": 4438, - "frame_ms": 10.099706649780273, - "gpu_total_ms": 0.6603600382804871, - "gpu_lod_terrain_ms": 0.06196000054478645, - "gpu_lod_water_ms": 0.007519999984651804, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45680001378059387, - "lod_cpu_ms": 1.943132999999989, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 1.7089730000000003, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 96010, - "cpu_frame_ms": { - "min": 0.0006709999870508909, - "avg": 0.10546287712592135, - "max": 3.151784896850586, - "p1": 0.0010720000136643648, - "p5": 0.0072639998979866505, - "p50": 0.092934500426054, - "p95": 0.24521200507879262, - "p99": 0.33499820858240137 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.07792742877334136, - "max": 0.1390399932861328, - "p1": 0, - "p5": 0, - "p50": 0.07104000449180603, - "p95": 0.13303999602794647, - "p99": 0.13363999128341675 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8746.089346, - "avg_ms": 0.09109560822830956 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 3.387837, - "avg_ms": 3.528629309446933e-5 - }, - "upload_prep": { - "total_ms": 9.112609, - "avg_ms": 9.491312363295491e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1100.647712, - "avg_ms": 0.01146388617852307 - }, - "coverage": { - "total_ms": 942.239102, - "avg_ms": 0.009813968357462764 - }, - "eviction": { - "total_ms": 60.060618000000005, - "avg_ms": 0.0006255662743464224 - }, - "manager_lock_wait": { - "total_ms": 46.429835, - "avg_ms": 0.0004835937402353921 - }, - "manager_lock_hold": { - "total_ms": 13193.822134, - "avg_ms": 0.1374213325070305 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 8070.959275075513, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 8070.959275075513, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 1032299.3088219977, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 116408212.69117801, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 168649056.5766066, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184557446.95927507, - "max_bytes": 202160576, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1582914, - "rejected_total": 595238, - "coverage_total": 1582914, - "levels": [ - { - "candidates": 71008, - "accepted": 26628, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 44380, - "rejected_chunk_coverage": 0, - "coverage_checks": 26628 - }, - { - "candidates": 17752, - "accepted": 8876, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 8876, - "rejected_chunk_coverage": 0, - "coverage_checks": 8876 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2089392, - "accepted": 1547410, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 541982, - "rejected_chunk_coverage": 0, - "coverage_checks": 1547410 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 3416000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00080680847168, - "upload_total_bytes": 62431488, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 825.964084, - "worker_mesh_construction_total_ms": 102.235, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.442207, - "compact_submissions": 321180, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00087356567383, - "requested_duration_s": 60, - "sampled_frame_count": 96010, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - }, - "medium": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 21283, - "duration_s": 60, - "fps": { - "min": 216.56881713867188, - "avg": 354.707935387884, - "max": 793.8095703125, - "p1": 267.6877264404297, - "p5": 285.7043914794922, - "p50": 346.46600341796875, - "p95": 454.051544189453, - "p99": 479.93524841308596 - }, - "frame_ms": { - "min": 1.2597479820251465, - "avg": 2.819220829966686, - "max": 4.617469787597656, - "p1": 2.0836143684387207, - "p5": 2.2023930311203004, - "p50": 2.8862860202789307, - "p95": 3.500120997428894, - "p99": 3.735696225166321 - }, - "max_frame_ms": 4.617469787597656, - "cpu_ms_avg": 2.819220829966686, - "gpu_ms": { - "shadow_avg": 0.10904416440272081, - "opaque_avg": 0.8896984240617014, - "lod_terrain_avg": 0.1581487478538266, - "lod_water_avg": 0.004441048699797691, - "lod_culling_avg": 0, - "total_avg": 2.349004877849791, - "total": { - "min": 2.240000009536743, - "avg": 2.349004877849791, - "max": 2.4189600944519043, - "p1": 2.263360023498535, - "p5": 2.2777199745178223, - "p50": 2.3622400760650635, - "p95": 2.3868000507354736, - "p99": 2.395359754562378 - }, - "lod_compact_terrain": { - "min": 0.1507200002670288, - "avg": 0.15798474653420388, - "max": 0.16755999624729156, - "p1": 0.1518000066280365, - "p5": 0.15267999470233917, - "p50": 0.1586800068616867, - "p95": 0.16132000088691711, - "p99": 0.16179999709129333 - }, - "lod_compact_water": { - "min": 0.003759999992325902, - "avg": 0.004278761447538889, - "max": 0.009879999794065952, - "p1": 0.00395999988541007, - "p5": 0.004040000028908253, - "p50": 0.004279999993741512, - "p95": 0.004559999797493219, - "p99": 0.004720000084489584 - } - }, - "draw_calls_avg": 1753, - "vertices_avg": 1580466, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 4375, - "frame_ms": 4.617469787597656, - "gpu_total_ms": 2.3206801414489746, - "gpu_lod_terrain_ms": 0.1550000011920929, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8864399790763855, - "lod_cpu_ms": 0.045234000000107244, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.041677999999990334, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 33, - "lod_rejected_count": 43, - "lod_coverage_count": 33, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 21283, - "cpu_frame_ms": { - "min": 0.020800000056624413, - "avg": 0.205661794614891, - "max": 0.7434679865837097, - "p1": 0.02181199938058853, - "p5": 0.02260199934244156, - "p50": 0.30035400390625, - "p95": 0.47582018673419946, - "p99": 0.5162701642513275 - }, - "gpu_frame_ms": { - "min": 0.15515999495983124, - "avg": 0.16258979639822518, - "max": 0.172200009226799, - "p1": 0.1563200056552887, - "p5": 0.15727999806404114, - "p50": 0.16327999532222748, - "p95": 0.16595999896526337, - "p99": 0.16648000478744507 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 3688.691561, - "avg_ms": 0.17331633515011982 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.9397839999999998, - "avg_ms": 4.41565568763802e-5 - }, - "upload_prep": { - "total_ms": 2.355944, - "avg_ms": 0.0001106960484894047 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 500.09687600000007, - "avg_ms": 0.023497480430390457 - }, - "coverage": { - "total_ms": 393.857808, - "avg_ms": 0.018505746746229384 - }, - "eviction": { - "total_ms": 18.418094, - "avg_ms": 0.0008653899356293756 - }, - "manager_lock_wait": { - "total_ms": 10.449805999999999, - "avg_ms": 0.0004909930930789832 - }, - "manager_lock_hold": { - "total_ms": 3548.7657259999996, - "avg_ms": 0.16674179984024806 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 37653184, - "max_bytes": 37653184, - "last_bytes": 37653184, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 96564544, - "max_bytes": 96564544, - "last_bytes": 96564544, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 226801896, - "max_bytes": 226801896, - "last_bytes": 226801896, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67108864, - "p99_bytes": 67108864 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 702339, - "rejected_total": 915169, - "coverage_total": 702339, - "levels": [ - { - "candidates": 808754, - "accepted": 319245, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 489509, - "rejected_chunk_coverage": 0, - "coverage_checks": 319245 - }, - { - "candidates": 212830, - "accepted": 63849, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 148981, - "rejected_chunk_coverage": 0, - "coverage_checks": 63849 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 595924, - "accepted": 319245, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 276679, - "rejected_chunk_coverage": 0, - "coverage_checks": 319245 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 739680 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00272274017334, - "upload_total_bytes": 120054784, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2003.014331, - "worker_mesh_construction_total_ms": 233.602394, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.389514, - "compact_submissions": 101190, - "pool_gpu_allocated_bytes": 37653184, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.001468658447266, - "requested_duration_s": 60, - "sampled_frame_count": 21283, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 38966, - "duration_s": 60, - "fps": { - "min": 100.51338958740234, - "avg": 649.4042693317867, - "max": 13241.173828125, - "p1": 161.62007217407228, - "p5": 198.27789306640625, - "p50": 1432.250244140625, - "p95": 9480.650390625, - "p99": 11384.141064453124 - }, - "frame_ms": { - "min": 0.07552200555801392, - "avg": 1.5398728453525006, - "max": 9.948923110961914, - "p1": 0.08784149810671807, - "p5": 0.10547800362110138, - "p50": 0.6982020139694214, - "p95": 5.0434266328811646, - "p99": 6.187350249290465 - }, - "max_frame_ms": 9.948923110961914, - "cpu_ms_avg": 1.5398728453525006, - "gpu_ms": { - "shadow_avg": 0.04767921369484377, - "opaque_avg": 0.38828584912581005, - "lod_terrain_avg": 0.0438286721553807, - "lod_water_avg": 0.005577187293406031, - "lod_culling_avg": 0, - "total_avg": 0.9979361969942626, - "total": { - "min": 0, - "avg": 0.9979361969942626, - "max": 5.112080097198486, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.077859997749329, - "p99": 4.9581901073455805 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.043772313279961006, - "max": 0.19971999526023865, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.16952000558376312, - "p99": 0.1889200061559677 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.005520405480691924, - "max": 0.0652799978852272, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.052799999713897705, - "p99": 0.06328000128269196 - } - }, - "draw_calls_avg": 665.9075347739055, - "vertices_avg": 1945277.8657290973, - "chunks_rendered_avg": 329.5348765590515, - "worst_frame": { - "frame_index": 30447, - "frame_ms": 9.948923110961914, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.03037699999867982, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.02683999999999287, - "lod_worker_generation_ms": 24.915186000000176, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 2309568, - "lod_visible_count": 13, - "lod_rejected_count": 44, - "lod_coverage_count": 13, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 38966, - "cpu_frame_ms": { - "min": 0.0009110000100918114, - "avg": 0.24968370780154875, - "max": 3.724761962890625, - "p1": 0.0014130000490695238, - "p5": 0.0017630000365898013, - "p50": 0.17582599818706512, - "p95": 0.633109986782074, - "p99": 0.7839169859886164 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.04940585946464498, - "max": 0.22960001230239868, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.19223999977111816, - "p99": 0.20589400902390476 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 9002.100488, - "avg_ms": 0.2310244954062516 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 1.116782, - "avg_ms": 2.8660421906277266e-5 - }, - "state_transition": { - "total_ms": 3.1688369999999995, - "avg_ms": 8.13231278550531e-5 - }, - "upload_prep": { - "total_ms": 4.453279, - "avg_ms": 0.00011428627521428938 - }, - "upload_submission": { - "total_ms": 40.556573, - "avg_ms": 0.0010408195093158138 - }, - "visibility": { - "total_ms": 372.587575, - "avg_ms": 0.00956186354770826 - }, - "coverage": { - "total_ms": 319.02409500000005, - "avg_ms": 0.00818724259610943 - }, - "eviction": { - "total_ms": 36.165524, - "avg_ms": 0.000928130267412616 - }, - "manager_lock_wait": { - "total_ms": 19.660065000000003, - "avg_ms": 0.0005045440897192425 - }, - "manager_lock_hold": { - "total_ms": 2439.4555790000004, - "avg_ms": 0.06260472152645898 - } - }, - "workers": { - "generation_total_ms": 6113.818425, - "mesh_construction_total_ms": 705.734383, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.527522 - }, - "memory_bytes": { - "upload_total_bytes": 232940704, - "upload_avg_bytes": 5978.050197608171, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 127072, - "pending_cpu_upload_bytes": { - "avg_bytes": 1603.1009598111175, - "max_bytes": 1110912, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 222904.68777908947, - "max_bytes": 4571520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 222904.68777908947, - "max_bytes": 4571520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 67331.7973618026, - "max_bytes": 2417664, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17337961.821896013, - "max_bytes": 47806720, - "last_bytes": 5474720, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 116879766.17810398, - "max_bytes": 130577760, - "last_bytes": 128743008, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 239639.2227069753, - "max_bytes": 288800, - "last_bytes": 288800, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66869224.77729303, - "max_bytes": 66947136, - "last_bytes": 66820064, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 213231622.4077401, - "max_bytes": 233118271, - "last_bytes": 205746413, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67177798.89832161, - "max_bytes": 69526528, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67634176, - "p99_bytes": 68598208 - }, - "logical_vram_bytes": { - "avg_bytes": 201549496.6877791, - "max_bytes": 205898112, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 202858560, - "p99_bytes": 203932608 - } - }, - "visibility": { - "visible_total": 284987, - "rejected_total": 433898, - "coverage_total": 285061, - "levels": [ - { - "candidates": 58503, - "accepted": 16809, - "rejected_no_draw": 0, - "rejected_not_ready": 807, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 40815, - "rejected_chunk_coverage": 72, - "coverage_checks": 16881 - }, - { - "candidates": 109338, - "accepted": 40541, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 68795, - "rejected_chunk_coverage": 2, - "coverage_checks": 40543 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 551044, - "accepted": 227637, - "rejected_no_draw": 0, - "rejected_not_ready": 4, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 323403, - "rejected_chunk_coverage": 0, - "coverage_checks": 227637 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 575694 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001161575317383, - "upload_total_bytes": 134290816, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2069.764847, - "worker_mesh_construction_total_ms": 254.713667, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.390745, - "compact_submissions": 120420, - "pool_gpu_allocated_bytes": 47806720, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.002567291259766, - "requested_duration_s": 60, - "sampled_frame_count": 38966, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 25301, - "duration_s": 60, - "fps": { - "min": 213.1246337890625, - "avg": 421.67928368423435, - "max": 1089.044677734375, - "p1": 262.4532165527344, - "p5": 291.1694641113281, - "p50": 430.5098876953125, - "p95": 688.767333984375, - "p99": 752.63916015625 - }, - "frame_ms": { - "min": 0.9182359576225281, - "avg": 2.3714705433545293, - "max": 4.692090034484863, - "p1": 1.3286579847335815, - "p5": 1.451869010925293, - "p50": 2.322827100753784, - "p95": 3.4344258308410645, - "p99": 3.8102028369903564 - }, - "max_frame_ms": 4.692090034484863, - "cpu_ms_avg": 2.3714705433545293, - "gpu_ms": { - "shadow_avg": 0.15428008770405324, - "opaque_avg": 0.6053252567201682, - "lod_terrain_avg": 0.1562057389094021, - "lod_water_avg": 0.00865767519183177, - "lod_culling_avg": 0, - "total_avg": 2.031630992644399, - "total": { - "min": 1.476680040359497, - "avg": 2.031630992644399, - "max": 2.772599697113037, - "p1": 1.524280071258545, - "p5": 1.558000087738037, - "p50": 2.0017600059509277, - "p95": 2.6872000694274902, - "p99": 2.7239201068878174 - }, - "lod_compact_terrain": { - "min": 0.12188000231981277, - "avg": 0.1560424758112316, - "max": 0.19672000408172607, - "p1": 0.1281999945640564, - "p5": 0.1316400021314621, - "p50": 0.15639999508857727, - "p95": 0.17880000174045563, - "p99": 0.18828000128269196 - }, - "lod_compact_water": { - "min": 0.002199999988079071, - "avg": 0.008493858743610642, - "max": 0.053040001541376114, - "p1": 0.0024800000246614218, - "p5": 0.0026400000788271427, - "p50": 0.0032399999909102917, - "p95": 0.023959999904036522, - "p99": 0.04871999844908714 - } - }, - "draw_calls_avg": 1792.2180941464765, - "vertices_avg": 1616968.9824117625, - "chunks_rendered_avg": 302.0690881783329, - "worst_frame": { - "frame_index": 9269, - "frame_ms": 4.692090034484863, - "gpu_total_ms": 1.9571599960327148, - "gpu_lod_terrain_ms": 0.15860000252723694, - "gpu_lod_water_ms": 0.002959999954327941, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5702400207519531, - "lod_cpu_ms": 0.043873000000303364, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.037090000000034706, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 10140864, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 34, - "lod_rejected_count": 54, - "lod_coverage_count": 34, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 5 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 25301, - "cpu_frame_ms": { - "min": 0.02015800029039383, - "avg": 0.1999728801528056, - "max": 0.7344110012054443, - "p1": 0.022933000698685646, - "p5": 0.024555999785661697, - "p50": 0.2914069890975952, - "p95": 0.4559270143508911, - "p99": 0.5125240087509155 - }, - "gpu_frame_ms": { - "min": 0.1446399986743927, - "avg": 0.1648634141840005, - "max": 0.20043998956680298, - "p1": 0.14667999744415283, - "p5": 0.14911998808383942, - "p50": 0.1631999909877777, - "p95": 0.18435999751091003, - "p99": 0.19196000695228577 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 4146.662353999999, - "avg_ms": 0.16389321979368401 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.0660479999999999, - "avg_ms": 4.2134619185012446e-5 - }, - "upload_prep": { - "total_ms": 16.295102, - "avg_ms": 0.0006440497213548871 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 650.652955, - "avg_ms": 0.025716491640646617 - }, - "coverage": { - "total_ms": 503.97866300000004, - "avg_ms": 0.019919317932097547 - }, - "eviction": { - "total_ms": 22.264232999999997, - "avg_ms": 0.0008799744278882257 - }, - "manager_lock_wait": { - "total_ms": 12.759905999999999, - "avg_ms": 0.0005043241769100035 - }, - "manager_lock_hold": { - "total_ms": 4355.102357, - "avg_ms": 0.1721316294612861 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 10140864, - "max_bytes": 10140864, - "last_bytes": 10140864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51612160, - "max_bytes": 51612160, - "last_bytes": 51612160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 82605568, - "max_bytes": 82605568, - "last_bytes": 82605568, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 252102060, - "max_bytes": 252102060, - "last_bytes": 252102060, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 77249728, - "max_bytes": 77249728, - "last_bytes": 77249728, - "p50_bytes": 77249728, - "p95_bytes": 77249728, - "p99_bytes": 77249728 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 860154, - "rejected_total": 1366334, - "coverage_total": 860154, - "levels": [ - { - "candidates": 961438, - "accepted": 353319, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 608119, - "rejected_chunk_coverage": 0, - "coverage_checks": 353319 - }, - { - "candidates": 556622, - "accepted": 178827, - "rejected_no_draw": 0, - "rejected_not_ready": 126505, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 251290, - "rejected_chunk_coverage": 0, - "coverage_checks": 178827 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 708428, - "accepted": 328008, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 380420, - "rejected_chunk_coverage": 0, - "coverage_checks": 328008 - } - ] - }, - "pressure": { - "staging_pressure_total": 126505, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 767286 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002336502075195, - "upload_total_bytes": 163341760, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2442.520968, - "worker_mesh_construction_total_ms": 347.034829, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.691216, - "compact_submissions": 111270, - "pool_gpu_allocated_bytes": 51612160, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000667572021484, - "requested_duration_s": 60, - "sampled_frame_count": 25301, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 35575, - "duration_s": 60, - "fps": { - "min": 26.52682876586914, - "avg": 592.886389901715, - "max": 13408.4208984375, - "p1": 243.0714587402344, - "p5": 273.95556640625, - "p50": 619.6807861328125, - "p95": 8077.087499999994, - "p99": 10807.142285156253 - }, - "frame_ms": { - "min": 0.07457999885082245, - "avg": 1.6866637808396543, - "max": 37.69768524169922, - "p1": 0.09253139853477478, - "p5": 0.12380700185894966, - "p50": 1.6137340068817139, - "p95": 3.6502270698547354, - "p99": 4.1140166282653805 - }, - "max_frame_ms": 37.69768524169922, - "cpu_ms_avg": 1.6866637808396543, - "gpu_ms": { - "shadow_avg": 0.06546274070201923, - "opaque_avg": 0.44541086144792474, - "lod_terrain_avg": 0.08112583783233325, - "lod_water_avg": 0.00973485650035251, - "lod_culling_avg": 0, - "total_avg": 1.224849416534413, - "total": { - "min": 0, - "avg": 1.224849416534413, - "max": 2.7942798137664795, - "p1": 0, - "p5": 0, - "p50": 1.5824799537658691, - "p95": 2.727759909629822, - "p99": 2.7565598487854004 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.0810357914478613, - "max": 0.16524000465869904, - "p1": 0, - "p5": 0, - "p50": 0.11964000016450882, - "p95": 0.15839999914169312, - "p99": 0.16096000373363495 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.009644858486019729, - "max": 0.05640000104904175, - "p1": 0, - "p5": 0, - "p50": 0.002520000096410513, - "p95": 0.051600001752376556, - "p99": 0.05195999890565872 - } - }, - "draw_calls_avg": 977.4407589599438, - "vertices_avg": 1776630.673281799, - "chunks_rendered_avg": 298.2763457484188, - "worst_frame": { - "frame_index": 1506, - "frame_ms": 37.69768524169922, - "gpu_total_ms": 2.294400215148926, - "gpu_lod_terrain_ms": 0.15372000634670258, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.885919988155365, - "lod_cpu_ms": 0.004228000000011889, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.0015530000000003596, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 22665984, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 12 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 35575, - "cpu_frame_ms": { - "min": 0.0007220000261440873, - "avg": 0.2743054770208268, - "max": 6.96904993057251, - "p1": 0.0013930000131949782, - "p5": 0.0016740000573918223, - "p50": 0.14977100491523743, - "p95": 0.7290372908115383, - "p99": 0.9569273316860202 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.09086069420199953, - "max": 0.2067599892616272, - "p1": 0, - "p5": 0, - "p50": 0.15060000121593475, - "p95": 0.19404000043869019, - "p99": 0.20040000975131989 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8452.436845, - "avg_ms": 0.23759485158116656 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.09829399999999999, - "avg_ms": 2.7630077301475755e-6 - }, - "state_transition": { - "total_ms": 1.7323709999999999, - "avg_ms": 4.869630358397751e-5 - }, - "upload_prep": { - "total_ms": 6.005358, - "avg_ms": 0.00016880837666900913 - }, - "upload_submission": { - "total_ms": 9.031519000000003, - "avg_ms": 0.00025387263527758264 - }, - "visibility": { - "total_ms": 1075.9610199999997, - "avg_ms": 0.03024486352775825 - }, - "coverage": { - "total_ms": 992.3198, - "avg_ms": 0.027893739985945187 - }, - "eviction": { - "total_ms": 31.620691000000004, - "avg_ms": 0.00088884584680253 - }, - "manager_lock_wait": { - "total_ms": 14.289860000000001, - "avg_ms": 0.00040168264230498946 - }, - "manager_lock_hold": { - "total_ms": 4127.1187740000005, - "avg_ms": 0.11601177158116656 - } - }, - "workers": { - "generation_total_ms": 824.754598, - "mesh_construction_total_ms": 74.540435, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.38233800000000007 - }, - "memory_bytes": { - "upload_total_bytes": 52815536, - "upload_avg_bytes": 1484.6250456781447, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 202160, - "pending_cpu_upload_bytes": { - "avg_bytes": 1494996.291103303, - "max_bytes": 22665984, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 89382.89472944483, - "max_bytes": 39924288, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 89382.89472944483, - "max_bytes": 39924288, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 3977.095825720309, - "max_bytes": 1077696, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 4359729.575488404, - "max_bytes": 40086016, - "last_bytes": 363888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 129857998.4245116, - "max_bytes": 133853840, - "last_bytes": 133853840, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 341650.846493324, - "max_bytes": 363888, - "last_bytes": 363888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66767213.153506674, - "max_bytes": 66947136, - "last_bytes": 66744976, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 206373245.4624596, - "max_bytes": 265655441, - "last_bytes": 202382598, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 68607837.38692902, - "max_bytes": 89774848, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 89774848, - "p99_bytes": 89774848 - }, - "logical_vram_bytes": { - "avg_bytes": 201415974.89472944, - "max_bytes": 241250880, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 202360512 - } - }, - "visibility": { - "visible_total": 729949, - "rejected_total": 516856, - "coverage_total": 729950, - "levels": [ - { - "candidates": 68274, - "accepted": 37742, - "rejected_no_draw": 0, - "rejected_not_ready": 33, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 30498, - "rejected_chunk_coverage": 1, - "coverage_checks": 37743 - }, - { - "candidates": 43706, - "accepted": 7534, - "rejected_no_draw": 0, - "rejected_not_ready": 18100, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 18072, - "rejected_chunk_coverage": 0, - "coverage_checks": 7534 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1134825, - "accepted": 684673, - "rejected_no_draw": 0, - "rejected_not_ready": 5, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 450147, - "rejected_chunk_coverage": 0, - "coverage_checks": 684673 - } - ] - }, - "pressure": { - "staging_pressure_total": 28140, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1474226 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000977516174316, - "upload_total_bytes": 133864768, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2634.586645, - "worker_mesh_construction_total_ms": 289.406362, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.411203, - "compact_submissions": 104880, - "pool_gpu_allocated_bytes": 40086016, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.002723693847656, - "requested_duration_s": 60, - "sampled_frame_count": 35575, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - }, - "high": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 13220, - "duration_s": 60, - "fps": { - "min": 125.0868148803711, - "avg": 220.3230879036607, - "max": 357.15777587890625, - "p1": 180.81555847167968, - "p5": 189.53513717651367, - "p50": 220.7610321044922, - "p95": 264.72271728515625, - "p99": 277.1500390625 - }, - "frame_ms": { - "min": 2.7998831272125244, - "avg": 4.53878896449229, - "max": 7.994447708129883, - "p1": 3.608153803348541, - "p5": 3.7775374293327335, - "p50": 4.529784917831421, - "p95": 5.276066279411316, - "p99": 5.530497460365295 - }, - "max_frame_ms": 7.994447708129883, - "cpu_ms_avg": 4.53878896449229, - "gpu_ms": { - "shadow_avg": 0.17123455900625634, - "opaque_avg": 1.6617527709064974, - "lod_terrain_avg": 0.3294877034430244, - "lod_water_avg": 0.004754944022360248, - "lod_culling_avg": 0, - "total_avg": 3.7284749838299542, - "total": { - "min": 3.649480104446411, - "avg": 3.7284749838299542, - "max": 3.791719913482666, - "p1": 3.6752552604675293, - "p5": 3.686840057373047, - "p50": 3.730600118637085, - "p95": 3.760482144355774, - "p99": 3.769232337474823 - }, - "lod_compact_terrain": { - "min": 0.295960009098053, - "avg": 0.32932302560685445, - "max": 0.3677600026130676, - "p1": 0.29844000935554504, - "p5": 0.3011600077152252, - "p50": 0.32919999957084656, - "p95": 0.359279990196228, - "p99": 0.36131998896598816 - }, - "lod_compact_water": { - "min": 0.003640000009909272, - "avg": 0.00459113767120139, - "max": 0.010400000028312206, - "p1": 0.003920000046491623, - "p5": 0.004040000028908253, - "p50": 0.004559999797493219, - "p95": 0.005200000014156103, - "p99": 0.0055599999614059925 - } - }, - "draw_calls_avg": 2223, - "vertices_avg": 1926666, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 1745, - "frame_ms": 7.994447708129883, - "gpu_total_ms": 3.72052001953125, - "gpu_lod_terrain_ms": 0.35339999198913574, - "gpu_lod_water_ms": 0.004519999958574772, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6861599683761597, - "lod_cpu_ms": 0.07427000000006956, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.06326899999999114, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 44525376, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 35, - "lod_rejected_count": 58, - "lod_coverage_count": 35, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 13220, - "cpu_frame_ms": { - "min": 0.04106700047850609, - "avg": 0.2559182526867797, - "max": 2.929025888442993, - "p1": 0.04335371047258377, - "p5": 0.04483354911208153, - "p50": 0.35259251296520233, - "p95": 0.5461850792169569, - "p99": 0.5957804894447326 - }, - "gpu_frame_ms": { - "min": 0.3004800081253052, - "avg": 0.3342426477374901, - "max": 0.3722800016403198, - "p1": 0.30332761943340303, - "p5": 0.30612000823020935, - "p50": 0.3341200053691864, - "p95": 0.36423999071121216, - "p99": 0.36632001399993896 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 2534.663638, - "avg_ms": 0.19172947337367624 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.0517109999999998, - "avg_ms": 7.955453857791224e-5 - }, - "upload_prep": { - "total_ms": 31.463210000000004, - "avg_ms": 0.0023799704992435704 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 609.7446059999999, - "avg_ms": 0.046122890015128586 - }, - "coverage": { - "total_ms": 525.314921, - "avg_ms": 0.039736378290468985 - }, - "eviction": { - "total_ms": 15.063522999999998, - "avg_ms": 0.0011394495461422086 - }, - "manager_lock_wait": { - "total_ms": 6.800823, - "avg_ms": 0.0005144344175491679 - }, - "manager_lock_hold": { - "total_ms": 2601.8017280000004, - "avg_ms": 0.19680799757942513 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 44525376, - "max_bytes": 44525376, - "last_bytes": 44525376, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 69447616, - "max_bytes": 69447616, - "last_bytes": 69447616, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 98324544, - "max_bytes": 98324544, - "last_bytes": 98324544, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 388041731, - "max_bytes": 388041731, - "last_bytes": 388041731, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 145188672, - "max_bytes": 145188672, - "last_bytes": 145188672, - "p50_bytes": 145188672, - "p95_bytes": 145188672, - "p99_bytes": 145188672 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 462700, - "rejected_total": 766760, - "coverage_total": 462700, - "levels": [ - { - "candidates": 105760, - "accepted": 52880, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 52880, - "rejected_chunk_coverage": 0, - "coverage_checks": 52880 - }, - { - "candidates": 542020, - "accepted": 105760, - "rejected_no_draw": 0, - "rejected_not_ready": 304060, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 132200, - "rejected_chunk_coverage": 0, - "coverage_checks": 105760 - }, - { - "candidates": 211520, - "accepted": 105760, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 105760, - "rejected_chunk_coverage": 0, - "coverage_checks": 105760 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 370160, - "accepted": 198300, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 171860, - "rejected_chunk_coverage": 0, - "coverage_checks": 198300 - } - ] - }, - "pressure": { - "staging_pressure_total": 304060, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 456510 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001523971557617, - "upload_total_bytes": 332063104, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5623.422823, - "worker_mesh_construction_total_ms": 668.850835, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.558509, - "compact_submissions": 59910, - "pool_gpu_allocated_bytes": 69447616, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.0025749206543, - "requested_duration_s": 60, - "sampled_frame_count": 13220, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 31045, - "duration_s": 60, - "fps": { - "min": 38.130882263183594, - "avg": 517.4081352129125, - "max": 11923.4990234375, - "p1": 110.70329315185546, - "p5": 143.10704650878907, - "p50": 1257.6337890625, - "p95": 8218.480273437497, - "p99": 10258.7877734375 - }, - "frame_ms": { - "min": 0.08386799693107605, - "avg": 1.9327102377091576, - "max": 26.225460052490234, - "p1": 0.0974773982167244, - "p5": 0.12167700231075287, - "p50": 0.7951440215110779, - "p95": 6.987775897979736, - "p99": 9.033154945373536 - }, - "max_frame_ms": 26.225460052490234, - "cpu_ms_avg": 1.9327102377091576, - "gpu_ms": { - "shadow_avg": 0.06186360279456642, - "opaque_avg": 0.49333068130672564, - "lod_terrain_avg": 0.07465216035398621, - "lod_water_avg": 0.004686510553716999, - "lod_culling_avg": 0, - "total_avg": 1.1569442147927538, - "total": { - "min": 0, - "avg": 1.1569442147927538, - "max": 7.012320518493652, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 5.651840019226073, - "p99": 6.74139825820923 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.07461082810224937, - "max": 0.5832399725914001, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.38415200710296626, - "p99": 0.5424799919128418 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.004645001774233505, - "max": 0.09019999951124191, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.038951998949050874, - "p99": 0.07310240030288702 - } - }, - "draw_calls_avg": 621.415686906104, - "vertices_avg": 2464466.607762925, - "chunks_rendered_avg": 420.2832984377516, - "worst_frame": { - "frame_index": 10620, - "frame_ms": 26.225460052490234, - "gpu_total_ms": 5.985640048980713, - "gpu_lod_terrain_ms": 0.0960799977183342, - "gpu_lod_water_ms": 0.004600000102072954, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 2.915519952774048, - "lod_cpu_ms": 0.6524870000004057, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.5068120000000818, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 11293440, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 9, - "lod_rejected_count": 63, - "lod_coverage_count": 9, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 6 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 31045, - "cpu_frame_ms": { - "min": 0.001744000008329749, - "avg": 0.35352289251313285, - "max": 20.67719841003418, - "p1": 0.002463999902829528, - "p5": 0.0028959999326616526, - "p50": 0.3257319927215576, - "p95": 0.9670219779014586, - "p99": 1.328999209403992 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.07933867090584326, - "max": 0.5879600048065186, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44076001048088076, - "p99": 0.5489423990249634 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 9577.417245999999, - "avg_ms": 0.3085011192140441 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 3.472569, - "avg_ms": 0.00011185598325012079 - }, - "state_transition": { - "total_ms": 8.089583, - "avg_ms": 0.00026057603478821066 - }, - "upload_prep": { - "total_ms": 23.461803, - "avg_ms": 0.0007557353196972137 - }, - "upload_submission": { - "total_ms": 291.486251, - "avg_ms": 0.009389152874859075 - }, - "visibility": { - "total_ms": 420.39509799999996, - "avg_ms": 0.013541475213399903 - }, - "coverage": { - "total_ms": 374.630807, - "avg_ms": 0.01206734762441617 - }, - "eviction": { - "total_ms": 93.36743799999999, - "avg_ms": 0.0030074871315831854 - }, - "manager_lock_wait": { - "total_ms": 57.033649999999994, - "avg_ms": 0.0018371283620550812 - }, - "manager_lock_hold": { - "total_ms": 1960.1845749999998, - "avg_ms": 0.0631401054920277 - } - }, - "workers": { - "generation_total_ms": 21901.809895, - "mesh_construction_total_ms": 2751.017459, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.612541 - }, - "memory_bytes": { - "upload_total_bytes": 1040721984, - "upload_avg_bytes": 33523.01446287647, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 207936, - "pending_cpu_upload_bytes": { - "avg_bytes": 9705013.096601708, - "max_bytes": 41645952, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 893202.8876791754, - "max_bytes": 13796544, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 893202.8876791754, - "max_bytes": 13796544, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 226281.70668384602, - "max_bytes": 3847296, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 53129097.02741182, - "max_bytes": 67651824, - "last_bytes": 33253888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 114643062.97258818, - "max_bytes": 135364800, - "last_bytes": 134518272, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 267209.35107102594, - "max_bytes": 369664, - "last_bytes": 369664, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66841654.64892898, - "max_bytes": 66947136, - "last_bytes": 66739200, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 319466411.5802867, - "max_bytes": 377504319, - "last_bytes": 291622862, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 110594590.80328555, - "max_bytes": 142309248, - "last_bytes": 100663296, - "p50_bytes": 104806272, - "p95_bytes": 132402048, - "p99_bytes": 142309248 - }, - "logical_vram_bytes": { - "avg_bytes": 235774226.8876792, - "max_bytes": 248677568, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 238488704, - "p99_bytes": 240571520 - } - }, - "visibility": { - "visible_total": 226575, - "rejected_total": 437817, - "coverage_total": 226674, - "levels": [ - { - "candidates": 76239, - "accepted": 20667, - "rejected_no_draw": 0, - "rejected_not_ready": 1729, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 53751, - "rejected_chunk_coverage": 92, - "coverage_checks": 20759 - }, - { - "candidates": 152332, - "accepted": 43615, - "rejected_no_draw": 0, - "rejected_not_ready": 51805, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 56908, - "rejected_chunk_coverage": 4, - "coverage_checks": 43619 - }, - { - "candidates": 90334, - "accepted": 27507, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 908, - "rejected_range": 0, - "rejected_frustum": 61916, - "rejected_chunk_coverage": 3, - "coverage_checks": 27510 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 345487, - "accepted": 134786, - "rejected_no_draw": 0, - "rejected_not_ready": 20, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 210681, - "rejected_chunk_coverage": 0, - "coverage_checks": 134786 - } - ] - }, - "pressure": { - "staging_pressure_total": 157739, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 340552 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003334045410156, - "upload_total_bytes": 302082112, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5451.201889, - "worker_mesh_construction_total_ms": 666.605237, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.3632, - "compact_submissions": 70980, - "pool_gpu_allocated_bytes": 60885568, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000946044921875, - "requested_duration_s": 60, - "sampled_frame_count": 31045, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 14849, - "duration_s": 60, - "fps": { - "min": 98.0180892944336, - "avg": 247.47524024979663, - "max": 591.5906372070312, - "p1": 143.4627947998047, - "p5": 166.03248596191406, - "p50": 254.33566284179688, - "p95": 398.9618347167968, - "p99": 448.96775634765646 - }, - "frame_ms": { - "min": 1.69035804271698, - "avg": 4.040808280418759, - "max": 10.202198028564453, - "p1": 2.227332057952881, - "p5": 2.506505393981934, - "p50": 3.931812047958374, - "p95": 6.022917747497558, - "p99": 6.970448551177979 - }, - "max_frame_ms": 10.202198028564453, - "cpu_ms_avg": 4.040808280418759, - "gpu_ms": { - "shadow_avg": 0.272882293712347, - "opaque_avg": 1.17158315289951, - "lod_terrain_avg": 0.3911234911535475, - "lod_water_avg": 0.010429839050730067, - "lod_culling_avg": 0, - "total_avg": 3.427910923580504, - "total": { - "min": 2.340679883956909, - "avg": 3.427910923580504, - "max": 4.562960147857666, - "p1": 2.479872093200684, - "p5": 2.5858479022979735, - "p50": 3.330160140991211, - "p95": 4.426231956481933, - "p99": 4.506163215637207 - }, - "lod_compact_terrain": { - "min": 0.25411999225616455, - "avg": 0.3909594720301383, - "max": 0.5372800230979919, - "p1": 0.26663840770721436, - "p5": 0.2847599983215332, - "p50": 0.3848400115966797, - "p95": 0.5016640186309813, - "p99": 0.5238208127021791 - }, - "lod_compact_water": { - "min": 0.0022799998987466097, - "avg": 0.010266118931572582, - "max": 0.07863999903202057, - "p1": 0.002520000096410513, - "p5": 0.0026000000070780516, - "p50": 0.003759999992325902, - "p95": 0.03144000098109245, - "p99": 0.06462399929761896 - } - }, - "draw_calls_avg": 2246.467775607785, - "vertices_avg": 1947134.0457943296, - "chunks_rendered_avg": 376.5890632365816, - "worst_frame": { - "frame_index": 3469, - "frame_ms": 10.202198028564453, - "gpu_total_ms": 3.643199920654297, - "gpu_lod_terrain_ms": 0.44196000695228577, - "gpu_lod_water_ms": 0.004879999905824661, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.2935600280761719, - "lod_cpu_ms": 0.06798799999995708, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.06292799999999943, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41705280, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 61, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 14849, - "cpu_frame_ms": { - "min": 0.030406000092625618, - "avg": 0.23689409787184912, - "max": 1.0835959911346436, - "p1": 0.03448459923267364, - "p5": 0.03956500068306923, - "p50": 0.33493998646736145, - "p95": 0.5249306082725523, - "p99": 0.5637063002586364 - }, - "gpu_frame_ms": { - "min": 0.2574400007724762, - "avg": 0.40155333039720287, - "max": 0.5891200304031372, - "p1": 0.27035999298095703, - "p5": 0.28828001022338867, - "p50": 0.39103999733924866, - "p95": 0.5313999652862549, - "p99": 0.5605807852745056 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 2699.437242, - "avg_ms": 0.18179252757761466 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.6400459999999999, - "avg_ms": 4.310364334298605e-5 - }, - "upload_prep": { - "total_ms": 27.976759, - "avg_ms": 0.0018840837093406964 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 615.3717, - "avg_ms": 0.0414419624217119 - }, - "coverage": { - "total_ms": 540.149758, - "avg_ms": 0.036376170651222305 - }, - "eviction": { - "total_ms": 14.518831, - "avg_ms": 0.0009777648999932656 - }, - "manager_lock_wait": { - "total_ms": 7.6476299999999995, - "avg_ms": 0.0005150266011179203 - }, - "manager_lock_hold": { - "total_ms": 2789.8714959999998, - "avg_ms": 0.187882786450266 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 41705280, - "max_bytes": 41705280, - "last_bytes": 41705280, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 63532864, - "max_bytes": 63532864, - "last_bytes": 63532864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 104239296, - "max_bytes": 104239296, - "last_bytes": 104239296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379478144, - "max_bytes": 379478144, - "last_bytes": 379478144, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 142368576, - "max_bytes": 142368576, - "last_bytes": 142368576, - "p50_bytes": 142368576, - "p95_bytes": 142368576, - "p99_bytes": 142368576 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 404900, - "rejected_total": 842416, - "coverage_total": 404900, - "levels": [ - { - "candidates": 29698, - "accepted": 9286, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20412, - "rejected_chunk_coverage": 0, - "coverage_checks": 9286 - }, - { - "candidates": 579111, - "accepted": 86481, - "rejected_no_draw": 0, - "rejected_not_ready": 326678, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 165952, - "rejected_chunk_coverage": 0, - "coverage_checks": 86481 - }, - { - "candidates": 222735, - "accepted": 116194, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 106541, - "rejected_chunk_coverage": 0, - "coverage_checks": 116194 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 415772, - "accepted": 192939, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 222833, - "rejected_chunk_coverage": 0, - "coverage_checks": 192939 - } - ] - }, - "pressure": { - "staging_pressure_total": 326678, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 457878 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00196361541748, - "upload_total_bytes": 305541952, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5535.541056, - "worker_mesh_construction_total_ms": 649.78311, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.574984, - "compact_submissions": 72000, - "pool_gpu_allocated_bytes": 63532864, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00204086303711, - "requested_duration_s": 60, - "sampled_frame_count": 14849, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 34455, - "duration_s": 60, - "fps": { - "min": 19.929645538330078, - "avg": 574.2180820379008, - "max": 11648.087890625, - "p1": 179.60630279541016, - "p5": 195.87515258789062, - "p50": 1488.398681640625, - "p95": 8156.473828124999, - "p99": 10186.417714843748 - }, - "frame_ms": { - "min": 0.08585100620985031, - "avg": 1.7414986244442154, - "max": 50.17650604248047, - "p1": 0.09816993802785873, - "p5": 0.12260200008749962, - "p50": 0.6718629598617554, - "p95": 5.10529260635376, - "p99": 5.567733163833617 - }, - "max_frame_ms": 50.17650604248047, - "cpu_ms_avg": 1.7414986244442154, - "gpu_ms": { - "shadow_avg": 0.06647528722166514, - "opaque_avg": 0.5103335574548246, - "lod_terrain_avg": 0.11031904339513555, - "lod_water_avg": 0.007334892477928304, - "lod_culling_avg": 0, - "total_avg": 1.221069773733659, - "total": { - "min": 0, - "avg": 1.221069773733659, - "max": 4.4659199714660645, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.398531866073609, - "p99": 4.425498094558716 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.11026477897968162, - "max": 0.41064000129699707, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.39368000626564026, - "p99": 0.40588000416755676 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.007280455094775256, - "max": 0.07332000136375427, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.06307999789714813, - "p99": 0.06379999965429306 - } - }, - "draw_calls_avg": 746.5013786097809, - "vertices_avg": 2252412.041619504, - "chunks_rendered_avg": 375.01056450442604, - "worst_frame": { - "frame_index": 944, - "frame_ms": 50.17650604248047, - "gpu_total_ms": 3.6976399421691895, - "gpu_lod_terrain_ms": 0.3580799996852875, - "gpu_lod_water_ms": 0.0041600000113248825, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6767200231552124, - "lod_cpu_ms": 3.451908999999887, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 3.0687390000000008, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 49150848, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 26 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 34455, - "cpu_frame_ms": { - "min": 0.0011820000363513827, - "avg": 0.25543539216112116, - "max": 22.078073501586914, - "p1": 0.002193999942392111, - "p5": 0.0025949999690055847, - "p50": 0.24148400127887726, - "p95": 0.6191990196704864, - "p99": 0.9759074628353119 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.11765393598594162, - "max": 0.47440001368522644, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44859999418258667, - "p99": 0.4696000057458877 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 7321.95742, - "avg_ms": 0.212507833986359 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 1.9115369999999998, - "avg_ms": 5.547923378319547e-5 - }, - "state_transition": { - "total_ms": 5.60691, - "avg_ms": 0.0001627313887679582 - }, - "upload_prep": { - "total_ms": 10.434485, - "avg_ms": 0.0003028438543027137 - }, - "upload_submission": { - "total_ms": 301.661991, - "avg_ms": 0.008755245711797997 - }, - "visibility": { - "total_ms": 562.7953669999999, - "avg_ms": 0.016334214685822086 - }, - "coverage": { - "total_ms": 481.11675499999996, - "avg_ms": 0.01396362661442461 - }, - "eviction": { - "total_ms": 68.464438, - "avg_ms": 0.00198706829197504 - }, - "manager_lock_wait": { - "total_ms": 29.892848, - "avg_ms": 0.0008675910027572196 - }, - "manager_lock_hold": { - "total_ms": 2685.362896, - "avg_ms": 0.07793826428675084 - } - }, - "workers": { - "generation_total_ms": 16185.269980999998, - "mesh_construction_total_ms": 1950.486157, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.77934 - }, - "memory_bytes": { - "upload_total_bytes": 957337712, - "upload_avg_bytes": 27785.160702365403, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 340784, - "pending_cpu_upload_bytes": { - "avg_bytes": 3778017.5305761136, - "max_bytes": 49150848, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 720859.4667827601, - "max_bytes": 37689792, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 720859.4667827601, - "max_bytes": 37689792, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 141104.95986068784, - "max_bytes": 2823936, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 44736848.96032506, - "max_bytes": 80699984, - "last_bytes": 33549744, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 123035311.03967494, - "max_bytes": 138851152, - "last_bytes": 134222416, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 456796.52323320275, - "max_bytes": 502512, - "last_bytes": 502512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66652067.476766795, - "max_bytes": 66947136, - "last_bytes": 66606352, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 296366725.7070672, - "max_bytes": 391403928, - "last_bytes": 288921213, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 104582418.4904368, - "max_bytes": 149814144, - "last_bytes": 100663296, - "p50_bytes": 100663296, - "p95_bytes": 149814144, - "p99_bytes": 149814144 - }, - "logical_vram_bytes": { - "avg_bytes": 235601883.46678275, - "max_bytes": 272570816, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 237423488, - "p99_bytes": 246712064 - } - }, - "visibility": { - "visible_total": 678039, - "rejected_total": 544231, - "coverage_total": 678069, - "levels": [ - { - "candidates": 163463, - "accepted": 67876, - "rejected_no_draw": 0, - "rejected_not_ready": 377, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 95180, - "rejected_chunk_coverage": 30, - "coverage_checks": 67906 - }, - { - "candidates": 46337, - "accepted": 8725, - "rejected_no_draw": 0, - "rejected_not_ready": 25023, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 12589, - "rejected_chunk_coverage": 0, - "coverage_checks": 8725 - }, - { - "candidates": 149201, - "accepted": 113547, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 35654, - "rejected_chunk_coverage": 0, - "coverage_checks": 113547 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 863269, - "accepted": 487891, - "rejected_no_draw": 0, - "rejected_not_ready": 6, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 375372, - "rejected_chunk_coverage": 0, - "coverage_checks": 487891 - } - ] - }, - "pressure": { - "staging_pressure_total": 96875, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1039832 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001958847045898, - "upload_total_bytes": 321012352, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5327.528866, - "worker_mesh_construction_total_ms": 594.789559, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.306733, - "compact_submissions": 64050, - "pool_gpu_allocated_bytes": 62365696, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.003395080566406, - "requested_duration_s": 60, - "sampled_frame_count": 34455, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - } - }, - "compact_pairs": { - "low": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 68799, - "duration_s": 60, - "fps": { - "min": 208.33059692382812, - "avg": 1146.6282799383855, - "max": 3147.544189453125, - "p1": 767.8080615234376, - "p5": 867.2644165039063, - "p50": 1082.4185791015625, - "p95": 1636.7177978515624, - "p99": 1845.9516406250013 - }, - "frame_ms": { - "min": 0.31770801544189453, - "avg": 0.8721222191151046, - "max": 4.800063133239746, - "p1": 0.5417260193824769, - "p5": 0.610978865623474, - "p50": 0.9238569736480713, - "p95": 1.153050935268402, - "p99": 1.3024088978767396 - }, - "max_frame_ms": 4.800063133239746, - "cpu_ms_avg": 0.8721222191151046, - "gpu_ms": { - "shadow_avg": 0.0003600194767264152, - "opaque_avg": 0.45773722478192164, - "lod_terrain_avg": 0.06185801485490708, - "lod_water_avg": 0.007541085767039716, - "lod_culling_avg": 0, - "total_avg": 0.6615371038581772, - "total": { - "min": 0.6571200489997864, - "avg": 0.6615371038581772, - "max": 0.6982401013374329, - "p1": 0.6589600443840027, - "p5": 0.6596799492835999, - "p50": 0.6614000201225281, - "p95": 0.663919985294342, - "p99": 0.6653600335121155 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 463, - "vertices_avg": 717354, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 57031, - "frame_ms": 4.800063133239746, - "gpu_total_ms": 0.6622400283813477, - "gpu_lod_terrain_ms": 0.06176000088453293, - "gpu_lod_water_ms": 0.007720000110566616, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45844000577926636, - "lod_cpu_ms": 0.2880720000000565, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.23405000000002474, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41464128, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 43, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 68799, - "cpu_frame_ms": { - "min": 0.01445700041949749, - "avg": 0.10690544384339819, - "max": 0.5671070218086243, - "p1": 0.01485699974000454, - "p5": 0.015227999538183212, - "p50": 0.09137199819087982, - "p95": 0.23406000435352325, - "p99": 0.26729162931442274 - }, - "gpu_frame_ms": { - "min": 0.06815999746322632, - "avg": 0.06939910062465417, - "max": 0.07603999972343445, - "p1": 0.06868000328540802, - "p5": 0.06884000450372696, - "p50": 0.06932000070810318, - "p95": 0.07016000151634216, - "p99": 0.07051999866962433 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 5851.883068, - "avg_ms": 0.08505767624529427 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.716306, - "avg_ms": 3.9481765723339e-5 - }, - "upload_prep": { - "total_ms": 96.09532600000001, - "avg_ms": 0.0013967546912019072 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1036.82278, - "avg_ms": 0.015070317591825462 - }, - "coverage": { - "total_ms": 833.8120170000001, - "avg_ms": 0.012119536868268435 - }, - "eviction": { - "total_ms": 65.06008899999999, - "avg_ms": 0.0009456545734676375 - }, - "manager_lock_wait": { - "total_ms": 34.457912, - "avg_ms": 0.0005008490239683716 - }, - "manager_lock_hold": { - "total_ms": 5624.438303999999, - "avg_ms": 0.08175174499629353 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 41464128, - "max_bytes": 41464128, - "last_bytes": 41464128, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 33245760, - "max_bytes": 33245760, - "last_bytes": 33245760, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 33863104, - "max_bytes": 33863104, - "last_bytes": 33863104, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 229398522, - "max_bytes": 229398522, - "last_bytes": 229398522, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 108572992, - "max_bytes": 108572992, - "last_bytes": 108572992, - "p50_bytes": 108572992, - "p95_bytes": 108572992, - "p99_bytes": 108572992 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 1582377, - "rejected_total": 2958357, - "coverage_total": 1582377, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2614362, - "accepted": 550392, - "rejected_no_draw": 0, - "rejected_not_ready": 1513578, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 550392, - "rejected_chunk_coverage": 0, - "coverage_checks": 550392 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1926372, - "accepted": 1031985, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 894387, - "rejected_chunk_coverage": 0, - "coverage_checks": 1031985 - } - ] - }, - "pressure": { - "staging_pressure_total": 1513578, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000208854675293, - "upload_total_bytes": 139121088, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2294.177547, - "worker_mesh_construction_total_ms": 311.64591, - "worker_far_expanded_mesh_construction_ms": 36.726558, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33245760, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000877380371094, - "requested_duration_s": 60, - "sampled_frame_count": 68799, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 68472, - "duration_s": 60, - "fps": { - "min": 271.9266052246094, - "avg": 1141.1873631009398, - "max": 2646.412841796875, - "p1": 726.7301165771485, - "p5": 822.9315032958984, - "p50": 1082.8827514648438, - "p95": 1670.5491149902346, - "p99": 1882.0639160156238 - }, - "frame_ms": { - "min": 0.37786999344825745, - "avg": 0.8762802957112209, - "max": 3.6774628162384033, - "p1": 0.5313316142559052, - "p5": 0.5986055612564087, - "p50": 0.923460990190506, - "p95": 1.2151679337024688, - "p99": 1.3760265767574307 - }, - "max_frame_ms": 3.6774628162384033, - "cpu_ms_avg": 0.8762802957112209, - "gpu_ms": { - "shadow_avg": 0.00036080689622872825, - "opaque_avg": 0.45590516591806085, - "lod_terrain_avg": 0.06212655327014407, - "lod_water_avg": 0.007747434279868088, - "lod_culling_avg": 0, - "total_avg": 0.6601311537155427, - "total": { - "min": 0.6551600098609924, - "avg": 0.6601311537155427, - "max": 0.6972000002861023, - "p1": 0.6573200225830078, - "p5": 0.6580400466918945, - "p50": 0.6599600315093994, - "p95": 0.6626800298690796, - "p99": 0.664400041103363 - }, - "lod_compact_terrain": { - "min": 0.061159998178482056, - "avg": 0.0619537861107973, - "max": 0.06759999692440033, - "p1": 0.06143999844789505, - "p5": 0.061560001224279404, - "p50": 0.061879999935626984, - "p95": 0.06247999891638756, - "p99": 0.06272000074386597 - }, - "lod_compact_water": { - "min": 0.006839999929070473, - "avg": 0.007581496664786669, - "max": 0.01396000012755394, - "p1": 0.007160000037401915, - "p5": 0.007280000019818544, - "p50": 0.0075599998235702515, - "p95": 0.007960000075399876, - "p99": 0.00827999971807003 - } - }, - "draw_calls_avg": 493, - "vertices_avg": 716820, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 14826, - "frame_ms": 3.6774628162384033, - "gpu_total_ms": 0.6620000600814819, - "gpu_lod_terrain_ms": 0.06239999830722809, - "gpu_lod_water_ms": 0.0075599998235702515, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45631998777389526, - "lod_cpu_ms": 0.36381400000010444, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.3281769999998687, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 21, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 68472, - "cpu_frame_ms": { - "min": 0.013205000199377537, - "avg": 0.10576299825716533, - "max": 0.629364013671875, - "p1": 0.013535999692976475, - "p5": 0.013876000419259071, - "p50": 0.15540749579668045, - "p95": 0.2341350436210632, - "p99": 0.26818298101425153 - }, - "gpu_frame_ms": { - "min": 0.06868000328540802, - "avg": 0.06987398752035406, - "max": 0.07671999931335449, - "p1": 0.06920000165700912, - "p5": 0.06936000287532806, - "p50": 0.06983999907970428, - "p95": 0.07051999866962433, - "p99": 0.0708799958229065 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 5869.038146, - "avg_ms": 0.08571442554620867 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.8450670000000002, - "avg_ms": 4.1550809089846946e-5 - }, - "upload_prep": { - "total_ms": 6.129153, - "avg_ms": 8.951327549947424e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1029.403503, - "avg_ms": 0.015033933622502629 - }, - "coverage": { - "total_ms": 827.517702, - "avg_ms": 0.012085490448650543 - }, - "eviction": { - "total_ms": 60.853539000000005, - "avg_ms": 0.0008887361111111112 - }, - "manager_lock_wait": { - "total_ms": 34.112373, - "avg_ms": 0.0004981944882579741 - }, - "manager_lock_hold": { - "total_ms": 10633.6023, - "avg_ms": 0.15529854977216964 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1574856, - "rejected_total": 1437912, - "coverage_total": 1574856, - "levels": [ - { - "candidates": 1095552, - "accepted": 410832, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 684720, - "rejected_chunk_coverage": 0, - "coverage_checks": 410832 - }, - { - "candidates": 273888, - "accepted": 136944, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 136944, - "rejected_chunk_coverage": 0, - "coverage_checks": 136944 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1643328, - "accepted": 1027080, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 616248, - "rejected_chunk_coverage": 0, - "coverage_checks": 1027080 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 2391870 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000015258789062, - "upload_total_bytes": 62389824, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 804.624581, - "worker_mesh_construction_total_ms": 82.612996, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.379283, - "compact_submissions": 337710, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000328063964844, - "requested_duration_s": 60, - "sampled_frame_count": 68472, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 36.726558, - "auto": 2.379283, - "status": "measured", - "reduction_fraction": 0.9352162813623863, - "delta": -34.347274999999996 - }, - "near_pool_geometry": { - "off": 33245760, - "auto": 17749824, - "status": "measured", - "delta": -15495936 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 139121088, - "auto": 62389824, - "status": "measured", - "delta": -76731264 - }, - "startup_worker_mesh_construction_ms": { - "off": 311.64591, - "auto": 82.612996, - "status": "measured", - "delta": -229.032914 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 77144, - "duration_s": 60, - "fps": { - "min": 43.66424560546875, - "avg": 1285.7187083806327, - "max": 13365.2314453125, - "p1": 402.48097259521484, - "p5": 488.89729766845704, - "p50": 1572.4235229492188, - "p95": 9652.9755859375, - "p99": 11939.158203125 - }, - "frame_ms": { - "min": 0.07482100278139114, - "avg": 0.777775102346845, - "max": 22.90203285217285, - "p1": 0.08375800400972366, - "p5": 0.10359499603509903, - "p50": 0.6359609663486481, - "p95": 2.0454194664955123, - "p99": 2.48458951950073 - }, - "max_frame_ms": 22.90203285217285, - "cpu_ms_avg": 0.777775102346845, - "gpu_ms": { - "shadow_avg": 0.00024188854653495054, - "opaque_avg": 0.3851128248182992, - "lod_terrain_avg": 0.041526048945317695, - "lod_water_avg": 0.02788402156441606, - "lod_culling_avg": 0, - "total_avg": 0.5223262160572195, - "total": { - "min": 0, - "avg": 0.5223262160572195, - "max": 2.0196800231933594, - "p1": 0, - "p5": 0, - "p50": 0.502020001411438, - "p95": 1.6266740083694455, - "p99": 1.8076683783531182 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 339.2347168930831, - "vertices_avg": 778814.4048532614, - "chunks_rendered_avg": 127.77142746033392, - "worst_frame": { - "frame_index": 58085, - "frame_ms": 22.90203285217285, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.018994999999790707, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.014066000000070744, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 21561792, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 5, - "lod_rejected_count": 49, - "lod_coverage_count": 5, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 11 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 77144, - "cpu_frame_ms": { - "min": 0.000901999999769032, - "avg": 0.11540859798193707, - "max": 20.70973014831543, - "p1": 0.0013830000534653664, - "p5": 0.002463999902829528, - "p50": 0.09925149753689766, - "p95": 0.29620314985513674, - "p99": 0.4005875611305222 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06941007051453107, - "max": 0.22623999416828156, - "p1": 0, - "p5": 0, - "p50": 0.07131999731063843, - "p95": 0.15439399182796446, - "p99": 0.18534839466214087 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 7759.639764, - "avg_ms": 0.10058643269729337 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 3.1077380000000003, - "avg_ms": 4.0284895779321795e-5 - }, - "upload_prep": { - "total_ms": 83.82197399999998, - "avg_ms": 0.001086565047184486 - }, - "upload_submission": { - "total_ms": 101.156114, - "avg_ms": 0.0013112635331328426 - }, - "visibility": { - "total_ms": 614.3124990000001, - "avg_ms": 0.007963192199004461 - }, - "coverage": { - "total_ms": 478.46992400000005, - "avg_ms": 0.006202296017836773 - }, - "eviction": { - "total_ms": 72.55772, - "avg_ms": 0.0009405491029762523 - }, - "manager_lock_wait": { - "total_ms": 32.289807, - "avg_ms": 0.00041856537125375926 - }, - "manager_lock_hold": { - "total_ms": 4069.518509, - "avg_ms": 0.05275223619464897 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 200509824, - "upload_avg_bytes": 2599.162916104947, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 29871519.839054238, - "max_bytes": 43207488, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 32227.910816136056, - "max_bytes": 5792832, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 32227.910816136056, - "max_bytes": 5792832, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 27311779.492689, - "max_bytes": 33546048, - "last_bytes": 7354560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 39797084.507311, - "max_bytes": 59754304, - "last_bytes": 59754304, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 206899506.48462614, - "max_bytes": 232405157, - "last_bytes": 144983044, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 96980383.83905424, - "max_bytes": 110316352, - "last_bytes": 67108864, - "p50_bytes": 104841280, - "p95_bytes": 108354304, - "p99_bytes": 110316352 - }, - "logical_vram_bytes": { - "avg_bytes": 72383971.91081613, - "max_bytes": 78144576, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 73815360 - } - }, - "visibility": { - "visible_total": 900871, - "rejected_total": 2079804, - "coverage_total": 900873, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1527587, - "accepted": 238998, - "rejected_no_draw": 0, - "rejected_not_ready": 830327, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 458260, - "rejected_chunk_coverage": 2, - "coverage_checks": 239000 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1453088, - "accepted": 661873, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 791215, - "rejected_chunk_coverage": 0, - "coverage_checks": 661873 - } - ] - }, - "pressure": { - "staging_pressure_total": 1225145, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000568389892578, - "upload_total_bytes": 139282368, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2304.928896, - "worker_mesh_construction_total_ms": 273.062344, - "worker_far_expanded_mesh_construction_ms": 22.72431, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33241920, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000701904296875, - "requested_duration_s": 60, - "sampled_frame_count": 77144, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 74191, - "duration_s": 60, - "fps": { - "min": 193.31912231445312, - "avg": 1236.510554705512, - "max": 13155.64453125, - "p1": 366.63747558593747, - "p5": 452.1833038330078, - "p50": 1571.89208984375, - "p95": 9600.0615234375, - "p99": 11820.470703125 - }, - "frame_ms": { - "min": 0.0760129988193512, - "avg": 0.8087274275132738, - "max": 5.172794342041016, - "p1": 0.08459900319576263, - "p5": 0.10416600108146667, - "p50": 0.6361759901046753, - "p95": 2.2114925384521484, - "p99": 2.7274898529052733 - }, - "max_frame_ms": 5.172794342041016, - "cpu_ms_avg": 0.8087274275132738, - "gpu_ms": { - "shadow_avg": 0.00023941973664197267, - "opaque_avg": 0.3787937900554475, - "lod_terrain_avg": 0.04091909474182167, - "lod_water_avg": 0.02854568289233632, - "lod_culling_avg": 0, - "total_avg": 0.5140627603089917, - "total": { - "min": 0, - "avg": 0.5140627603089917, - "max": 2.025320053100586, - "p1": 0, - "p5": 0, - "p50": 0.496319979429245, - "p95": 1.6265800595283508, - "p99": 1.8081998825073242 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.04080559689898507, - "max": 0.06747999787330627, - "p1": 0, - "p5": 0, - "p50": 0.06136000156402588, - "p95": 0.06239999830722809, - "p99": 0.06276000291109085 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.028436661055362276, - "max": 0.16651999950408936, - "p1": 0, - "p5": 0, - "p50": 0.00875999964773655, - "p95": 0.09291999787092209, - "p99": 0.12756000459194183 - } - }, - "draw_calls_avg": 349.28864687091425, - "vertices_avg": 773148.710382661, - "chunks_rendered_avg": 127.75811082206737, - "worst_frame": { - "frame_index": 29199, - "frame_ms": 5.172794342041016, - "gpu_total_ms": 1.8091599941253662, - "gpu_lod_terrain_ms": 0.061560001224279404, - "gpu_lod_water_ms": 0.04907999932765961, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5959999561309814, - "lod_cpu_ms": 0.011882000000412063, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.010199000000000069, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 5, - "lod_rejected_count": 23, - "lod_coverage_count": 5, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 74191, - "cpu_frame_ms": { - "min": 0.000661000027321279, - "avg": 0.11860171668888984, - "max": 3.4054009914398193, - "p1": 0.0008820000221021473, - "p5": 0.0011220000451430678, - "p50": 0.0939359962940216, - "p95": 0.3020624965429306, - "p99": 0.41558298766613017 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06946477763080319, - "max": 0.22803999483585358, - "p1": 0, - "p5": 0, - "p50": 0.07103999704122543, - "p95": 0.15503999590873718, - "p99": 0.188960000872612 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8002.714489, - "avg_ms": 0.10786637852300145 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.9937129999999996, - "avg_ms": 4.035143076653502e-5 - }, - "upload_prep": { - "total_ms": 6.791329, - "avg_ms": 9.153844805973771e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 543.9349290000001, - "avg_ms": 0.007331548691889854 - }, - "coverage": { - "total_ms": 447.07241500000003, - "avg_ms": 0.006025965615775499 - }, - "eviction": { - "total_ms": 55.280616, - "avg_ms": 0.0007451121564610263 - }, - "manager_lock_wait": { - "total_ms": 31.599758, - "avg_ms": 0.0004259244113167365 - }, - "manager_lock_hold": { - "total_ms": 7282.318192000001, - "avg_ms": 0.09815635578439434 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 5023.008397244949, - "max_bytes": 4295232, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 5023.008397244949, - "max_bytes": 4295232, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 7411439.501179389, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 110029072.49882062, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 172369942.62729982, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184554399.00839725, - "max_bytes": 188844608, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 625217, - "rejected_total": 734067, - "coverage_total": 625222, - "levels": [ - { - "candidates": 25599, - "accepted": 6516, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 19079, - "rejected_chunk_coverage": 4, - "coverage_checks": 6520 - }, - { - "candidates": 156893, - "accepted": 48863, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 108029, - "rejected_chunk_coverage": 1, - "coverage_checks": 48864 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1176792, - "accepted": 569838, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 606954, - "rejected_chunk_coverage": 0, - "coverage_checks": 569838 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1534626 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000370979309082, - "upload_total_bytes": 62520384, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 777.63043, - "worker_mesh_construction_total_ms": 87.56293, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.291616, - "compact_submissions": 394950, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000335693359375, - "requested_duration_s": 60, - "sampled_frame_count": 74191, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 22.72431, - "auto": 0.291616, - "status": "measured", - "reduction_fraction": 0.9871672231192058, - "delta": -22.432693999999998 - }, - "near_pool_geometry": { - "off": 33241920, - "auto": 17749824, - "status": "measured", - "delta": -15492096 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 139282368, - "auto": 62520384, - "status": "measured", - "delta": -76761984 - }, - "startup_worker_mesh_construction_ms": { - "off": 273.062344, - "auto": 87.56293, - "status": "measured", - "delta": -185.499414 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 99744, - "duration_s": 60, - "fps": { - "min": 599.1766357421875, - "avg": 1662.3766385071292, - "max": 3454.541748046875, - "p1": 897.5301495361327, - "p5": 949.7085083007812, - "p50": 1627.6173095703125, - "p95": 3335.0525756835927, - "p99": 3394.283203125 - }, - "frame_ms": { - "min": 0.2894740104675293, - "avg": 0.6015483957342146, - "max": 1.668956995010376, - "p1": 0.2946130037307739, - "p5": 0.2998453468084335, - "p50": 0.614395022392273, - "p95": 1.0529546201229092, - "p99": 1.114168661832809 - }, - "max_frame_ms": 1.668956995010376, - "cpu_ms_avg": 0.6015483957342146, - "gpu_ms": { - "shadow_avg": 0.00035911724191618077, - "opaque_avg": 0.2756020636442675, - "lod_terrain_avg": 0.06160093517776072, - "lod_water_avg": 0.015736328600334405, - "lod_culling_avg": 0, - "total_avg": 0.47715242628640364, - "total": { - "min": 0.34624001383781433, - "avg": 0.47715242628640364, - "max": 0.7197200059890747, - "p1": 0.3570399880409241, - "p5": 0.3627999722957611, - "p50": 0.4360399842262268, - "p95": 0.7064000368118286, - "p99": 0.7106000185012817 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 463, - "vertices_avg": 716796, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 2105, - "frame_ms": 1.668956995010376, - "gpu_total_ms": 0.5157600045204163, - "gpu_lod_terrain_ms": 0.06232000142335892, - "gpu_lod_water_ms": 0.039319999516010284, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.3118399977684021, - "lod_cpu_ms": 0.014227000000005319, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.011883000000011634, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43380096, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 20, - "lod_rejected_count": 47, - "lod_coverage_count": 20, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 99744, - "cpu_frame_ms": { - "min": 0.012072999961674213, - "avg": 0.0948177852387026, - "max": 0.47415000200271606, - "p1": 0.012864430267363787, - "p5": 0.013445000164210796, - "p50": 0.12842649966478348, - "p95": 0.1959488533437252, - "p99": 0.2322627013921737 - }, - "gpu_frame_ms": { - "min": 0.06452000141143799, - "avg": 0.07733726378722683, - "max": 0.1281599998474121, - "p1": 0.06487999856472015, - "p5": 0.06511999666690826, - "p50": 0.06719999760389328, - "p95": 0.11103999614715576, - "p99": 0.1167600005865097 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 7611.238425, - "avg_ms": 0.07630773204403273 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 3.4868269999999995, - "avg_ms": 3.4957761870388186e-5 - }, - "upload_prep": { - "total_ms": 121.09565099999999, - "avg_ms": 0.0012140645151588064 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1268.90343, - "avg_ms": 0.012721601600096248 - }, - "coverage": { - "total_ms": 1012.74932, - "avg_ms": 0.010153486124478665 - }, - "eviction": { - "total_ms": 85.09699200000001, - "avg_ms": 0.0008531539942252167 - }, - "manager_lock_wait": { - "total_ms": 48.492567, - "avg_ms": 0.0004861702658806545 - }, - "manager_lock_hold": { - "total_ms": 7832.358015, - "avg_ms": 0.0785246031340231 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 43380096, - "max_bytes": 43380096, - "last_bytes": 43380096, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 33073152, - "max_bytes": 33073152, - "last_bytes": 33073152, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 34035712, - "max_bytes": 34035712, - "last_bytes": 34035712, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 232577765, - "max_bytes": 232577765, - "last_bytes": 232577765, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 110488960, - "max_bytes": 110488960, - "last_bytes": 110488960, - "p50_bytes": 110488960, - "p95_bytes": 110488960, - "p99_bytes": 110488960 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 2054828, - "rejected_total": 4628020, - "coverage_total": 2054828, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 3890016, - "accepted": 772304, - "rejected_no_draw": 0, - "rejected_not_ready": 2294112, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 823600, - "rejected_chunk_coverage": 0, - "coverage_checks": 772304 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2792832, - "accepted": 1282524, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1510308, - "rejected_chunk_coverage": 0, - "coverage_checks": 1282524 - } - ] - }, - "pressure": { - "staging_pressure_total": 2294112, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00023365020752, - "upload_total_bytes": 138602688, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2522.629935, - "worker_mesh_construction_total_ms": 347.344479, - "worker_far_expanded_mesh_construction_ms": 49.741305, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33073152, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00077819824219, - "requested_duration_s": 60, - "sampled_frame_count": 99744, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 56517, - "duration_s": 60, - "fps": { - "min": 11.457269668579102, - "avg": 941.9449824670724, - "max": 2785.794677734375, - "p1": 137.70696716308595, - "p5": 242.62127075195312, - "p50": 1474.9827880859375, - "p95": 2547.2126953125, - "p99": 2665.7380273437493 - }, - "frame_ms": { - "min": 0.3589639961719513, - "avg": 1.0616331299741883, - "max": 87.28083038330078, - "p1": 0.3751306438446045, - "p5": 0.3925859868526459, - "p50": 0.6779740452766418, - "p95": 4.1216503143310526, - "p99": 7.261796607971186 - }, - "max_frame_ms": 87.28083038330078, - "cpu_ms_avg": 1.0616331299741883, - "gpu_ms": { - "shadow_avg": 0.0003607921502856257, - "opaque_avg": 0.2925049857138018, - "lod_terrain_avg": 0.06179864537858393, - "lod_water_avg": 0.02367230532807716, - "lod_culling_avg": 0, - "total_avg": 0.49457648092177897, - "total": { - "min": 0.3463599681854248, - "avg": 0.49457648092177897, - "max": 0.7743600010871887, - "p1": 0.35227999091148376, - "p5": 0.3598000109195709, - "p50": 0.4405600130558014, - "p95": 0.7437600493431091, - "p99": 0.747920036315918 - }, - "lod_compact_terrain": { - "min": 0.060600001364946365, - "avg": 0.06161213508842931, - "max": 0.06712000072002411, - "p1": 0.06088000163435936, - "p5": 0.061039999127388, - "p50": 0.061560001224279404, - "p95": 0.0623599998652935, - "p99": 0.06276000291109085 - }, - "lod_compact_water": { - "min": 0.004000000189989805, - "avg": 0.023508448796326516, - "max": 0.08856000006198883, - "p1": 0.004120000172406435, - "p5": 0.00419999985024333, - "p50": 0.00571999978274107, - "p95": 0.07671999931335449, - "p99": 0.08436000347137451 - } - }, - "draw_calls_avg": 485.86193534688675, - "vertices_avg": 717240, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 30058, - "frame_ms": 87.28083038330078, - "gpu_total_ms": 0.5791600346565247, - "gpu_lod_terrain_ms": 0.0623599998652935, - "gpu_lod_water_ms": 0.063680000603199, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.37591999769210815, - "lod_cpu_ms": 0.2906270000000859, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.24005200000010518, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 22, - "lod_rejected_count": 22, - "lod_coverage_count": 22, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 56517, - "cpu_frame_ms": { - "min": 0.01001999992877245, - "avg": 0.13931004663265487, - "max": 10.867037773132324, - "p1": 0.01135099958628416, - "p5": 0.012502999976277351, - "p50": 0.16273599863052368, - "p95": 0.2802797973155975, - "p99": 0.3535144007205961 - }, - "gpu_frame_ms": { - "min": 0.06515999883413315, - "avg": 0.08547095075145, - "max": 0.15107999742031097, - "p1": 0.06560000032186508, - "p5": 0.06576000154018402, - "p50": 0.06763999909162521, - "p95": 0.13911999762058258, - "p99": 0.14667999744415283 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 6348.938274, - "avg_ms": 0.11233678847072562 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 2.3929690000000003, - "avg_ms": 4.234069395049278e-5 - }, - "upload_prep": { - "total_ms": 7.4733480000000005, - "avg_ms": 0.00013223185944052232 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1151.212912, - "avg_ms": 0.02036932094767946 - }, - "coverage": { - "total_ms": 925.547762, - "avg_ms": 0.01637644889148398 - }, - "eviction": { - "total_ms": 64.41428400000001, - "avg_ms": 0.0011397328945273106 - }, - "manager_lock_wait": { - "total_ms": 32.793074000000004, - "avg_ms": 0.0005802338057575597 - }, - "manager_lock_hold": { - "total_ms": 10072.736300999999, - "avg_ms": 0.1782248934126015 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1104652, - "rejected_total": 1382096, - "coverage_total": 1104652, - "levels": [ - { - "candidates": 904272, - "accepted": 337821, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 566451, - "rejected_chunk_coverage": 0, - "coverage_checks": 337821 - }, - { - "candidates": 226068, - "accepted": 120787, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 105281, - "rejected_chunk_coverage": 0, - "coverage_checks": 120787 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1356408, - "accepted": 646044, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 710364, - "rejected_chunk_coverage": 0, - "coverage_checks": 646044 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1612278 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000676155090332, - "upload_total_bytes": 63255360, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 829.376579, - "worker_mesh_construction_total_ms": 108.077499, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.37628, - "compact_submissions": 320190, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00021743774414, - "requested_duration_s": 60, - "sampled_frame_count": 56517, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 49.741305, - "auto": 1.37628, - "status": "measured", - "reduction_fraction": 0.9723312446265734, - "delta": -48.365024999999996 - }, - "near_pool_geometry": { - "off": 33073152, - "auto": 17749824, - "status": "measured", - "delta": -15323328 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 138602688, - "auto": 63255360, - "status": "measured", - "delta": -75347328 - }, - "startup_worker_mesh_construction_ms": { - "off": 347.344479, - "auto": 108.077499, - "status": "measured", - "delta": -239.26698 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 105258, - "duration_s": 60, - "fps": { - "min": 80.59586334228516, - "avg": 1754.304274190343, - "max": 13221.7421875, - "p1": 915.8339331054688, - "p5": 982.0827911376953, - "p50": 1853.3482666015625, - "p95": 3447.740966796875, - "p99": 9390.931923828119 - }, - "frame_ms": { - "min": 0.07563299685716629, - "avg": 0.5700265425514773, - "max": 12.407584190368652, - "p1": 0.10648570165038108, - "p5": 0.29004499316215515, - "p50": 0.5395640134811401, - "p95": 1.018244069814682, - "p99": 1.0919010257720947 - }, - "max_frame_ms": 12.407584190368652, - "cpu_ms_avg": 0.5700265425514773, - "gpu_ms": { - "shadow_avg": 0.0003289726213907084, - "opaque_avg": 0.24734222269084105, - "lod_terrain_avg": 0.05654398337701384, - "lod_water_avg": 0.02086977522177728, - "lod_culling_avg": 0, - "total_avg": 0.43164420178691554, - "total": { - "min": 0, - "avg": 0.43164420178691554, - "max": 0.6794400215148926, - "p1": 0, - "p5": 0, - "p50": 0.3797599971294403, - "p95": 0.6591200232505798, - "p99": 0.6613200306892395 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 409.3683140473883, - "vertices_avg": 650996.352961295, - "chunks_rendered_avg": 112.99322616808223, - "worst_frame": { - "frame_index": 4695, - "frame_ms": 12.407584190368652, - "gpu_total_ms": 0.6584000587463379, - "gpu_lod_terrain_ms": 0.06196000054478645, - "gpu_lod_water_ms": 0.00791999977082014, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.4548400044441223, - "lod_cpu_ms": 0.004157999999961248, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.001583000000000112, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41535744, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 105258, - "cpu_frame_ms": { - "min": 0.0008120000129565597, - "avg": 0.09932478015090135, - "max": 3.0281519889831543, - "p1": 0.0011320000048726797, - "p5": 0.00967899989336729, - "p50": 0.13369600474834442, - "p95": 0.2230290025472641, - "p99": 0.3272479051351545 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.07741375863032539, - "max": 0.1379999965429306, - "p1": 0, - "p5": 0, - "p50": 0.07072000205516815, - "p95": 0.1324400007724762, - "p99": 0.13291999697685242 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8762.207123999999, - "avg_ms": 0.08324504668528757 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 3.600507, - "avg_ms": 3.420649261813829e-5 - }, - "upload_prep": { - "total_ms": 15.081804000000002, - "avg_ms": 0.00014328415892378728 - }, - "upload_submission": { - "total_ms": 9.74876599999999, - "avg_ms": 9.261781527294827e-5 - }, - "visibility": { - "total_ms": 1362.460334, - "avg_ms": 0.01294400742936404 - }, - "coverage": { - "total_ms": 1166.360339, - "avg_ms": 0.011080966187843206 - }, - "eviction": { - "total_ms": 70.475759, - "avg_ms": 0.00066955251857341 - }, - "manager_lock_wait": { - "total_ms": 52.09406, - "avg_ms": 0.0004949178209732277 - }, - "manager_lock_hold": { - "total_ms": 7657.031929000001, - "avg_ms": 0.07274536784852459 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 41535744, - "upload_avg_bytes": 394.608903836288, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 2019054.2917402952, - "max_bytes": 41535744, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 17400.07250755287, - "max_bytes": 33172224, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 17400.07250755287, - "max_bytes": 33172224, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 1626251.5173003478, - "max_bytes": 33172224, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 65482612.482699655, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 144202293.57285908, - "max_bytes": 229470138, - "last_bytes": 139929944, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 69127918.2917403, - "max_bytes": 108644608, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67108864, - "p99_bytes": 108644608 - }, - "logical_vram_bytes": { - "avg_bytes": 72369144.07250756, - "max_bytes": 105523968, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 1987070, - "rejected_total": 889918, - "coverage_total": 1987070, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 178712, - "accepted": 32877, - "rejected_no_draw": 0, - "rejected_not_ready": 103580, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 42255, - "rejected_chunk_coverage": 0, - "coverage_checks": 32877 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2698276, - "accepted": 1954193, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 744083, - "rejected_chunk_coverage": 0, - "coverage_checks": 1954193 - } - ] - }, - "pressure": { - "staging_pressure_total": 112507, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000358581542969, - "upload_total_bytes": 139058496, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2271.152544, - "worker_mesh_construction_total_ms": 286.589808, - "worker_far_expanded_mesh_construction_ms": 50.380958, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33172224, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00034713745117, - "requested_duration_s": 60, - "sampled_frame_count": 105258, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 96010, - "duration_s": 60, - "fps": { - "min": 99.01277923583984, - "avg": 1600.1444326931228, - "max": 13213.181640625, - "p1": 751.370962524414, - "p5": 930.0608276367188, - "p50": 1695.3634643554688, - "p95": 2824.411865234375, - "p99": 9245.151855468757 - }, - "frame_ms": { - "min": 0.07568199932575226, - "avg": 0.6249435860717586, - "max": 10.099706649780273, - "p1": 0.10816480569541453, - "p5": 0.3540560007095337, - "p50": 0.5898439884185791, - "p95": 1.0751984655857088, - "p99": 1.3309005463123338 - }, - "max_frame_ms": 10.099706649780273, - "cpu_ms_avg": 0.6249435860717586, - "gpu_ms": { - "shadow_avg": 0.00032815362723378024, - "opaque_avg": 0.2512480778615185, - "lod_terrain_avg": 0.05612606144230939, - "lod_water_avg": 0.021801367344047303, - "lod_culling_avg": 0, - "total_avg": 0.4343696973433334, - "total": { - "min": 0, - "avg": 0.4343696973433334, - "max": 0.6787600517272949, - "p1": 0, - "p5": 0, - "p50": 0.3806000053882599, - "p95": 0.66156005859375, - "p99": 0.6635599732398987 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.055965006143691244, - "max": 0.06747999787330627, - "p1": 0, - "p5": 0, - "p50": 0.06168000027537346, - "p95": 0.0623599998652935, - "p99": 0.06267999857664108 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.021652833656344357, - "max": 0.07528000324964523, - "p1": 0, - "p5": 0, - "p50": 0.00875999964773655, - "p95": 0.07072000205516815, - "p99": 0.07100000232458115 - } - }, - "draw_calls_avg": 437.7715342151859, - "vertices_avg": 661555.1592542443, - "chunks_rendered_avg": 112.99410478075201, - "worst_frame": { - "frame_index": 4438, - "frame_ms": 10.099706649780273, - "gpu_total_ms": 0.6603600382804871, - "gpu_lod_terrain_ms": 0.06196000054478645, - "gpu_lod_water_ms": 0.007519999984651804, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45680001378059387, - "lod_cpu_ms": 1.943132999999989, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 1.7089730000000003, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 96010, - "cpu_frame_ms": { - "min": 0.0006709999870508909, - "avg": 0.10546287712592135, - "max": 3.151784896850586, - "p1": 0.0010720000136643648, - "p5": 0.0072639998979866505, - "p50": 0.092934500426054, - "p95": 0.24521200507879262, - "p99": 0.33499820858240137 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.07792742877334136, - "max": 0.1390399932861328, - "p1": 0, - "p5": 0, - "p50": 0.07104000449180603, - "p95": 0.13303999602794647, - "p99": 0.13363999128341675 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8746.089346, - "avg_ms": 0.09109560822830956 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 3.387837, - "avg_ms": 3.528629309446933e-5 - }, - "upload_prep": { - "total_ms": 9.112609, - "avg_ms": 9.491312363295491e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 1100.647712, - "avg_ms": 0.01146388617852307 - }, - "coverage": { - "total_ms": 942.239102, - "avg_ms": 0.009813968357462764 - }, - "eviction": { - "total_ms": 60.060618000000005, - "avg_ms": 0.0006255662743464224 - }, - "manager_lock_wait": { - "total_ms": 46.429835, - "avg_ms": 0.0004835937402353921 - }, - "manager_lock_hold": { - "total_ms": 13193.822134, - "avg_ms": 0.1374213325070305 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 8070.959275075513, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 8070.959275075513, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 1032299.3088219977, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 116408212.69117801, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 168649056.5766066, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184557446.95927507, - "max_bytes": 202160576, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 1582914, - "rejected_total": 595238, - "coverage_total": 1582914, - "levels": [ - { - "candidates": 71008, - "accepted": 26628, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 44380, - "rejected_chunk_coverage": 0, - "coverage_checks": 26628 - }, - { - "candidates": 17752, - "accepted": 8876, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 8876, - "rejected_chunk_coverage": 0, - "coverage_checks": 8876 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2089392, - "accepted": 1547410, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 541982, - "rejected_chunk_coverage": 0, - "coverage_checks": 1547410 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 3416000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00080680847168, - "upload_total_bytes": 62431488, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 825.964084, - "worker_mesh_construction_total_ms": 102.235, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.442207, - "compact_submissions": 321180, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00087356567383, - "requested_duration_s": 60, - "sampled_frame_count": 96010, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 50.380958, - "auto": 0.442207, - "status": "measured", - "reduction_fraction": 0.9912227353834756, - "delta": -49.938750999999996 - }, - "near_pool_geometry": { - "off": 33172224, - "auto": 17749824, - "status": "measured", - "delta": -15422400 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 139058496, - "auto": 62431488, - "status": "measured", - "delta": -76627008 - }, - "startup_worker_mesh_construction_ms": { - "off": 286.589808, - "auto": 102.235, - "status": "measured", - "delta": -184.354808 - } - } - } - }, - "medium": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 21287, - "duration_s": 60, - "fps": { - "min": 40.08667755126953, - "avg": 354.7768322322055, - "max": 1327.2861328125, - "p1": 277.12963012695315, - "p5": 297.335693359375, - "p50": 345.3451232910156, - "p95": 441.5340606689453, - "p99": 464.8534393310547 - }, - "frame_ms": { - "min": 0.7534170150756836, - "avg": 2.818673343769777, - "max": 24.94594383239746, - "p1": 2.151215724945068, - "p5": 2.26483097076416, - "p50": 2.8956539630889893, - "p95": 3.3632020950317383, - "p99": 3.6084197568893432 - }, - "max_frame_ms": 24.94594383239746, - "cpu_ms_avg": 2.818673343769777, - "gpu_ms": { - "shadow_avg": 0.10900811481033142, - "opaque_avg": 0.8903974762886683, - "lod_terrain_avg": 0.1593652144014939, - "lod_water_avg": 0.004307547323902751, - "lod_culling_avg": 0, - "total_avg": 2.351174162996265, - "total": { - "min": 2.247640371322632, - "avg": 2.351174162996265, - "max": 2.4148800373077393, - "p1": 2.266400098800659, - "p5": 2.2814519643783573, - "p50": 2.3643200397491455, - "p95": 2.387080192565918, - "p99": 2.394199848175049 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 1723, - "vertices_avg": 1580580, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 4736, - "frame_ms": 24.94594383239746, - "gpu_total_ms": 2.3471603393554688, - "gpu_lod_terrain_ms": 0.15644000470638275, - "gpu_lod_water_ms": 0.004120000172406435, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8898800015449524, - "lod_cpu_ms": 0.04234900000005837, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.03746000000001004, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 11699520, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 35, - "lod_rejected_count": 54, - "lod_coverage_count": 35, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 6 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 21287, - "cpu_frame_ms": { - "min": 0.022400999441742897, - "avg": 0.21637773338134617, - "max": 0.6458650231361389, - "p1": 0.024205999448895454, - "p5": 0.02561900019645691, - "p50": 0.09036000072956085, - "p95": 0.47688600420951843, - "p99": 0.5132147967815399 - }, - "gpu_frame_ms": { - "min": 0.15503999590873718, - "avg": 0.16367276233821365, - "max": 0.17319999635219574, - "p1": 0.15776000916957855, - "p5": 0.15916000306606293, - "p50": 0.16408000886440277, - "p95": 0.16780000925064087, - "p99": 0.16871999204158783 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 3813.7290179999995, - "avg_ms": 0.17915765575233708 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.027532, - "avg_ms": 4.827039977451026e-5 - }, - "upload_prep": { - "total_ms": 18.303894, - "avg_ms": 0.0008598625452153896 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 551.900144, - "avg_ms": 0.025926628646591815 - }, - "coverage": { - "total_ms": 425.465887, - "avg_ms": 0.019987122985859914 - }, - "eviction": { - "total_ms": 21.892014000000003, - "avg_ms": 0.0010284217597594778 - }, - "manager_lock_wait": { - "total_ms": 11.338894999999999, - "avg_ms": 0.0005326675905482219 - }, - "manager_lock_hold": { - "total_ms": 2068.777768, - "avg_ms": 0.09718503161554 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 11699520, - "max_bytes": 11699520, - "last_bytes": 11699520, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51400512, - "max_bytes": 51400512, - "last_bytes": 51400512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 15708352, - "max_bytes": 15708352, - "last_bytes": 15708352, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 193058007, - "max_bytes": 193058007, - "last_bytes": 193058007, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 78808384, - "max_bytes": 78808384, - "last_bytes": 78808384, - "p50_bytes": 78808384, - "p95_bytes": 78808384, - "p99_bytes": 78808384 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 745045, - "rejected_total": 1149498, - "coverage_total": 745045, - "levels": [ - { - "candidates": 808906, - "accepted": 319305, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 489601, - "rejected_chunk_coverage": 0, - "coverage_checks": 319305 - }, - { - "candidates": 489601, - "accepted": 106435, - "rejected_no_draw": 0, - "rejected_not_ready": 127722, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 255444, - "rejected_chunk_coverage": 0, - "coverage_checks": 106435 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 596036, - "accepted": 319305, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 276731, - "rejected_chunk_coverage": 0, - "coverage_checks": 319305 - } - ] - }, - "pressure": { - "staging_pressure_total": 127722, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00001049041748, - "upload_total_bytes": 185046336, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2971.118084, - "worker_mesh_construction_total_ms": 419.896424, - "worker_far_expanded_mesh_construction_ms": 16.191337, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51400512, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00127410888672, - "requested_duration_s": 60, - "sampled_frame_count": 21287, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 21283, - "duration_s": 60, - "fps": { - "min": 216.56881713867188, - "avg": 354.707935387884, - "max": 793.8095703125, - "p1": 267.6877264404297, - "p5": 285.7043914794922, - "p50": 346.46600341796875, - "p95": 454.051544189453, - "p99": 479.93524841308596 - }, - "frame_ms": { - "min": 1.2597479820251465, - "avg": 2.819220829966686, - "max": 4.617469787597656, - "p1": 2.0836143684387207, - "p5": 2.2023930311203004, - "p50": 2.8862860202789307, - "p95": 3.500120997428894, - "p99": 3.735696225166321 - }, - "max_frame_ms": 4.617469787597656, - "cpu_ms_avg": 2.819220829966686, - "gpu_ms": { - "shadow_avg": 0.10904416440272081, - "opaque_avg": 0.8896984240617014, - "lod_terrain_avg": 0.1581487478538266, - "lod_water_avg": 0.004441048699797691, - "lod_culling_avg": 0, - "total_avg": 2.349004877849791, - "total": { - "min": 2.240000009536743, - "avg": 2.349004877849791, - "max": 2.4189600944519043, - "p1": 2.263360023498535, - "p5": 2.2777199745178223, - "p50": 2.3622400760650635, - "p95": 2.3868000507354736, - "p99": 2.395359754562378 - }, - "lod_compact_terrain": { - "min": 0.1507200002670288, - "avg": 0.15798474653420388, - "max": 0.16755999624729156, - "p1": 0.1518000066280365, - "p5": 0.15267999470233917, - "p50": 0.1586800068616867, - "p95": 0.16132000088691711, - "p99": 0.16179999709129333 - }, - "lod_compact_water": { - "min": 0.003759999992325902, - "avg": 0.004278761447538889, - "max": 0.009879999794065952, - "p1": 0.00395999988541007, - "p5": 0.004040000028908253, - "p50": 0.004279999993741512, - "p95": 0.004559999797493219, - "p99": 0.004720000084489584 - } - }, - "draw_calls_avg": 1753, - "vertices_avg": 1580466, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 4375, - "frame_ms": 4.617469787597656, - "gpu_total_ms": 2.3206801414489746, - "gpu_lod_terrain_ms": 0.1550000011920929, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8864399790763855, - "lod_cpu_ms": 0.045234000000107244, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.041677999999990334, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 33, - "lod_rejected_count": 43, - "lod_coverage_count": 33, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 21283, - "cpu_frame_ms": { - "min": 0.020800000056624413, - "avg": 0.205661794614891, - "max": 0.7434679865837097, - "p1": 0.02181199938058853, - "p5": 0.02260199934244156, - "p50": 0.30035400390625, - "p95": 0.47582018673419946, - "p99": 0.5162701642513275 - }, - "gpu_frame_ms": { - "min": 0.15515999495983124, - "avg": 0.16258979639822518, - "max": 0.172200009226799, - "p1": 0.1563200056552887, - "p5": 0.15727999806404114, - "p50": 0.16327999532222748, - "p95": 0.16595999896526337, - "p99": 0.16648000478744507 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 3688.691561, - "avg_ms": 0.17331633515011982 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.9397839999999998, - "avg_ms": 4.41565568763802e-5 - }, - "upload_prep": { - "total_ms": 2.355944, - "avg_ms": 0.0001106960484894047 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 500.09687600000007, - "avg_ms": 0.023497480430390457 - }, - "coverage": { - "total_ms": 393.857808, - "avg_ms": 0.018505746746229384 - }, - "eviction": { - "total_ms": 18.418094, - "avg_ms": 0.0008653899356293756 - }, - "manager_lock_wait": { - "total_ms": 10.449805999999999, - "avg_ms": 0.0004909930930789832 - }, - "manager_lock_hold": { - "total_ms": 3548.7657259999996, - "avg_ms": 0.16674179984024806 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 37653184, - "max_bytes": 37653184, - "last_bytes": 37653184, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 96564544, - "max_bytes": 96564544, - "last_bytes": 96564544, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 226801896, - "max_bytes": 226801896, - "last_bytes": 226801896, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67108864, - "p99_bytes": 67108864 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 702339, - "rejected_total": 915169, - "coverage_total": 702339, - "levels": [ - { - "candidates": 808754, - "accepted": 319245, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 489509, - "rejected_chunk_coverage": 0, - "coverage_checks": 319245 - }, - { - "candidates": 212830, - "accepted": 63849, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 148981, - "rejected_chunk_coverage": 0, - "coverage_checks": 63849 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 595924, - "accepted": 319245, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 276679, - "rejected_chunk_coverage": 0, - "coverage_checks": 319245 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 739680 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00272274017334, - "upload_total_bytes": 120054784, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2003.014331, - "worker_mesh_construction_total_ms": 233.602394, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.389514, - "compact_submissions": 101190, - "pool_gpu_allocated_bytes": 37653184, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.001468658447266, - "requested_duration_s": 60, - "sampled_frame_count": 21283, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 16.191337, - "auto": 0.389514, - "status": "measured", - "reduction_fraction": 0.9759430614037617, - "delta": -15.801823 - }, - "near_pool_geometry": { - "off": 51400512, - "auto": 37653184, - "status": "measured", - "delta": -13747328 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 185046336, - "auto": 120054784, - "status": "measured", - "delta": -64991552 - }, - "startup_worker_mesh_construction_ms": { - "off": 419.896424, - "auto": 233.602394, - "status": "measured", - "delta": -186.29403000000002 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 247.647877778533, - "gpu_memory_mb_max": 250.845947265625, - "frames": 35172, - "duration_s": 60, - "fps": { - "min": 40.46984100341797, - "avg": 586.1723003779542, - "max": 12770.2509765625, - "p1": 149.98197952270507, - "p5": 189.1095527648926, - "p50": 1278.9931640625, - "p95": 8344.111328125, - "p99": 10422.017763671874 - }, - "frame_ms": { - "min": 0.07830700278282166, - "avg": 1.7059830349458964, - "max": 24.709758758544922, - "p1": 0.09595070883631707, - "p5": 0.11984499543905258, - "p50": 0.7818650305271149, - "p95": 5.2879402637481645, - "p99": 6.667467823028565 - }, - "max_frame_ms": 24.709758758544922, - "cpu_ms_avg": 1.7059830349458964, - "gpu_ms": { - "shadow_avg": 0.048490562924985374, - "opaque_avg": 0.3923261161543235, - "lod_terrain_avg": 0.044305446361013946, - "lod_water_avg": 0.0049055828475164575, - "lod_culling_avg": 0, - "total_avg": 1.0116669868400692, - "total": { - "min": 0, - "avg": 1.0116669868400692, - "max": 5.135000228881836, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.088057899475098, - "p99": 4.982459082603455 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 659.3160752871603, - "vertices_avg": 1934317.6849198225, - "chunks_rendered_avg": 328.9821448879791, - "worst_frame": { - "frame_index": 12722, - "frame_ms": 24.709758758544922, - "gpu_total_ms": 3.9499597549438477, - "gpu_lod_terrain_ms": 0.06255999952554703, - "gpu_lod_water_ms": 0.004639999940991402, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.7293200492858887, - "lod_cpu_ms": 0.49368800000019064, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.2895539999999528, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 3719616, - "lod_visible_count": 8, - "lod_rejected_count": 74, - "lod_coverage_count": 9, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 35172, - "cpu_frame_ms": { - "min": 0.0015030000358819962, - "avg": 0.2879280197997913, - "max": 21.126222610473633, - "p1": 0.002274000085890293, - "p5": 0.0028150000143796206, - "p50": 0.23590800166130066, - "p95": 0.7170675456523894, - "p99": 1.5597808802127848 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.04921102918426894, - "max": 0.22815999388694763, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.1905599981546402, - "p99": 0.2051199972629547 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8403.460825, - "avg_ms": 0.2389247362959172 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 5.916154000000001, - "avg_ms": 0.00016820635732969408 - }, - "state_transition": { - "total_ms": 13.039190999999999, - "avg_ms": 0.0003707264585465711 - }, - "upload_prep": { - "total_ms": 15.577676, - "avg_ms": 0.0004428999203912203 - }, - "upload_submission": { - "total_ms": 287.973819, - "avg_ms": 0.008187587256908905 - }, - "visibility": { - "total_ms": 474.090004, - "avg_ms": 0.013479188104173775 - }, - "coverage": { - "total_ms": 386.20847999999995, - "avg_ms": 0.010980566359604229 - }, - "eviction": { - "total_ms": 154.41607499999998, - "avg_ms": 0.0043903126066189005 - }, - "manager_lock_wait": { - "total_ms": 84.411376, - "avg_ms": 0.0023999595132491755 - }, - "manager_lock_hold": { - "total_ms": 1902.355956, - "avg_ms": 0.05408722722620266 - } - }, - "workers": { - "generation_total_ms": 33789.919389, - "mesh_construction_total_ms": 4438.835342, - "far_expanded_mesh_construction_total_ms": 26.961448000000004, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 1272618048, - "upload_avg_bytes": 36182.70351415899, - "far_expanded_upload_total_bytes": 5665728, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 2277862.9546229956, - "max_bytes": 16489152, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 1128409.8751279428, - "max_bytes": 12846336, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 1128409.8751279428, - "max_bytes": 12846336, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 268474.72671443195, - "max_bytes": 3566208, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 36441995.34356875, - "max_bytes": 51584448, - "last_bytes": 15466944, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 30666868.65643125, - "max_bytes": 54242560, - "last_bytes": 51641920, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 11326645.089503014, - "max_bytes": 14680064, - "last_bytes": 14680064, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 173555835.81001934, - "max_bytes": 201637464, - "last_bytes": 160816295, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 69655201.68133743, - "max_bytes": 83738944, - "last_bytes": 67108864, - "p50_bytes": 67642624, - "p95_bytes": 75008320, - "p99_bytes": 83598016 - }, - "logical_vram_bytes": { - "avg_bytes": 79563918.96463096, - "max_bytes": 94635264, - "last_bytes": 81788928, - "p50_bytes": 80478208, - "p95_bytes": 85084480, - "p99_bytes": 87220416 - } - }, - "visibility": { - "visible_total": 405262, - "rejected_total": 694651, - "coverage_total": 405564, - "levels": [ - { - "candidates": 277279, - "accepted": 97658, - "rejected_no_draw": 0, - "rejected_not_ready": 2899, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 176422, - "rejected_chunk_coverage": 300, - "coverage_checks": 97958 - }, - { - "candidates": 175992, - "accepted": 61387, - "rejected_no_draw": 0, - "rejected_not_ready": 18617, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 95986, - "rejected_chunk_coverage": 2, - "coverage_checks": 61389 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 646642, - "accepted": 246217, - "rejected_no_draw": 0, - "rejected_not_ready": 14, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 400411, - "rejected_chunk_coverage": 0, - "coverage_checks": 246217 - } - ] - }, - "pressure": { - "staging_pressure_total": 39716, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001051902770996, - "upload_total_bytes": 184152192, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3348.288843, - "worker_mesh_construction_total_ms": 531.777998, - "worker_far_expanded_mesh_construction_ms": 16.521254, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51584448, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00254440307617, - "requested_duration_s": 60, - "sampled_frame_count": 35172, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 38966, - "duration_s": 60, - "fps": { - "min": 100.51338958740234, - "avg": 649.4042693317867, - "max": 13241.173828125, - "p1": 161.62007217407228, - "p5": 198.27789306640625, - "p50": 1432.250244140625, - "p95": 9480.650390625, - "p99": 11384.141064453124 - }, - "frame_ms": { - "min": 0.07552200555801392, - "avg": 1.5398728453525006, - "max": 9.948923110961914, - "p1": 0.08784149810671807, - "p5": 0.10547800362110138, - "p50": 0.6982020139694214, - "p95": 5.0434266328811646, - "p99": 6.187350249290465 - }, - "max_frame_ms": 9.948923110961914, - "cpu_ms_avg": 1.5398728453525006, - "gpu_ms": { - "shadow_avg": 0.04767921369484377, - "opaque_avg": 0.38828584912581005, - "lod_terrain_avg": 0.0438286721553807, - "lod_water_avg": 0.005577187293406031, - "lod_culling_avg": 0, - "total_avg": 0.9979361969942626, - "total": { - "min": 0, - "avg": 0.9979361969942626, - "max": 5.112080097198486, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.077859997749329, - "p99": 4.9581901073455805 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.043772313279961006, - "max": 0.19971999526023865, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.16952000558376312, - "p99": 0.1889200061559677 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.005520405480691924, - "max": 0.0652799978852272, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.052799999713897705, - "p99": 0.06328000128269196 - } - }, - "draw_calls_avg": 665.9075347739055, - "vertices_avg": 1945277.8657290973, - "chunks_rendered_avg": 329.5348765590515, - "worst_frame": { - "frame_index": 30447, - "frame_ms": 9.948923110961914, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.03037699999867982, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.02683999999999287, - "lod_worker_generation_ms": 24.915186000000176, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 2309568, - "lod_visible_count": 13, - "lod_rejected_count": 44, - "lod_coverage_count": 13, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 38966, - "cpu_frame_ms": { - "min": 0.0009110000100918114, - "avg": 0.24968370780154875, - "max": 3.724761962890625, - "p1": 0.0014130000490695238, - "p5": 0.0017630000365898013, - "p50": 0.17582599818706512, - "p95": 0.633109986782074, - "p99": 0.7839169859886164 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.04940585946464498, - "max": 0.22960001230239868, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.19223999977111816, - "p99": 0.20589400902390476 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 9002.100488, - "avg_ms": 0.2310244954062516 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 1.116782, - "avg_ms": 2.8660421906277266e-5 - }, - "state_transition": { - "total_ms": 3.1688369999999995, - "avg_ms": 8.13231278550531e-5 - }, - "upload_prep": { - "total_ms": 4.453279, - "avg_ms": 0.00011428627521428938 - }, - "upload_submission": { - "total_ms": 40.556573, - "avg_ms": 0.0010408195093158138 - }, - "visibility": { - "total_ms": 372.587575, - "avg_ms": 0.00956186354770826 - }, - "coverage": { - "total_ms": 319.02409500000005, - "avg_ms": 0.00818724259610943 - }, - "eviction": { - "total_ms": 36.165524, - "avg_ms": 0.000928130267412616 - }, - "manager_lock_wait": { - "total_ms": 19.660065000000003, - "avg_ms": 0.0005045440897192425 - }, - "manager_lock_hold": { - "total_ms": 2439.4555790000004, - "avg_ms": 0.06260472152645898 - } - }, - "workers": { - "generation_total_ms": 6113.818425, - "mesh_construction_total_ms": 705.734383, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.527522 - }, - "memory_bytes": { - "upload_total_bytes": 232940704, - "upload_avg_bytes": 5978.050197608171, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 127072, - "pending_cpu_upload_bytes": { - "avg_bytes": 1603.1009598111175, - "max_bytes": 1110912, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 222904.68777908947, - "max_bytes": 4571520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 222904.68777908947, - "max_bytes": 4571520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 67331.7973618026, - "max_bytes": 2417664, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17337961.821896013, - "max_bytes": 47806720, - "last_bytes": 5474720, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 116879766.17810398, - "max_bytes": 130577760, - "last_bytes": 128743008, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 239639.2227069753, - "max_bytes": 288800, - "last_bytes": 288800, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66869224.77729303, - "max_bytes": 66947136, - "last_bytes": 66820064, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 213231622.4077401, - "max_bytes": 233118271, - "last_bytes": 205746413, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67177798.89832161, - "max_bytes": 69526528, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67634176, - "p99_bytes": 68598208 - }, - "logical_vram_bytes": { - "avg_bytes": 201549496.6877791, - "max_bytes": 205898112, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 202858560, - "p99_bytes": 203932608 - } - }, - "visibility": { - "visible_total": 284987, - "rejected_total": 433898, - "coverage_total": 285061, - "levels": [ - { - "candidates": 58503, - "accepted": 16809, - "rejected_no_draw": 0, - "rejected_not_ready": 807, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 40815, - "rejected_chunk_coverage": 72, - "coverage_checks": 16881 - }, - { - "candidates": 109338, - "accepted": 40541, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 68795, - "rejected_chunk_coverage": 2, - "coverage_checks": 40543 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 551044, - "accepted": 227637, - "rejected_no_draw": 0, - "rejected_not_ready": 4, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 323403, - "rejected_chunk_coverage": 0, - "coverage_checks": 227637 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 575694 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001161575317383, - "upload_total_bytes": 134290816, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2069.764847, - "worker_mesh_construction_total_ms": 254.713667, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.390745, - "compact_submissions": 120420, - "pool_gpu_allocated_bytes": 47806720, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.002567291259766, - "requested_duration_s": 60, - "sampled_frame_count": 38966, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 16.521254, - "auto": 0.390745, - "status": "measured", - "reduction_fraction": 0.9763489502673345, - "delta": -16.130509 - }, - "near_pool_geometry": { - "off": 51584448, - "auto": 47806720, - "status": "measured", - "delta": -3777728 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 184152192, - "auto": 134290816, - "status": "measured", - "delta": -49861376 - }, - "startup_worker_mesh_construction_ms": { - "off": 531.777998, - "auto": 254.713667, - "status": "measured", - "delta": -277.06433100000004 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 23797, - "duration_s": 60, - "fps": { - "min": 175.46096801757812, - "avg": 396.61138504766825, - "max": 1074.4139404296875, - "p1": 257.3740393066406, - "p5": 274.53150634765626, - "p50": 396.9031982421875, - "p95": 661.2046752929688, - "p99": 714.1902856445316 - }, - "frame_ms": { - "min": 0.9307399988174438, - "avg": 2.521359793743216, - "max": 5.699273109436035, - "p1": 1.4001870679855346, - "p5": 1.5123910188674927, - "p50": 2.5195059776306152, - "p95": 3.6425690650939946, - "p99": 3.8853954601287843 - }, - "max_frame_ms": 5.699273109436035, - "cpu_ms_avg": 2.521359793743216, - "gpu_ms": { - "shadow_avg": 0.154423818521644, - "opaque_avg": 0.6042536053205882, - "lod_terrain_avg": 0.156051211510881, - "lod_water_avg": 0.007354269866303185, - "lod_culling_avg": 0, - "total_avg": 2.0346358762937777, - "total": { - "min": 1.4758399724960327, - "avg": 2.0346358762937777, - "max": 2.76636004447937, - "p1": 1.5261183547973634, - "p5": 1.558703899383545, - "p50": 2.005239963531494, - "p95": 2.6905358314514163, - "p99": 2.721614456176758 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 1766.3523553389082, - "vertices_avg": 1617589.0599655418, - "chunks_rendered_avg": 302.0781190906417, - "worst_frame": { - "frame_index": 5602, - "frame_ms": 5.699273109436035, - "gpu_total_ms": 1.9434399604797363, - "gpu_lod_terrain_ms": 0.161080002784729, - "gpu_lod_water_ms": 0.0024800000246614218, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5849199891090393, - "lod_cpu_ms": 0.04036500000009369, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.03441399999999817, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 16969344, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 34, - "lod_rejected_count": 58, - "lod_coverage_count": 34, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 9 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 23797, - "cpu_frame_ms": { - "min": 0.021500999107956886, - "avg": 0.20519542415700823, - "max": 0.7278670072555542, - "p1": 0.024314599633216857, - "p5": 0.025949999690055847, - "p50": 0.10797200351953506, - "p95": 0.4669252038002014, - "p99": 0.5164854550361638 - }, - "gpu_frame_ms": { - "min": 0.14083999395370483, - "avg": 0.16340548156422066, - "max": 0.19900000095367432, - "p1": 0.14548000693321228, - "p5": 0.14796000719070435, - "p50": 0.16171999275684357, - "p95": 0.18304000794887543, - "p99": 0.190561597943306 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 3982.357878, - "avg_ms": 0.16734705542715467 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.045742, - "avg_ms": 4.3944278690591246e-5 - }, - "upload_prep": { - "total_ms": 23.481246, - "avg_ms": 0.0009867313526915158 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 627.3428289999999, - "avg_ms": 0.026362265369584397 - }, - "coverage": { - "total_ms": 479.610648, - "avg_ms": 0.020154248350632432 - }, - "eviction": { - "total_ms": 23.780808, - "avg_ms": 0.0009993195780980796 - }, - "manager_lock_wait": { - "total_ms": 12.254757999999999, - "avg_ms": 0.0005149707105937723 - }, - "manager_lock_hold": { - "total_ms": 2372.041763, - "avg_ms": 0.09967818477118966 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 16969344, - "max_bytes": 16969344, - "last_bytes": 16969344, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51119616, - "max_bytes": 51119616, - "last_bytes": 51119616, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 15989248, - "max_bytes": 15989248, - "last_bytes": 15989248, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 202117656, - "max_bytes": 202117656, - "last_bytes": 202117656, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 84078208, - "max_bytes": 84078208, - "last_bytes": 84078208, - "p50_bytes": 84078208, - "p95_bytes": 84078208, - "p99_bytes": 84078208 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 806386, - "rejected_total": 1382938, - "coverage_total": 806386, - "levels": [ - { - "candidates": 904286, - "accepted": 332307, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 571979, - "rejected_chunk_coverage": 0, - "coverage_checks": 332307 - }, - { - "candidates": 618722, - "accepted": 165730, - "rejected_no_draw": 0, - "rejected_not_ready": 214173, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 238819, - "rejected_chunk_coverage": 0, - "coverage_checks": 165730 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 666316, - "accepted": 308349, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 357967, - "rejected_chunk_coverage": 0, - "coverage_checks": 308349 - } - ] - }, - "pressure": { - "staging_pressure_total": 214173, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001801490783691, - "upload_total_bytes": 148710528, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3273.404621, - "worker_mesh_construction_total_ms": 513.315522, - "worker_far_expanded_mesh_construction_ms": 20.563808, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51119616, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00071334838867, - "requested_duration_s": 60, - "sampled_frame_count": 23797, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 25301, - "duration_s": 60, - "fps": { - "min": 213.1246337890625, - "avg": 421.67928368423435, - "max": 1089.044677734375, - "p1": 262.4532165527344, - "p5": 291.1694641113281, - "p50": 430.5098876953125, - "p95": 688.767333984375, - "p99": 752.63916015625 - }, - "frame_ms": { - "min": 0.9182359576225281, - "avg": 2.3714705433545293, - "max": 4.692090034484863, - "p1": 1.3286579847335815, - "p5": 1.451869010925293, - "p50": 2.322827100753784, - "p95": 3.4344258308410645, - "p99": 3.8102028369903564 - }, - "max_frame_ms": 4.692090034484863, - "cpu_ms_avg": 2.3714705433545293, - "gpu_ms": { - "shadow_avg": 0.15428008770405324, - "opaque_avg": 0.6053252567201682, - "lod_terrain_avg": 0.1562057389094021, - "lod_water_avg": 0.00865767519183177, - "lod_culling_avg": 0, - "total_avg": 2.031630992644399, - "total": { - "min": 1.476680040359497, - "avg": 2.031630992644399, - "max": 2.772599697113037, - "p1": 1.524280071258545, - "p5": 1.558000087738037, - "p50": 2.0017600059509277, - "p95": 2.6872000694274902, - "p99": 2.7239201068878174 - }, - "lod_compact_terrain": { - "min": 0.12188000231981277, - "avg": 0.1560424758112316, - "max": 0.19672000408172607, - "p1": 0.1281999945640564, - "p5": 0.1316400021314621, - "p50": 0.15639999508857727, - "p95": 0.17880000174045563, - "p99": 0.18828000128269196 - }, - "lod_compact_water": { - "min": 0.002199999988079071, - "avg": 0.008493858743610642, - "max": 0.053040001541376114, - "p1": 0.0024800000246614218, - "p5": 0.0026400000788271427, - "p50": 0.0032399999909102917, - "p95": 0.023959999904036522, - "p99": 0.04871999844908714 - } - }, - "draw_calls_avg": 1792.2180941464765, - "vertices_avg": 1616968.9824117625, - "chunks_rendered_avg": 302.0690881783329, - "worst_frame": { - "frame_index": 9269, - "frame_ms": 4.692090034484863, - "gpu_total_ms": 1.9571599960327148, - "gpu_lod_terrain_ms": 0.15860000252723694, - "gpu_lod_water_ms": 0.002959999954327941, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5702400207519531, - "lod_cpu_ms": 0.043873000000303364, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.037090000000034706, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 10140864, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 34, - "lod_rejected_count": 54, - "lod_coverage_count": 34, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 5 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 25301, - "cpu_frame_ms": { - "min": 0.02015800029039383, - "avg": 0.1999728801528056, - "max": 0.7344110012054443, - "p1": 0.022933000698685646, - "p5": 0.024555999785661697, - "p50": 0.2914069890975952, - "p95": 0.4559270143508911, - "p99": 0.5125240087509155 - }, - "gpu_frame_ms": { - "min": 0.1446399986743927, - "avg": 0.1648634141840005, - "max": 0.20043998956680298, - "p1": 0.14667999744415283, - "p5": 0.14911998808383942, - "p50": 0.1631999909877777, - "p95": 0.18435999751091003, - "p99": 0.19196000695228577 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 4146.662353999999, - "avg_ms": 0.16389321979368401 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.0660479999999999, - "avg_ms": 4.2134619185012446e-5 - }, - "upload_prep": { - "total_ms": 16.295102, - "avg_ms": 0.0006440497213548871 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 650.652955, - "avg_ms": 0.025716491640646617 - }, - "coverage": { - "total_ms": 503.97866300000004, - "avg_ms": 0.019919317932097547 - }, - "eviction": { - "total_ms": 22.264232999999997, - "avg_ms": 0.0008799744278882257 - }, - "manager_lock_wait": { - "total_ms": 12.759905999999999, - "avg_ms": 0.0005043241769100035 - }, - "manager_lock_hold": { - "total_ms": 4355.102357, - "avg_ms": 0.1721316294612861 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 10140864, - "max_bytes": 10140864, - "last_bytes": 10140864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51612160, - "max_bytes": 51612160, - "last_bytes": 51612160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 82605568, - "max_bytes": 82605568, - "last_bytes": 82605568, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 252102060, - "max_bytes": 252102060, - "last_bytes": 252102060, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 77249728, - "max_bytes": 77249728, - "last_bytes": 77249728, - "p50_bytes": 77249728, - "p95_bytes": 77249728, - "p99_bytes": 77249728 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 860154, - "rejected_total": 1366334, - "coverage_total": 860154, - "levels": [ - { - "candidates": 961438, - "accepted": 353319, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 608119, - "rejected_chunk_coverage": 0, - "coverage_checks": 353319 - }, - { - "candidates": 556622, - "accepted": 178827, - "rejected_no_draw": 0, - "rejected_not_ready": 126505, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 251290, - "rejected_chunk_coverage": 0, - "coverage_checks": 178827 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 708428, - "accepted": 328008, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 380420, - "rejected_chunk_coverage": 0, - "coverage_checks": 328008 - } - ] - }, - "pressure": { - "staging_pressure_total": 126505, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 767286 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002336502075195, - "upload_total_bytes": 163341760, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2442.520968, - "worker_mesh_construction_total_ms": 347.034829, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.691216, - "compact_submissions": 111270, - "pool_gpu_allocated_bytes": 51612160, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000667572021484, - "requested_duration_s": 60, - "sampled_frame_count": 25301, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 20.563808, - "auto": 1.691216, - "status": "measured", - "reduction_fraction": 0.9177576448875616, - "delta": -18.872592 - }, - "near_pool_geometry": { - "off": 51119616, - "auto": 51612160, - "status": "measured", - "delta": 492544 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 148710528, - "auto": 163341760, - "status": "measured", - "delta": 14631232 - }, - "startup_worker_mesh_construction_ms": { - "off": 513.315522, - "auto": 347.034829, - "status": "measured", - "delta": -166.28069299999999 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 252.69599719471123, - "gpu_memory_mb_max": 254.720947265625, - "frames": 34549, - "duration_s": 60, - "fps": { - "min": 28.714000701904297, - "avg": 575.7905407823773, - "max": 13248.19140625, - "p1": 243.06125610351563, - "p5": 272.5340576171875, - "p50": 607.9758911132812, - "p95": 7783.578906249999, - "p99": 10357.207773437494 - }, - "frame_ms": { - "min": 0.07548199594020844, - "avg": 1.7367426679868898, - "max": 34.82621383666992, - "p1": 0.09655111640691758, - "p5": 0.12847560048103332, - "p50": 1.6448020935058594, - "p95": 3.6692658901214603, - "p99": 4.114189376831052 - }, - "max_frame_ms": 34.82621383666992, - "cpu_ms_avg": 1.7367426679868898, - "gpu_ms": { - "shadow_avg": 0.06716080148709715, - "opaque_avg": 0.4554422645945774, - "lod_terrain_avg": 0.0827393418180604, - "lod_water_avg": 0.0096837268972875, - "lod_culling_avg": 0, - "total_avg": 1.2544880651655466, - "total": { - "min": 0, - "avg": 1.2544880651655466, - "max": 2.8380000591278076, - "p1": 0, - "p5": 0, - "p50": 1.605639934539795, - "p95": 2.727400064468384, - "p99": 2.762822532653808 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 962.0566441865177, - "vertices_avg": 1782318.6759095776, - "chunks_rendered_avg": 298.3859446004226, - "worst_frame": { - "frame_index": 1441, - "frame_ms": 34.82621383666992, - "gpu_total_ms": 2.305760145187378, - "gpu_lod_terrain_ms": 0.15379999577999115, - "gpu_lod_water_ms": 0.004519999958574772, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8870000243186951, - "lod_cpu_ms": 0.004737999999861131, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.001072000000000628, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 11704512, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 6 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 34549, - "cpu_frame_ms": { - "min": 0.001062000053934753, - "avg": 0.30011163468628077, - "max": 25.530231475830078, - "p1": 0.0022844799328595403, - "p5": 0.00291600008495152, - "p50": 0.23548200726509094, - "p95": 0.8054416060447687, - "p99": 1.1574027013778676 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.09242306879915588, - "max": 0.2049199938774109, - "p1": 0, - "p5": 0, - "p50": 0.1486399918794632, - "p95": 0.19275999069213867, - "p99": 0.19895999133586884 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8039.768316, - "avg_ms": 0.2327062524530377 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 1.8289300000000002, - "avg_ms": 5.293727748994182e-5 - }, - "state_transition": { - "total_ms": 8.901504000000001, - "avg_ms": 0.00025764867289936033 - }, - "upload_prep": { - "total_ms": 11.189437, - "avg_ms": 0.0003238715158181134 - }, - "upload_submission": { - "total_ms": 313.79030499999993, - "avg_ms": 0.009082471417407159 - }, - "visibility": { - "total_ms": 1300.0616029999999, - "avg_ms": 0.03762950021708298 - }, - "coverage": { - "total_ms": 1169.702104, - "avg_ms": 0.03385632301947958 - }, - "eviction": { - "total_ms": 80.289141, - "avg_ms": 0.002323920837071985 - }, - "manager_lock_wait": { - "total_ms": 36.47475300000001, - "avg_ms": 0.0010557397609192742 - }, - "manager_lock_hold": { - "total_ms": 3056.088567, - "avg_ms": 0.08845664323135256 - } - }, - "workers": { - "generation_total_ms": 17531.309690000002, - "mesh_construction_total_ms": 2252.186781, - "far_expanded_mesh_construction_total_ms": 29.686592, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 1003093632, - "upload_avg_bytes": 29033.94112709485, - "far_expanded_upload_total_bytes": 8046720, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 4726207.679527627, - "max_bytes": 41062272, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 653370.3259718082, - "max_bytes": 54951168, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 653370.3259718082, - "max_bytes": 54951168, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 103164.91383252772, - "max_bytes": 2787072, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 23877158.540044576, - "max_bytes": 66604224, - "last_bytes": 13603008, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 43231705.459955424, - "max_bytes": 62896768, - "last_bytes": 53505856, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 16619981.954441518, - "max_bytes": 18743296, - "last_bytes": 18743296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 167576474.12567657, - "max_bytes": 235366305, - "last_bytes": 160280316, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 71938236.59336016, - "max_bytes": 108171136, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 108171136, - "p99_bytes": 108171136 - }, - "logical_vram_bytes": { - "avg_bytes": 84382216.28041333, - "max_bytes": 129924352, - "last_bytes": 85852160, - "p50_bytes": 85852160, - "p95_bytes": 88399232, - "p99_bytes": 95787584 - } - }, - "visibility": { - "visible_total": 1036377, - "rejected_total": 987470, - "coverage_total": 1036421, - "levels": [ - { - "candidates": 449499, - "accepted": 172076, - "rejected_no_draw": 0, - "rejected_not_ready": 516, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 276863, - "rejected_chunk_coverage": 44, - "coverage_checks": 172120 - }, - { - "candidates": 94527, - "accepted": 24773, - "rejected_no_draw": 0, - "rejected_not_ready": 39254, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 30500, - "rejected_chunk_coverage": 0, - "coverage_checks": 24773 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1479821, - "accepted": 839528, - "rejected_no_draw": 0, - "rejected_not_ready": 20, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 640273, - "rejected_chunk_coverage": 0, - "coverage_checks": 839528 - } - ] - }, - "pressure": { - "staging_pressure_total": 93499, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00274658203125, - "upload_total_bytes": 185120256, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3039.224186, - "worker_mesh_construction_total_ms": 454.648539, - "worker_far_expanded_mesh_construction_ms": 12.217116, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51395520, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00266647338867, - "requested_duration_s": 60, - "sampled_frame_count": 34549, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 35575, - "duration_s": 60, - "fps": { - "min": 26.52682876586914, - "avg": 592.886389901715, - "max": 13408.4208984375, - "p1": 243.0714587402344, - "p5": 273.95556640625, - "p50": 619.6807861328125, - "p95": 8077.087499999994, - "p99": 10807.142285156253 - }, - "frame_ms": { - "min": 0.07457999885082245, - "avg": 1.6866637808396543, - "max": 37.69768524169922, - "p1": 0.09253139853477478, - "p5": 0.12380700185894966, - "p50": 1.6137340068817139, - "p95": 3.6502270698547354, - "p99": 4.1140166282653805 - }, - "max_frame_ms": 37.69768524169922, - "cpu_ms_avg": 1.6866637808396543, - "gpu_ms": { - "shadow_avg": 0.06546274070201923, - "opaque_avg": 0.44541086144792474, - "lod_terrain_avg": 0.08112583783233325, - "lod_water_avg": 0.00973485650035251, - "lod_culling_avg": 0, - "total_avg": 1.224849416534413, - "total": { - "min": 0, - "avg": 1.224849416534413, - "max": 2.7942798137664795, - "p1": 0, - "p5": 0, - "p50": 1.5824799537658691, - "p95": 2.727759909629822, - "p99": 2.7565598487854004 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.0810357914478613, - "max": 0.16524000465869904, - "p1": 0, - "p5": 0, - "p50": 0.11964000016450882, - "p95": 0.15839999914169312, - "p99": 0.16096000373363495 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.009644858486019729, - "max": 0.05640000104904175, - "p1": 0, - "p5": 0, - "p50": 0.002520000096410513, - "p95": 0.051600001752376556, - "p99": 0.05195999890565872 - } - }, - "draw_calls_avg": 977.4407589599438, - "vertices_avg": 1776630.673281799, - "chunks_rendered_avg": 298.2763457484188, - "worst_frame": { - "frame_index": 1506, - "frame_ms": 37.69768524169922, - "gpu_total_ms": 2.294400215148926, - "gpu_lod_terrain_ms": 0.15372000634670258, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.885919988155365, - "lod_cpu_ms": 0.004228000000011889, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.0015530000000003596, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 22665984, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 12 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 35575, - "cpu_frame_ms": { - "min": 0.0007220000261440873, - "avg": 0.2743054770208268, - "max": 6.96904993057251, - "p1": 0.0013930000131949782, - "p5": 0.0016740000573918223, - "p50": 0.14977100491523743, - "p95": 0.7290372908115383, - "p99": 0.9569273316860202 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.09086069420199953, - "max": 0.2067599892616272, - "p1": 0, - "p5": 0, - "p50": 0.15060000121593475, - "p95": 0.19404000043869019, - "p99": 0.20040000975131989 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 8452.436845, - "avg_ms": 0.23759485158116656 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.09829399999999999, - "avg_ms": 2.7630077301475755e-6 - }, - "state_transition": { - "total_ms": 1.7323709999999999, - "avg_ms": 4.869630358397751e-5 - }, - "upload_prep": { - "total_ms": 6.005358, - "avg_ms": 0.00016880837666900913 - }, - "upload_submission": { - "total_ms": 9.031519000000003, - "avg_ms": 0.00025387263527758264 - }, - "visibility": { - "total_ms": 1075.9610199999997, - "avg_ms": 0.03024486352775825 - }, - "coverage": { - "total_ms": 992.3198, - "avg_ms": 0.027893739985945187 - }, - "eviction": { - "total_ms": 31.620691000000004, - "avg_ms": 0.00088884584680253 - }, - "manager_lock_wait": { - "total_ms": 14.289860000000001, - "avg_ms": 0.00040168264230498946 - }, - "manager_lock_hold": { - "total_ms": 4127.1187740000005, - "avg_ms": 0.11601177158116656 - } - }, - "workers": { - "generation_total_ms": 824.754598, - "mesh_construction_total_ms": 74.540435, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.38233800000000007 - }, - "memory_bytes": { - "upload_total_bytes": 52815536, - "upload_avg_bytes": 1484.6250456781447, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 202160, - "pending_cpu_upload_bytes": { - "avg_bytes": 1494996.291103303, - "max_bytes": 22665984, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 89382.89472944483, - "max_bytes": 39924288, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 89382.89472944483, - "max_bytes": 39924288, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 3977.095825720309, - "max_bytes": 1077696, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 4359729.575488404, - "max_bytes": 40086016, - "last_bytes": 363888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 129857998.4245116, - "max_bytes": 133853840, - "last_bytes": 133853840, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 341650.846493324, - "max_bytes": 363888, - "last_bytes": 363888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66767213.153506674, - "max_bytes": 66947136, - "last_bytes": 66744976, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 206373245.4624596, - "max_bytes": 265655441, - "last_bytes": 202382598, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 68607837.38692902, - "max_bytes": 89774848, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 89774848, - "p99_bytes": 89774848 - }, - "logical_vram_bytes": { - "avg_bytes": 201415974.89472944, - "max_bytes": 241250880, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 202360512 - } - }, - "visibility": { - "visible_total": 729949, - "rejected_total": 516856, - "coverage_total": 729950, - "levels": [ - { - "candidates": 68274, - "accepted": 37742, - "rejected_no_draw": 0, - "rejected_not_ready": 33, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 30498, - "rejected_chunk_coverage": 1, - "coverage_checks": 37743 - }, - { - "candidates": 43706, - "accepted": 7534, - "rejected_no_draw": 0, - "rejected_not_ready": 18100, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 18072, - "rejected_chunk_coverage": 0, - "coverage_checks": 7534 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 1134825, - "accepted": 684673, - "rejected_no_draw": 0, - "rejected_not_ready": 5, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 450147, - "rejected_chunk_coverage": 0, - "coverage_checks": 684673 - } - ] - }, - "pressure": { - "staging_pressure_total": 28140, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1474226 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000977516174316, - "upload_total_bytes": 133864768, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2634.586645, - "worker_mesh_construction_total_ms": 289.406362, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.411203, - "compact_submissions": 104880, - "pool_gpu_allocated_bytes": 40086016, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.002723693847656, - "requested_duration_s": 60, - "sampled_frame_count": 35575, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 12.217116, - "auto": 0.411203, - "status": "measured", - "reduction_fraction": 0.9663420565049885, - "delta": -11.805913 - }, - "near_pool_geometry": { - "off": 51395520, - "auto": 40086016, - "status": "measured", - "delta": -11309504 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 185120256, - "auto": 133864768, - "status": "measured", - "delta": -51255488 - }, - "startup_worker_mesh_construction_ms": { - "off": 454.648539, - "auto": 289.406362, - "status": "measured", - "delta": -165.24217700000003 - } - } - } - }, - "high": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.845947265625, - "gpu_memory_mb_max": 689.845947265625, - "frames": 12964, - "duration_s": 60, - "fps": { - "min": 152.35963439941406, - "avg": 216.0616443633581, - "max": 331.7027893066406, - "p1": 179.48295944213868, - "p5": 185.9127372741699, - "p50": 216.44506072998047, - "p95": 259.85692596435536, - "p99": 270.8066766357421 - }, - "frame_ms": { - "min": 3.014747142791748, - "avg": 4.628308753951101, - "max": 6.563417911529541, - "p1": 3.6926712250709537, - "p5": 3.848271298408508, - "p50": 4.620110034942627, - "p95": 5.378867673873901, - "p99": 5.5715593385696405 - }, - "max_frame_ms": 6.563417911529541, - "cpu_ms_avg": 4.628308753951101, - "gpu_ms": { - "shadow_avg": 0.17150219147797904, - "opaque_avg": 1.656388395557577, - "lod_terrain_avg": 0.32858790811271954, - "lod_water_avg": 0.004540592400251236, - "lod_culling_avg": 0, - "total_avg": 3.7238489439442586, - "total": { - "min": 3.653400182723999, - "avg": 3.7238489439442586, - "max": 3.785599708557129, - "p1": 3.670625171661377, - "p5": 3.68168613910675, - "p50": 3.726960062980652, - "p95": 3.7570402264595035, - "p99": 3.7658400535583496 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 2193, - "vertices_avg": 1926678, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 4441, - "frame_ms": 6.563417911529541, - "gpu_total_ms": 3.7381198406219482, - "gpu_lod_terrain_ms": 0.3370400071144104, - "gpu_lod_water_ms": 0.004399999976158142, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.665120005607605, - "lod_cpu_ms": 0.06511199999994233, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.057938999999976204, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 84792576, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 43, - "lod_rejected_count": 86, - "lod_coverage_count": 43, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 45 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 12964, - "cpu_frame_ms": { - "min": 0.048562001436948776, - "avg": 0.27464682015480313, - "max": 0.8471519947052002, - "p1": 0.051006000488996506, - "p5": 0.052268148772418505, - "p50": 0.2920540049672127, - "p95": 0.5927991777658461, - "p99": 0.643724815249443 - }, - "gpu_frame_ms": { - "min": 0.2999200224876404, - "avg": 0.3331285006252015, - "max": 0.3700000047683716, - "p1": 0.3023200035095215, - "p5": 0.30532002449035645, - "p50": 0.33302000164985657, - "p95": 0.3627600073814392, - "p99": 0.365119993686676 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 2609.15706, - "avg_ms": 0.2012617294045048 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.874541, - "avg_ms": 6.745919469299598e-5 - }, - "upload_prep": { - "total_ms": 42.730511, - "avg_ms": 0.003296090018512805 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 662.2642159999999, - "avg_ms": 0.051084867016352974 - }, - "coverage": { - "total_ms": 568.9277719999999, - "avg_ms": 0.04388520302375809 - }, - "eviction": { - "total_ms": 16.243842, - "avg_ms": 0.0012529961431656897 - }, - "manager_lock_wait": { - "total_ms": 6.509898999999999, - "avg_ms": 0.0005021520364085158 - }, - "manager_lock_hold": { - "total_ms": 1687.670132, - "avg_ms": 0.13018128139463128 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 84792576, - "max_bytes": 84792576, - "last_bytes": 84792576, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 76199232, - "max_bytes": 76199232, - "last_bytes": 76199232, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 24464064, - "max_bytes": 24464064, - "last_bytes": 24464064, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 398793551, - "max_bytes": 398793551, - "last_bytes": 398793551, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 185455872, - "max_bytes": 185455872, - "last_bytes": 185455872, - "p50_bytes": 185455872, - "p95_bytes": 185455872, - "p99_bytes": 185455872 - }, - "logical_vram_bytes": { - "avg_bytes": 105906176, - "max_bytes": 105906176, - "last_bytes": 105906176, - "p50_bytes": 105906176, - "p95_bytes": 105906176, - "p99_bytes": 105906176 - } - }, - "visibility": { - "visible_total": 557452, - "rejected_total": 1114904, - "coverage_total": 557452, - "levels": [ - { - "candidates": 285208, - "accepted": 168532, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 116676, - "rejected_chunk_coverage": 0, - "coverage_checks": 168532 - }, - { - "candidates": 803768, - "accepted": 90748, - "rejected_no_draw": 0, - "rejected_not_ready": 583380, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 129640, - "rejected_chunk_coverage": 0, - "coverage_checks": 90748 - }, - { - "candidates": 220388, - "accepted": 103712, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 116676, - "rejected_chunk_coverage": 0, - "coverage_checks": 103712 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 362992, - "accepted": 194460, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 168532, - "rejected_chunk_coverage": 0, - "coverage_checks": 194460 - } - ] - }, - "pressure": { - "staging_pressure_total": 583380, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002833366394043, - "upload_total_bytes": 266838912, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5688.436026, - "worker_mesh_construction_total_ms": 730.127717, - "worker_far_expanded_mesh_construction_ms": 24.149307, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 76199232, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00136947631836, - "requested_duration_s": 60, - "sampled_frame_count": 12964, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 13220, - "duration_s": 60, - "fps": { - "min": 125.0868148803711, - "avg": 220.3230879036607, - "max": 357.15777587890625, - "p1": 180.81555847167968, - "p5": 189.53513717651367, - "p50": 220.7610321044922, - "p95": 264.72271728515625, - "p99": 277.1500390625 - }, - "frame_ms": { - "min": 2.7998831272125244, - "avg": 4.53878896449229, - "max": 7.994447708129883, - "p1": 3.608153803348541, - "p5": 3.7775374293327335, - "p50": 4.529784917831421, - "p95": 5.276066279411316, - "p99": 5.530497460365295 - }, - "max_frame_ms": 7.994447708129883, - "cpu_ms_avg": 4.53878896449229, - "gpu_ms": { - "shadow_avg": 0.17123455900625634, - "opaque_avg": 1.6617527709064974, - "lod_terrain_avg": 0.3294877034430244, - "lod_water_avg": 0.004754944022360248, - "lod_culling_avg": 0, - "total_avg": 3.7284749838299542, - "total": { - "min": 3.649480104446411, - "avg": 3.7284749838299542, - "max": 3.791719913482666, - "p1": 3.6752552604675293, - "p5": 3.686840057373047, - "p50": 3.730600118637085, - "p95": 3.760482144355774, - "p99": 3.769232337474823 - }, - "lod_compact_terrain": { - "min": 0.295960009098053, - "avg": 0.32932302560685445, - "max": 0.3677600026130676, - "p1": 0.29844000935554504, - "p5": 0.3011600077152252, - "p50": 0.32919999957084656, - "p95": 0.359279990196228, - "p99": 0.36131998896598816 - }, - "lod_compact_water": { - "min": 0.003640000009909272, - "avg": 0.00459113767120139, - "max": 0.010400000028312206, - "p1": 0.003920000046491623, - "p5": 0.004040000028908253, - "p50": 0.004559999797493219, - "p95": 0.005200000014156103, - "p99": 0.0055599999614059925 - } - }, - "draw_calls_avg": 2223, - "vertices_avg": 1926666, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 1745, - "frame_ms": 7.994447708129883, - "gpu_total_ms": 3.72052001953125, - "gpu_lod_terrain_ms": 0.35339999198913574, - "gpu_lod_water_ms": 0.004519999958574772, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6861599683761597, - "lod_cpu_ms": 0.07427000000006956, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.06326899999999114, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 44525376, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 35, - "lod_rejected_count": 58, - "lod_coverage_count": 35, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 13220, - "cpu_frame_ms": { - "min": 0.04106700047850609, - "avg": 0.2559182526867797, - "max": 2.929025888442993, - "p1": 0.04335371047258377, - "p5": 0.04483354911208153, - "p50": 0.35259251296520233, - "p95": 0.5461850792169569, - "p99": 0.5957804894447326 - }, - "gpu_frame_ms": { - "min": 0.3004800081253052, - "avg": 0.3342426477374901, - "max": 0.3722800016403198, - "p1": 0.30332761943340303, - "p5": 0.30612000823020935, - "p50": 0.3341200053691864, - "p95": 0.36423999071121216, - "p99": 0.36632001399993896 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 2534.663638, - "avg_ms": 0.19172947337367624 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 1.0517109999999998, - "avg_ms": 7.955453857791224e-5 - }, - "upload_prep": { - "total_ms": 31.463210000000004, - "avg_ms": 0.0023799704992435704 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 609.7446059999999, - "avg_ms": 0.046122890015128586 - }, - "coverage": { - "total_ms": 525.314921, - "avg_ms": 0.039736378290468985 - }, - "eviction": { - "total_ms": 15.063522999999998, - "avg_ms": 0.0011394495461422086 - }, - "manager_lock_wait": { - "total_ms": 6.800823, - "avg_ms": 0.0005144344175491679 - }, - "manager_lock_hold": { - "total_ms": 2601.8017280000004, - "avg_ms": 0.19680799757942513 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 44525376, - "max_bytes": 44525376, - "last_bytes": 44525376, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 69447616, - "max_bytes": 69447616, - "last_bytes": 69447616, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 98324544, - "max_bytes": 98324544, - "last_bytes": 98324544, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 388041731, - "max_bytes": 388041731, - "last_bytes": 388041731, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 145188672, - "max_bytes": 145188672, - "last_bytes": 145188672, - "p50_bytes": 145188672, - "p95_bytes": 145188672, - "p99_bytes": 145188672 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 462700, - "rejected_total": 766760, - "coverage_total": 462700, - "levels": [ - { - "candidates": 105760, - "accepted": 52880, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 52880, - "rejected_chunk_coverage": 0, - "coverage_checks": 52880 - }, - { - "candidates": 542020, - "accepted": 105760, - "rejected_no_draw": 0, - "rejected_not_ready": 304060, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 132200, - "rejected_chunk_coverage": 0, - "coverage_checks": 105760 - }, - { - "candidates": 211520, - "accepted": 105760, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 105760, - "rejected_chunk_coverage": 0, - "coverage_checks": 105760 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 370160, - "accepted": 198300, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 171860, - "rejected_chunk_coverage": 0, - "coverage_checks": 198300 - } - ] - }, - "pressure": { - "staging_pressure_total": 304060, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 456510 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001523971557617, - "upload_total_bytes": 332063104, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5623.422823, - "worker_mesh_construction_total_ms": 668.850835, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.558509, - "compact_submissions": 59910, - "pool_gpu_allocated_bytes": 69447616, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.0025749206543, - "requested_duration_s": 60, - "sampled_frame_count": 13220, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 24.149307, - "auto": 2.558509, - "status": "measured", - "reduction_fraction": 0.8940545581701371, - "delta": -21.590798 - }, - "near_pool_geometry": { - "off": 76199232, - "auto": 69447616, - "status": "measured", - "delta": -6751616 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 266838912, - "auto": 332063104, - "status": "measured", - "delta": 65224192 - }, - "startup_worker_mesh_construction_ms": { - "off": 730.127717, - "auto": 668.850835, - "status": "measured", - "delta": -61.276882 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 690.3530773444085, - "gpu_memory_mb_max": 692.845947265625, - "frames": 29067, - "duration_s": 60, - "fps": { - "min": 28.568946838378906, - "avg": 484.42977896008625, - "max": 11793.84375, - "p1": 108.06817001342773, - "p5": 143.09896240234377, - "p50": 1153.9229736328125, - "p95": 7823.930419921873, - "p99": 9918.37263671875 - }, - "frame_ms": { - "min": 0.08478999882936478, - "avg": 2.0642826750797107, - "max": 35.0030403137207, - "p1": 0.10082300201058388, - "p5": 0.12781300097703935, - "p50": 0.8666089773178101, - "p95": 6.988170480728147, - "p99": 9.253419399261475 - }, - "max_frame_ms": 35.0030403137207, - "cpu_ms_avg": 2.0642826750797107, - "gpu_ms": { - "shadow_avg": 0.06564489899568893, - "opaque_avg": 0.5199907510774474, - "lod_terrain_avg": 0.080141796526008, - "lod_water_avg": 0.004009798048391247, - "lod_culling_avg": 0, - "total_avg": 1.2223744058640178, - "total": { - "min": 0, - "avg": 1.2223744058640178, - "max": 6.9693603515625, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 5.675151872634883, - "p99": 6.7736452770233155 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 652.0901021777273, - "vertices_avg": 2452845.152028073, - "chunks_rendered_avg": 419.29211820965355, - "worst_frame": { - "frame_index": 22405, - "frame_ms": 35.0030403137207, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.16097299999955794, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.1531079999999747, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 54725376, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 6, - "lod_rejected_count": 82, - "lod_coverage_count": 6, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 28 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 29067, - "cpu_frame_ms": { - "min": 0.0026050000451505184, - "avg": 0.41275628830122674, - "max": 26.271995544433594, - "p1": 0.003737000050023198, - "p5": 0.004238000139594078, - "p50": 0.3683120012283325, - "p95": 1.0341230154037473, - "p99": 1.865414407253266 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.08415159457133937, - "max": 0.6054400205612183, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44152000546455383, - "p99": 0.5475335800647736 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 9856.074186, - "avg_ms": 0.339081232531737 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 7.021973, - "avg_ms": 0.0002415788695083772 - }, - "state_transition": { - "total_ms": 7.775021, - "avg_ms": 0.0002674861870850105 - }, - "upload_prep": { - "total_ms": 68.49887600000001, - "avg_ms": 0.0023565856813568655 - }, - "upload_submission": { - "total_ms": 1035.159533, - "avg_ms": 0.03561287828121237 - }, - "visibility": { - "total_ms": 445.09900100000004, - "avg_ms": 0.01531286341899749 - }, - "coverage": { - "total_ms": 395.62143100000003, - "avg_ms": 0.013610672962466028 - }, - "eviction": { - "total_ms": 96.10720599999999, - "avg_ms": 0.0033064026559328445 - }, - "manager_lock_wait": { - "total_ms": 39.307753000000005, - "avg_ms": 0.0013523154436302339 - }, - "manager_lock_hold": { - "total_ms": 1331.573552, - "avg_ms": 0.04581049134757629 - } - }, - "workers": { - "generation_total_ms": 19654.610580999997, - "mesh_construction_total_ms": 2699.5955639999997, - "far_expanded_mesh_construction_total_ms": 15.243837, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 2136888384, - "upload_avg_bytes": 73515.95912890907, - "far_expanded_upload_total_bytes": 2099904, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 54507208.018990606, - "max_bytes": 86365440, - "last_bytes": 12014016, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 945847.9215605325, - "max_bytes": 19249920, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 945847.9215605325, - "max_bytes": 19249920, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 110971.66683868304, - "max_bytes": 5193984, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 63855524.75260605, - "max_bytes": 91519488, - "last_bytes": 72289152, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 36807771.24739395, - "max_bytes": 44394624, - "last_bytes": 28374144, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5774644.429490487, - "max_bytes": 8388608, - "last_bytes": 8388608, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 339692721.7501978, - "max_bytes": 400327246, - "last_bytes": 277830481, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 155281475.68582928, - "max_bytes": 187028736, - "last_bytes": 112677312, - "p50_bytes": 160588992, - "p95_bytes": 178349952, - "p99_bytes": 187028736 - }, - "logical_vram_bytes": { - "avg_bytes": 107383788.35105102, - "max_bytes": 127515392, - "last_bytes": 109051904, - "p50_bytes": 105906176, - "p95_bytes": 115122944, - "p99_bytes": 118585728 - } - }, - "visibility": { - "visible_total": 226947, - "rejected_total": 573259, - "coverage_total": 227001, - "levels": [ - { - "candidates": 91539, - "accepted": 37612, - "rejected_no_draw": 0, - "rejected_not_ready": 1172, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 52706, - "rejected_chunk_coverage": 49, - "coverage_checks": 37661 - }, - { - "candidates": 355845, - "accepted": 50009, - "rejected_no_draw": 0, - "rejected_not_ready": 217544, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 16, - "rejected_range": 0, - "rejected_frustum": 88273, - "rejected_chunk_coverage": 3, - "coverage_checks": 50012 - }, - { - "candidates": 106442, - "accepted": 30870, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 6269, - "rejected_range": 0, - "rejected_frustum": 69301, - "rejected_chunk_coverage": 2, - "coverage_checks": 30872 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 246380, - "accepted": 108456, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 137924, - "rejected_chunk_coverage": 0, - "coverage_checks": 108456 - } - ] - }, - "pressure": { - "staging_pressure_total": 814567, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003457069396973, - "upload_total_bytes": 239151360, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5756.304651, - "worker_mesh_construction_total_ms": 765.122769, - "worker_far_expanded_mesh_construction_ms": 14.239808, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 74874048, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.0026969909668, - "requested_duration_s": 60, - "sampled_frame_count": 29067, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 31045, - "duration_s": 60, - "fps": { - "min": 38.130882263183594, - "avg": 517.4081352129125, - "max": 11923.4990234375, - "p1": 110.70329315185546, - "p5": 143.10704650878907, - "p50": 1257.6337890625, - "p95": 8218.480273437497, - "p99": 10258.7877734375 - }, - "frame_ms": { - "min": 0.08386799693107605, - "avg": 1.9327102377091576, - "max": 26.225460052490234, - "p1": 0.0974773982167244, - "p5": 0.12167700231075287, - "p50": 0.7951440215110779, - "p95": 6.987775897979736, - "p99": 9.033154945373536 - }, - "max_frame_ms": 26.225460052490234, - "cpu_ms_avg": 1.9327102377091576, - "gpu_ms": { - "shadow_avg": 0.06186360279456642, - "opaque_avg": 0.49333068130672564, - "lod_terrain_avg": 0.07465216035398621, - "lod_water_avg": 0.004686510553716999, - "lod_culling_avg": 0, - "total_avg": 1.1569442147927538, - "total": { - "min": 0, - "avg": 1.1569442147927538, - "max": 7.012320518493652, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 5.651840019226073, - "p99": 6.74139825820923 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.07461082810224937, - "max": 0.5832399725914001, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.38415200710296626, - "p99": 0.5424799919128418 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.004645001774233505, - "max": 0.09019999951124191, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.038951998949050874, - "p99": 0.07310240030288702 - } - }, - "draw_calls_avg": 621.415686906104, - "vertices_avg": 2464466.607762925, - "chunks_rendered_avg": 420.2832984377516, - "worst_frame": { - "frame_index": 10620, - "frame_ms": 26.225460052490234, - "gpu_total_ms": 5.985640048980713, - "gpu_lod_terrain_ms": 0.0960799977183342, - "gpu_lod_water_ms": 0.004600000102072954, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 2.915519952774048, - "lod_cpu_ms": 0.6524870000004057, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.5068120000000818, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 11293440, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 9, - "lod_rejected_count": 63, - "lod_coverage_count": 9, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 6 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 31045, - "cpu_frame_ms": { - "min": 0.001744000008329749, - "avg": 0.35352289251313285, - "max": 20.67719841003418, - "p1": 0.002463999902829528, - "p5": 0.0028959999326616526, - "p50": 0.3257319927215576, - "p95": 0.9670219779014586, - "p99": 1.328999209403992 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.07933867090584326, - "max": 0.5879600048065186, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44076001048088076, - "p99": 0.5489423990249634 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 9577.417245999999, - "avg_ms": 0.3085011192140441 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 3.472569, - "avg_ms": 0.00011185598325012079 - }, - "state_transition": { - "total_ms": 8.089583, - "avg_ms": 0.00026057603478821066 - }, - "upload_prep": { - "total_ms": 23.461803, - "avg_ms": 0.0007557353196972137 - }, - "upload_submission": { - "total_ms": 291.486251, - "avg_ms": 0.009389152874859075 - }, - "visibility": { - "total_ms": 420.39509799999996, - "avg_ms": 0.013541475213399903 - }, - "coverage": { - "total_ms": 374.630807, - "avg_ms": 0.01206734762441617 - }, - "eviction": { - "total_ms": 93.36743799999999, - "avg_ms": 0.0030074871315831854 - }, - "manager_lock_wait": { - "total_ms": 57.033649999999994, - "avg_ms": 0.0018371283620550812 - }, - "manager_lock_hold": { - "total_ms": 1960.1845749999998, - "avg_ms": 0.0631401054920277 - } - }, - "workers": { - "generation_total_ms": 21901.809895, - "mesh_construction_total_ms": 2751.017459, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.612541 - }, - "memory_bytes": { - "upload_total_bytes": 1040721984, - "upload_avg_bytes": 33523.01446287647, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 207936, - "pending_cpu_upload_bytes": { - "avg_bytes": 9705013.096601708, - "max_bytes": 41645952, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 893202.8876791754, - "max_bytes": 13796544, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 893202.8876791754, - "max_bytes": 13796544, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 226281.70668384602, - "max_bytes": 3847296, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 53129097.02741182, - "max_bytes": 67651824, - "last_bytes": 33253888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 114643062.97258818, - "max_bytes": 135364800, - "last_bytes": 134518272, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 267209.35107102594, - "max_bytes": 369664, - "last_bytes": 369664, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66841654.64892898, - "max_bytes": 66947136, - "last_bytes": 66739200, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 319466411.5802867, - "max_bytes": 377504319, - "last_bytes": 291622862, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 110594590.80328555, - "max_bytes": 142309248, - "last_bytes": 100663296, - "p50_bytes": 104806272, - "p95_bytes": 132402048, - "p99_bytes": 142309248 - }, - "logical_vram_bytes": { - "avg_bytes": 235774226.8876792, - "max_bytes": 248677568, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 238488704, - "p99_bytes": 240571520 - } - }, - "visibility": { - "visible_total": 226575, - "rejected_total": 437817, - "coverage_total": 226674, - "levels": [ - { - "candidates": 76239, - "accepted": 20667, - "rejected_no_draw": 0, - "rejected_not_ready": 1729, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 53751, - "rejected_chunk_coverage": 92, - "coverage_checks": 20759 - }, - { - "candidates": 152332, - "accepted": 43615, - "rejected_no_draw": 0, - "rejected_not_ready": 51805, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 56908, - "rejected_chunk_coverage": 4, - "coverage_checks": 43619 - }, - { - "candidates": 90334, - "accepted": 27507, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 908, - "rejected_range": 0, - "rejected_frustum": 61916, - "rejected_chunk_coverage": 3, - "coverage_checks": 27510 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 345487, - "accepted": 134786, - "rejected_no_draw": 0, - "rejected_not_ready": 20, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 210681, - "rejected_chunk_coverage": 0, - "coverage_checks": 134786 - } - ] - }, - "pressure": { - "staging_pressure_total": 157739, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 340552 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003334045410156, - "upload_total_bytes": 302082112, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5451.201889, - "worker_mesh_construction_total_ms": 666.605237, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.3632, - "compact_submissions": 70980, - "pool_gpu_allocated_bytes": 60885568, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.000946044921875, - "requested_duration_s": 60, - "sampled_frame_count": 31045, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 14.239808, - "auto": 0.3632, - "status": "measured", - "reduction_fraction": 0.9744940381218623, - "delta": -13.876608 - }, - "near_pool_geometry": { - "off": 74874048, - "auto": 60885568, - "status": "measured", - "delta": -13988480 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 239151360, - "auto": 302082112, - "status": "measured", - "delta": 62930752 - }, - "startup_worker_mesh_construction_ms": { - "off": 765.122769, - "auto": 666.605237, - "status": "measured", - "delta": -98.51753199999996 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.845947265625, - "gpu_memory_mb_max": 689.845947265625, - "frames": 15495, - "duration_s": 60, - "fps": { - "min": 123.66195678710938, - "avg": 258.24569985065006, - "max": 614.8598022460938, - "p1": 171.36016052246092, - "p5": 187.8769989013672, - "p50": 259.83514404296875, - "p95": 401.47125549316405, - "p99": 455.9086199951172 - }, - "frame_ms": { - "min": 1.6263870000839233, - "avg": 3.8722813219284005, - "max": 8.08656120300293, - "p1": 2.1934221267700194, - "p5": 2.49083845615387, - "p50": 3.8485939502716064, - "p95": 5.322631454467773, - "p99": 5.83566234588623 - }, - "max_frame_ms": 8.08656120300293, - "cpu_ms_avg": 3.8722813219284005, - "gpu_ms": { - "shadow_avg": 0.27315481120673024, - "opaque_avg": 1.170840314443206, - "lod_terrain_avg": 0.3911178832036443, - "lod_water_avg": 0.009610565989865781, - "lod_culling_avg": 0, - "total_avg": 3.42967829485792, - "total": { - "min": 2.335279941558838, - "avg": 3.42967829485792, - "max": 4.590919494628906, - "p1": 2.485265407562256, - "p5": 2.59538791179657, - "p50": 3.329200029373169, - "p95": 4.429703903198242, - "p99": 4.511562366485596 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 2220.499451435947, - "vertices_avg": 1946979.9709583737, - "chunks_rendered_avg": 376.5997418522104, - "worst_frame": { - "frame_index": 2408, - "frame_ms": 8.08656120300293, - "gpu_total_ms": 4.3489203453063965, - "gpu_lod_terrain_ms": 0.5281999707221985, - "gpu_lod_water_ms": 0.027720000594854355, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5168800354003906, - "lod_cpu_ms": 0.06210600000005684, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.05181699999999978, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 84792576, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 36, - "lod_rejected_count": 88, - "lod_coverage_count": 36, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 45 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 15495, - "cpu_frame_ms": { - "min": 0.03639800101518631, - "avg": 0.2755164744994186, - "max": 0.8219259977340698, - "p1": 0.04458494059741497, - "p5": 0.0483207006007433, - "p50": 0.13464400172233582, - "p95": 0.5920343101024627, - "p99": 0.6521904206275938 - }, - "gpu_frame_ms": { - "min": 0.2571600079536438, - "avg": 0.40072844885133396, - "max": 0.5707600116729736, - "p1": 0.2701951849460602, - "p5": 0.2884399890899658, - "p50": 0.39135998487472534, - "p95": 0.5282520055770874, - "p99": 0.5546424508094787 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 3142.472831, - "avg_ms": 0.2028056038076799 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.8708489999999998, - "avg_ms": 5.6201936108422053e-5 - }, - "upload_prep": { - "total_ms": 53.639981999999996, - "avg_ms": 0.0034617606969990318 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 792.6032869999999, - "avg_ms": 0.05115219664407873 - }, - "coverage": { - "total_ms": 684.8785640000001, - "avg_ms": 0.04419997186189094 - }, - "eviction": { - "total_ms": 18.898546999999997, - "avg_ms": 0.0012196545337205549 - }, - "manager_lock_wait": { - "total_ms": 8.191783, - "avg_ms": 0.000528672668602775 - }, - "manager_lock_hold": { - "total_ms": 2015.363, - "avg_ms": 0.13006537592771863 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 84792576, - "max_bytes": 84792576, - "last_bytes": 84792576, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 73961472, - "max_bytes": 73961472, - "last_bytes": 73961472, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 26701824, - "max_bytes": 26701824, - "last_bytes": 26701824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 397165496, - "max_bytes": 397165496, - "last_bytes": 397165496, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 185455872, - "max_bytes": 185455872, - "last_bytes": 185455872, - "p50_bytes": 185455872, - "p95_bytes": 185455872, - "p99_bytes": 185455872 - }, - "logical_vram_bytes": { - "avg_bytes": 105906176, - "max_bytes": 105906176, - "last_bytes": 105906176, - "p50_bytes": 105906176, - "p95_bytes": 105906176, - "p99_bytes": 105906176 - } - }, - "visibility": { - "visible_total": 529023, - "rejected_total": 1392357, - "coverage_total": 529023, - "levels": [ - { - "candidates": 263415, - "accepted": 92971, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 170444, - "rejected_chunk_coverage": 0, - "coverage_checks": 92971 - }, - { - "candidates": 960690, - "accepted": 101464, - "rejected_no_draw": 0, - "rejected_not_ready": 697275, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 161951, - "rejected_chunk_coverage": 0, - "coverage_checks": 101464 - }, - { - "candidates": 263415, - "accepted": 133397, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 130018, - "rejected_chunk_coverage": 0, - "coverage_checks": 133397 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 433860, - "accepted": 201191, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 232669, - "rejected_chunk_coverage": 0, - "coverage_checks": 201191 - } - ] - }, - "pressure": { - "staging_pressure_total": 697275, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000560760498047, - "upload_total_bytes": 262998336, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5833.514517, - "worker_mesh_construction_total_ms": 756.238284, - "worker_far_expanded_mesh_construction_ms": 15.437912, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 73961472, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00102615356445, - "requested_duration_s": 60, - "sampled_frame_count": 15495, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 14849, - "duration_s": 60, - "fps": { - "min": 98.0180892944336, - "avg": 247.47524024979663, - "max": 591.5906372070312, - "p1": 143.4627947998047, - "p5": 166.03248596191406, - "p50": 254.33566284179688, - "p95": 398.9618347167968, - "p99": 448.96775634765646 - }, - "frame_ms": { - "min": 1.69035804271698, - "avg": 4.040808280418759, - "max": 10.202198028564453, - "p1": 2.227332057952881, - "p5": 2.506505393981934, - "p50": 3.931812047958374, - "p95": 6.022917747497558, - "p99": 6.970448551177979 - }, - "max_frame_ms": 10.202198028564453, - "cpu_ms_avg": 4.040808280418759, - "gpu_ms": { - "shadow_avg": 0.272882293712347, - "opaque_avg": 1.17158315289951, - "lod_terrain_avg": 0.3911234911535475, - "lod_water_avg": 0.010429839050730067, - "lod_culling_avg": 0, - "total_avg": 3.427910923580504, - "total": { - "min": 2.340679883956909, - "avg": 3.427910923580504, - "max": 4.562960147857666, - "p1": 2.479872093200684, - "p5": 2.5858479022979735, - "p50": 3.330160140991211, - "p95": 4.426231956481933, - "p99": 4.506163215637207 - }, - "lod_compact_terrain": { - "min": 0.25411999225616455, - "avg": 0.3909594720301383, - "max": 0.5372800230979919, - "p1": 0.26663840770721436, - "p5": 0.2847599983215332, - "p50": 0.3848400115966797, - "p95": 0.5016640186309813, - "p99": 0.5238208127021791 - }, - "lod_compact_water": { - "min": 0.0022799998987466097, - "avg": 0.010266118931572582, - "max": 0.07863999903202057, - "p1": 0.002520000096410513, - "p5": 0.0026000000070780516, - "p50": 0.003759999992325902, - "p95": 0.03144000098109245, - "p99": 0.06462399929761896 - } - }, - "draw_calls_avg": 2246.467775607785, - "vertices_avg": 1947134.0457943296, - "chunks_rendered_avg": 376.5890632365816, - "worst_frame": { - "frame_index": 3469, - "frame_ms": 10.202198028564453, - "gpu_total_ms": 3.643199920654297, - "gpu_lod_terrain_ms": 0.44196000695228577, - "gpu_lod_water_ms": 0.004879999905824661, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.2935600280761719, - "lod_cpu_ms": 0.06798799999995708, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.06292799999999943, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41705280, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 61, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 14849, - "cpu_frame_ms": { - "min": 0.030406000092625618, - "avg": 0.23689409787184912, - "max": 1.0835959911346436, - "p1": 0.03448459923267364, - "p5": 0.03956500068306923, - "p50": 0.33493998646736145, - "p95": 0.5249306082725523, - "p99": 0.5637063002586364 - }, - "gpu_frame_ms": { - "min": 0.2574400007724762, - "avg": 0.40155333039720287, - "max": 0.5891200304031372, - "p1": 0.27035999298095703, - "p5": 0.28828001022338867, - "p50": 0.39103999733924866, - "p95": 0.5313999652862549, - "p99": 0.5605807852745056 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 2699.437242, - "avg_ms": 0.18179252757761466 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.6400459999999999, - "avg_ms": 4.310364334298605e-5 - }, - "upload_prep": { - "total_ms": 27.976759, - "avg_ms": 0.0018840837093406964 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 615.3717, - "avg_ms": 0.0414419624217119 - }, - "coverage": { - "total_ms": 540.149758, - "avg_ms": 0.036376170651222305 - }, - "eviction": { - "total_ms": 14.518831, - "avg_ms": 0.0009777648999932656 - }, - "manager_lock_wait": { - "total_ms": 7.6476299999999995, - "avg_ms": 0.0005150266011179203 - }, - "manager_lock_hold": { - "total_ms": 2789.8714959999998, - "avg_ms": 0.187882786450266 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 41705280, - "max_bytes": 41705280, - "last_bytes": 41705280, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 63532864, - "max_bytes": 63532864, - "last_bytes": 63532864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 104239296, - "max_bytes": 104239296, - "last_bytes": 104239296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379478144, - "max_bytes": 379478144, - "last_bytes": 379478144, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 142368576, - "max_bytes": 142368576, - "last_bytes": 142368576, - "p50_bytes": 142368576, - "p95_bytes": 142368576, - "p99_bytes": 142368576 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 404900, - "rejected_total": 842416, - "coverage_total": 404900, - "levels": [ - { - "candidates": 29698, - "accepted": 9286, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20412, - "rejected_chunk_coverage": 0, - "coverage_checks": 9286 - }, - { - "candidates": 579111, - "accepted": 86481, - "rejected_no_draw": 0, - "rejected_not_ready": 326678, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 165952, - "rejected_chunk_coverage": 0, - "coverage_checks": 86481 - }, - { - "candidates": 222735, - "accepted": 116194, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 106541, - "rejected_chunk_coverage": 0, - "coverage_checks": 116194 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 415772, - "accepted": 192939, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 222833, - "rejected_chunk_coverage": 0, - "coverage_checks": 192939 - } - ] - }, - "pressure": { - "staging_pressure_total": 326678, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 457878 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00196361541748, - "upload_total_bytes": 305541952, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5535.541056, - "worker_mesh_construction_total_ms": 649.78311, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.574984, - "compact_submissions": 72000, - "pool_gpu_allocated_bytes": 63532864, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00204086303711, - "requested_duration_s": 60, - "sampled_frame_count": 14849, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 15.437912, - "auto": 0.574984, - "status": "measured", - "reduction_fraction": 0.9627550668769196, - "delta": -14.862928 - }, - "near_pool_geometry": { - "off": 73961472, - "auto": 63532864, - "status": "measured", - "delta": -10428608 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 262998336, - "auto": 305541952, - "status": "measured", - "delta": 42543616 - }, - "startup_worker_mesh_construction_ms": { - "off": 756.238284, - "auto": 649.78311, - "status": "measured", - "delta": -106.45517400000006 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 723.3591647314814, - "gpu_memory_mb_max": 734.720947265625, - "frames": 33242, - "duration_s": 60, - "fps": { - "min": 22.385108947753906, - "avg": 553.9933260634302, - "max": 12154.3603515625, - "p1": 178.9082844543457, - "p5": 194.93989181518555, - "p50": 1384.0198364257812, - "p95": 8155.243603515623, - "p99": 10305.136328124985 - }, - "frame_ms": { - "min": 0.08227500319480896, - "avg": 1.8050758970433944, - "max": 44.67255783081055, - "p1": 0.09703899130225181, - "p5": 0.12262049391865731, - "p50": 0.7225330173969269, - "p95": 5.129786562919615, - "p99": 5.589456119537349 - }, - "max_frame_ms": 44.67255783081055, - "cpu_ms_avg": 1.8050758970433944, - "gpu_ms": { - "shadow_avg": 0.06862796100929974, - "opaque_avg": 0.5236978509725772, - "lod_terrain_avg": 0.1133399819415633, - "lod_water_avg": 0.007308295531824326, - "lod_culling_avg": 0, - "total_avg": 1.2570120164557819, - "total": { - "min": 0, - "avg": 1.2570120164557819, - "max": 4.533760070800781, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.404754376411438, - "p99": 4.437983498573303 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 737.4455808916431, - "vertices_avg": 2239029.193851152, - "chunks_rendered_avg": 374.8177907466458, - "worst_frame": { - "frame_index": 9986, - "frame_ms": 44.67255783081055, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.30763900000010835, - "dominant_lod_cpu_category": "upload_submission", - "dominant_lod_cpu_category_ms": 0.2941030000000069, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 1429056, - "lod_pending_cpu_upload_bytes": 3822912, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 33242, - "cpu_frame_ms": { - "min": 0.0016629999736323953, - "avg": 0.26971352346453337, - "max": 43.376060485839844, - "p1": 0.0021840000990778208, - "p5": 0.002584999892860651, - "p50": 0.23754650354385376, - "p95": 0.641112008690834, - "p99": 0.9720434683561305 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.12064827705260169, - "max": 0.4758799970149994, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44991999864578247, - "p99": 0.470320003926754 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 7788.393397, - "avg_ms": 0.23429376683111724 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.333795, - "avg_ms": 1.0041363335539379e-5 - }, - "state_transition": { - "total_ms": 2.644522, - "avg_ms": 7.955363696528488e-5 - }, - "upload_prep": { - "total_ms": 20.516845, - "avg_ms": 0.0006171964683232056 - }, - "upload_submission": { - "total_ms": 157.81963299999998, - "avg_ms": 0.004747597406894891 - }, - "visibility": { - "total_ms": 589.235872, - "avg_ms": 0.01772564442572649 - }, - "coverage": { - "total_ms": 498.51429299999995, - "avg_ms": 0.014996519252752541 - }, - "eviction": { - "total_ms": 63.370813000000005, - "avg_ms": 0.0019063477829252152 - }, - "manager_lock_wait": { - "total_ms": 16.252553000000002, - "avg_ms": 0.0004889162204440167 - }, - "manager_lock_hold": { - "total_ms": 1575.047927, - "avg_ms": 0.04738126246916551 - } - }, - "workers": { - "generation_total_ms": 7714.486416999999, - "mesh_construction_total_ms": 1028.792389, - "far_expanded_mesh_construction_total_ms": 40.069714999999995, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 544533504, - "upload_avg_bytes": 16380.888755189219, - "far_expanded_upload_total_bytes": 8046720, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 11074133.270441009, - "max_bytes": 84792576, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 554077.2988388183, - "max_bytes": 61313856, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 554077.2988388183, - "max_bytes": 61313856, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 14454.061729137838, - "max_bytes": 2099520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 124138881.85596535, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 52962337.81938512, - "max_bytes": 100142784, - "last_bytes": 33514944, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 71176544.03658023, - "max_bytes": 100702784, - "last_bytes": 100702784, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 124138881.85596535, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 16908449.661512543, - "max_bytes": 18743296, - "last_bytes": 18743296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 309612464.98177004, - "max_bytes": 397491107, - "last_bytes": 310112721, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 135227469.1881355, - "max_bytes": 185455872, - "last_bytes": 134217728, - "p50_bytes": 134217728, - "p95_bytes": 185455872, - "p99_bytes": 185455872 - }, - "logical_vram_bytes": { - "avg_bytes": 141601408.8163167, - "max_bytes": 180720448, - "last_bytes": 152961024, - "p50_bytes": 152961024, - "p95_bytes": 152961024, - "p99_bytes": 154358208 - } - }, - "visibility": { - "visible_total": 706399, - "rejected_total": 607064, - "coverage_total": 706435, - "levels": [ - { - "candidates": 152407, - "accepted": 58449, - "rejected_no_draw": 0, - "rejected_not_ready": 1782, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 92140, - "rejected_chunk_coverage": 36, - "coverage_checks": 58485 - }, - { - "candidates": 110023, - "accepted": 19907, - "rejected_no_draw": 0, - "rejected_not_ready": 61197, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 28919, - "rejected_chunk_coverage": 0, - "coverage_checks": 19907 - }, - { - "candidates": 193307, - "accepted": 142776, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 50531, - "rejected_chunk_coverage": 0, - "coverage_checks": 142776 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 857726, - "accepted": 485267, - "rejected_no_draw": 0, - "rejected_not_ready": 24, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 372435, - "rejected_chunk_coverage": 0, - "coverage_checks": 485267 - } - ] - }, - "pressure": { - "staging_pressure_total": 250380, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001081466674805, - "upload_total_bytes": 265923648, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5560.98152, - "worker_mesh_construction_total_ms": 717.153769, - "worker_far_expanded_mesh_construction_ms": 24.804161, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 74465856, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00400161743164, - "requested_duration_s": 60, - "sampled_frame_count": 33242, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 34455, - "duration_s": 60, - "fps": { - "min": 19.929645538330078, - "avg": 574.2180820379008, - "max": 11648.087890625, - "p1": 179.60630279541016, - "p5": 195.87515258789062, - "p50": 1488.398681640625, - "p95": 8156.473828124999, - "p99": 10186.417714843748 - }, - "frame_ms": { - "min": 0.08585100620985031, - "avg": 1.7414986244442154, - "max": 50.17650604248047, - "p1": 0.09816993802785873, - "p5": 0.12260200008749962, - "p50": 0.6718629598617554, - "p95": 5.10529260635376, - "p99": 5.567733163833617 - }, - "max_frame_ms": 50.17650604248047, - "cpu_ms_avg": 1.7414986244442154, - "gpu_ms": { - "shadow_avg": 0.06647528722166514, - "opaque_avg": 0.5103335574548246, - "lod_terrain_avg": 0.11031904339513555, - "lod_water_avg": 0.007334892477928304, - "lod_culling_avg": 0, - "total_avg": 1.221069773733659, - "total": { - "min": 0, - "avg": 1.221069773733659, - "max": 4.4659199714660645, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 4.398531866073609, - "p99": 4.425498094558716 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.11026477897968162, - "max": 0.41064000129699707, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.39368000626564026, - "p99": 0.40588000416755676 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.007280455094775256, - "max": 0.07332000136375427, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.06307999789714813, - "p99": 0.06379999965429306 - } - }, - "draw_calls_avg": 746.5013786097809, - "vertices_avg": 2252412.041619504, - "chunks_rendered_avg": 375.01056450442604, - "worst_frame": { - "frame_index": 944, - "frame_ms": 50.17650604248047, - "gpu_total_ms": 3.6976399421691895, - "gpu_lod_terrain_ms": 0.3580799996852875, - "gpu_lod_water_ms": 0.0041600000113248825, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6767200231552124, - "lod_cpu_ms": 3.451908999999887, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 3.0687390000000008, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 49150848, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 26 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 34455, - "cpu_frame_ms": { - "min": 0.0011820000363513827, - "avg": 0.25543539216112116, - "max": 22.078073501586914, - "p1": 0.002193999942392111, - "p5": 0.0025949999690055847, - "p50": 0.24148400127887726, - "p95": 0.6191990196704864, - "p99": 0.9759074628353119 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.11765393598594162, - "max": 0.47440001368522644, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.44859999418258667, - "p99": 0.4696000057458877 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 7321.95742, - "avg_ms": 0.212507833986359 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 1.9115369999999998, - "avg_ms": 5.547923378319547e-5 - }, - "state_transition": { - "total_ms": 5.60691, - "avg_ms": 0.0001627313887679582 - }, - "upload_prep": { - "total_ms": 10.434485, - "avg_ms": 0.0003028438543027137 - }, - "upload_submission": { - "total_ms": 301.661991, - "avg_ms": 0.008755245711797997 - }, - "visibility": { - "total_ms": 562.7953669999999, - "avg_ms": 0.016334214685822086 - }, - "coverage": { - "total_ms": 481.11675499999996, - "avg_ms": 0.01396362661442461 - }, - "eviction": { - "total_ms": 68.464438, - "avg_ms": 0.00198706829197504 - }, - "manager_lock_wait": { - "total_ms": 29.892848, - "avg_ms": 0.0008675910027572196 - }, - "manager_lock_hold": { - "total_ms": 2685.362896, - "avg_ms": 0.07793826428675084 - } - }, - "workers": { - "generation_total_ms": 16185.269980999998, - "mesh_construction_total_ms": 1950.486157, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.77934 - }, - "memory_bytes": { - "upload_total_bytes": 957337712, - "upload_avg_bytes": 27785.160702365403, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 340784, - "pending_cpu_upload_bytes": { - "avg_bytes": 3778017.5305761136, - "max_bytes": 49150848, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 720859.4667827601, - "max_bytes": 37689792, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 720859.4667827601, - "max_bytes": 37689792, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 141104.95986068784, - "max_bytes": 2823936, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 44736848.96032506, - "max_bytes": 80699984, - "last_bytes": 33549744, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 123035311.03967494, - "max_bytes": 138851152, - "last_bytes": 134222416, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 456796.52323320275, - "max_bytes": 502512, - "last_bytes": 502512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66652067.476766795, - "max_bytes": 66947136, - "last_bytes": 66606352, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 296366725.7070672, - "max_bytes": 391403928, - "last_bytes": 288921213, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 104582418.4904368, - "max_bytes": 149814144, - "last_bytes": 100663296, - "p50_bytes": 100663296, - "p95_bytes": 149814144, - "p99_bytes": 149814144 - }, - "logical_vram_bytes": { - "avg_bytes": 235601883.46678275, - "max_bytes": 272570816, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 237423488, - "p99_bytes": 246712064 - } - }, - "visibility": { - "visible_total": 678039, - "rejected_total": 544231, - "coverage_total": 678069, - "levels": [ - { - "candidates": 163463, - "accepted": 67876, - "rejected_no_draw": 0, - "rejected_not_ready": 377, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 95180, - "rejected_chunk_coverage": 30, - "coverage_checks": 67906 - }, - { - "candidates": 46337, - "accepted": 8725, - "rejected_no_draw": 0, - "rejected_not_ready": 25023, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 12589, - "rejected_chunk_coverage": 0, - "coverage_checks": 8725 - }, - { - "candidates": 149201, - "accepted": 113547, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 35654, - "rejected_chunk_coverage": 0, - "coverage_checks": 113547 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 863269, - "accepted": 487891, - "rejected_no_draw": 0, - "rejected_not_ready": 6, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 375372, - "rejected_chunk_coverage": 0, - "coverage_checks": 487891 - } - ] - }, - "pressure": { - "staging_pressure_total": 96875, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 1039832 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001958847045898, - "upload_total_bytes": 321012352, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5327.528866, - "worker_mesh_construction_total_ms": 594.789559, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.306733, - "compact_submissions": 64050, - "pool_gpu_allocated_bytes": 62365696, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.003395080566406, - "requested_duration_s": 60, - "sampled_frame_count": 34455, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 24.804161, - "auto": 0.306733, - "status": "measured", - "reduction_fraction": 0.987633808698468, - "delta": -24.497428 - }, - "near_pool_geometry": { - "off": 74465856, - "auto": 62365696, - "status": "measured", - "delta": -12100160 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 265923648, - "auto": 321012352, - "status": "measured", - "delta": 55088704 - }, - "startup_worker_mesh_construction_ms": { - "off": 717.153769, - "auto": 594.789559, - "status": "measured", - "delta": -122.36420999999996 - } - } - } - } - } -} diff --git a/docs/benchmarks/baseline.json b/docs/benchmarks/baseline.json deleted file mode 100644 index f8ec16f7..00000000 --- a/docs/benchmarks/baseline.json +++ /dev/null @@ -1,16105 +0,0 @@ -{ - "schema_version": 3, - "artifact_type": "benchmark-baseline", - "status": "ready", - "generated": true, - "required_presets": ["low", "medium", "high"], - "required_scenarios": ["stationary", "traversal", "rapid-turn", "teleport-eviction"], - "provenance": { - "world_seed": 12345, - "build.mode": "ReleaseFast", - "build.world": "overworld", - "build.fixture": "none", - "build.headless": true, - "build.resolution": [1920, 1080], - "provenance.gpu_adapter": "AMD Radeon RX 5700 XT", - "provenance.gpu_driver": "Mesa RADV 25.2.6", - "provenance.runner": "local NixOS x86_64", - "provenance.zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "capture_duration_s": 5, - "results": { - "low": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 5877, - "duration_s": 5, - "fps": { - "min": 576.8047485351562, - "avg": 1175.3042256307788, - "max": 2357.56201171875, - "p1": 849.8942993164062, - "p5": 929.0026977539063, - "p50": 1114.558837890625, - "p95": 1566.268798828125, - "p99": 1705.0885937499993 - }, - "frame_ms": { - "min": 0.4241670072078705, - "avg": 0.8508435332675724, - "max": 1.7336889505386353, - "p1": 0.586480097770691, - "p5": 0.6384600043296814, - "p50": 0.8972160220146179, - "p95": 1.0764231204986572, - "p99": 1.1766173219680782 - }, - "max_frame_ms": 1.7336889505386353, - "cpu_ms_avg": 0.8508435332675724, - "gpu_ms": { - "shadow_avg": 0.0003596477786645814, - "opaque_avg": 0.45569919345126136, - "lod_terrain_avg": 0.06215789005659826, - "lod_water_avg": 0.007694027559653936, - "lod_culling_avg": 0, - "total_avg": 0.6599225825206224, - "total": { - "min": 0.6557599902153015, - "avg": 0.6599225825206224, - "max": 0.6714800596237183, - "p1": 0.6572303891181945, - "p5": 0.6579200029373169, - "p50": 0.6597599983215332, - "p95": 0.6624000668525696, - "p99": 0.6647696828842163 - }, - "lod_compact_terrain": { - "min": 0.06128000095486641, - "avg": 0.0619798740836376, - "max": 0.06620000302791595, - "p1": 0.06143999844789505, - "p5": 0.06159999966621399, - "p50": 0.06191999837756157, - "p95": 0.06247999891638756, - "p99": 0.06272000074386597 - }, - "lod_compact_water": { - "min": 0.00687999976798892, - "avg": 0.007528010883028001, - "max": 0.011839999817311764, - "p1": 0.007150400076061487, - "p5": 0.007240000180900097, - "p50": 0.007519999984651804, - "p95": 0.007888000272214406, - "p99": 0.00824000034481287 - } - }, - "draw_calls_avg": 493, - "vertices_avg": 716982, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 3255, - "frame_ms": 1.7336889505386353, - "gpu_total_ms": 0.658280074596405, - "gpu_lod_terrain_ms": 0.06196000054478645, - "gpu_lod_water_ms": 0.0074800001457333565, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45443999767303467, - "lod_cpu_ms": 0.014737000000025091, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.012813999999991665, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 21, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 5877, - "cpu_frame_ms": { - "min": 0.013353999704122543, - "avg": 0.09647072550915314, - "max": 0.9158509969711304, - "p1": 0.013546000234782696, - "p5": 0.013886000029742718, - "p50": 0.15349799394607544, - "p95": 0.19753699898719787, - "p99": 0.23047295928001402 - }, - "gpu_frame_ms": { - "min": 0.06880000233650208, - "avg": 0.06985191755555856, - "max": 0.07400000095367432, - "p1": 0.06920000165700912, - "p5": 0.06936000287532806, - "p50": 0.06980000436306, - "p95": 0.07047999650239944, - "p99": 0.0708799958229065 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 460.4110079999999, - "avg_ms": 0.07834116181725369 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.24041100000000004, - "avg_ms": 4.090709545686575e-5 - }, - "upload_prep": { - "total_ms": 0.6508689999999999, - "avg_ms": 0.00011074851114514207 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 79.31354299999998, - "avg_ms": 0.01349558329079462 - }, - "coverage": { - "total_ms": 63.585162, - "avg_ms": 0.010819323124042879 - }, - "eviction": { - "total_ms": 4.7581060000000015, - "avg_ms": 0.0008096147694401909 - }, - "manager_lock_wait": { - "total_ms": 3.005123, - "avg_ms": 0.0005113362259656287 - }, - "manager_lock_hold": { - "total_ms": 891.2458510000001, - "avg_ms": 0.15164979598434578 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 135171, - "rejected_total": 123417, - "coverage_total": 135171, - "levels": [ - { - "candidates": 94032, - "accepted": 35262, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 58770, - "rejected_chunk_coverage": 0, - "coverage_checks": 35262 - }, - { - "candidates": 23508, - "accepted": 11754, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11754, - "rejected_chunk_coverage": 0, - "coverage_checks": 11754 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 141048, - "accepted": 88155, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 52893, - "rejected_chunk_coverage": 0, - "coverage_checks": 88155 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 515340 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000269889831543, - "upload_total_bytes": 63685056, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 792.001341, - "worker_mesh_construction_total_ms": 91.195402, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.678035, - "compact_submissions": 339030, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000419616699219, - "requested_duration_s": 5, - "sampled_frame_count": 5877, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 9032, - "duration_s": 5, - "fps": { - "min": 267.22784423828125, - "avg": 1806.3916070976609, - "max": 13555.830078125, - "p1": 1059.5944140625002, - "p5": 1187.1000061035156, - "p50": 1966.3131103515625, - "p95": 8204.051416015622, - "p99": 12323.770722656256 - }, - "frame_ms": { - "min": 0.0737689957022667, - "avg": 0.5535898174409176, - "max": 3.7421247959136963, - "p1": 0.08114401243627072, - "p5": 0.1218909990042448, - "p50": 0.5085659921169281, - "p95": 0.8423890262842177, - "p99": 0.9437581723928461 - }, - "max_frame_ms": 3.7421247959136963, - "cpu_ms_avg": 0.5535898174409176, - "gpu_ms": { - "shadow_avg": 0.00030210031165940496, - "opaque_avg": 0.25212711247593245, - "lod_terrain_avg": 0.0522941407781388, - "lod_water_avg": 0.054026076180950726, - "lod_culling_avg": 0, - "total_avg": 0.42187778191084985, - "total": { - "min": 0, - "avg": 0.42187778191084985, - "max": 0.5989200472831726, - "p1": 0, - "p5": 0, - "p50": 0.488740012049675, - "p95": 0.5561200380325317, - "p99": 0.58761520922184 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.05214510631002651, - "max": 0.06723999977111816, - "p1": 0, - "p5": 0, - "p50": 0.06191999837756157, - "p95": 0.06239999830722809, - "p99": 0.06260000169277191 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.05388811781394697, - "max": 0.10044000297784805, - "p1": 0, - "p5": 0, - "p50": 0.06309999898076057, - "p95": 0.09551999717950821, - "p99": 0.09836000204086304 - } - }, - "draw_calls_avg": 419.1480292294066, - "vertices_avg": 847030.2914083259, - "chunks_rendered_avg": 123.42891939769707, - "worst_frame": { - "frame_index": 4384, - "frame_ms": 3.7421247959136963, - "gpu_total_ms": 0.5054000020027161, - "gpu_lod_terrain_ms": 0.06323999911546707, - "gpu_lod_water_ms": 0.07388000190258026, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.3022800087928772, - "lod_cpu_ms": 0.26661200000009444, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.23774700000012672, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 15, - "lod_rejected_count": 14, - "lod_coverage_count": 15, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 9032, - "cpu_frame_ms": { - "min": 0.0007919999770820141, - "avg": 0.09838450501137093, - "max": 1.9862940311431885, - "p1": 0.0010420000180602074, - "p5": 0.001563000027090311, - "p50": 0.12225949764251709, - "p95": 0.23271799907088261, - "p99": 0.3412468838691716 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.10632021696058468, - "max": 0.1629599928855896, - "p1": 0, - "p5": 0, - "p50": 0.12547999620437622, - "p95": 0.1578800082206726, - "p99": 0.16082760632038118 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 774.234665, - "avg_ms": 0.08572128709034543 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.35783999999999994, - "avg_ms": 3.9619131975199284e-5 - }, - "upload_prep": { - "total_ms": 0.9891729999999999, - "avg_ms": 0.00010951871124889281 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 82.873714, - "avg_ms": 0.009175566209034544 - }, - "coverage": { - "total_ms": 68.36670700000002, - "avg_ms": 0.007569387400354298 - }, - "eviction": { - "total_ms": 6.979297000000001, - "avg_ms": 0.0007727299601417184 - }, - "manager_lock_wait": { - "total_ms": 4.214599, - "avg_ms": 0.00046662965013286094 - }, - "manager_lock_hold": { - "total_ms": 1119.461299, - "avg_ms": 0.1239438993578388 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 40161.03454384411, - "max_bytes": 2980800, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 40161.03454384411, - "max_bytes": 2980800, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 10861396.76173605, - "max_bytes": 17749824, - "last_bytes": 8848704, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 106579115.23826395, - "max_bytes": 108591808, - "last_bytes": 108591808, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 174351973.05425155, - "max_bytes": 178437324, - "last_bytes": 173227548, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184589537.03454384, - "max_bytes": 187530176, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 185937152 - } - }, - "visibility": { - "visible_total": 112146, - "rejected_total": 127969, - "coverage_total": 112150, - "levels": [ - { - "candidates": 27427, - "accepted": 6958, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20465, - "rejected_chunk_coverage": 4, - "coverage_checks": 6962 - }, - { - "candidates": 30384, - "accepted": 4553, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 25831, - "rejected_chunk_coverage": 0, - "coverage_checks": 4553 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 182304, - "accepted": 100635, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 81669, - "rejected_chunk_coverage": 0, - "coverage_checks": 100635 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 625500 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000826835632324, - "upload_total_bytes": 63593664, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 770.458421, - "worker_mesh_construction_total_ms": 96.58237, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.068969, - "compact_submissions": 424230, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.00002384185791, - "requested_duration_s": 5, - "sampled_frame_count": 9032, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 7756, - "duration_s": 5, - "fps": { - "min": 756.4783325195312, - "avg": 1551.106477416076, - "max": 2810.8125, - "p1": 927.6631378173828, - "p5": 947.4012756347656, - "p50": 1585.2747192382812, - "p95": 2745.1411743164062, - "p99": 2774.517370605469 - }, - "frame_ms": { - "min": 0.35576900839805603, - "avg": 0.6447010663419178, - "max": 1.3219149112701416, - "p1": 0.36042305678129194, - "p5": 0.36427999287843704, - "p50": 0.630805492401123, - "p95": 1.0555189549922943, - "p99": 1.0779775559902192 - }, - "max_frame_ms": 1.3219149112701416, - "cpu_ms_avg": 0.6447010663419178, - "gpu_ms": { - "shadow_avg": 0.0003588383211191698, - "opaque_avg": 0.3136609128285862, - "lod_terrain_avg": 0.06177940696829477, - "lod_water_avg": 0.027708849921001696, - "lod_culling_avg": 0, - "total_avg": 0.5156604178238309, - "total": { - "min": 0.34808000922203064, - "avg": 0.5156604178238309, - "max": 0.7481600046157837, - "p1": 0.3603420123457909, - "p5": 0.3655899912118912, - "p50": 0.4660400003194809, - "p95": 0.7396000623703003, - "p99": 0.7429999977350235 - }, - "lod_compact_terrain": { - "min": 0.06072000041604042, - "avg": 0.061590928321607244, - "max": 0.06672000139951706, - "p1": 0.060920000076293945, - "p5": 0.061039999127388, - "p50": 0.06159999966621399, - "p95": 0.06212000176310539, - "p99": 0.06232000142335892 - }, - "lod_compact_water": { - "min": 0.004040000028908253, - "avg": 0.027538298085272358, - "max": 0.08820000290870667, - "p1": 0.004120000172406435, - "p5": 0.004240000154823065, - "p50": 0.005919999908655882, - "p95": 0.08037000149488449, - "p99": 0.08532000333070755 - } - }, - "draw_calls_avg": 486.77153171738007, - "vertices_avg": 716682, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 862, - "frame_ms": 1.3219149112701416, - "gpu_total_ms": 0.7232400178909302, - "gpu_lod_terrain_ms": 0.06212000176310539, - "gpu_lod_water_ms": 0.07744000107049942, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5187600255012512, - "lod_cpu_ms": 0.018584000000004153, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.016339999999985366, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 24, - "lod_rejected_count": 20, - "lod_coverage_count": 24, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 7756, - "cpu_frame_ms": { - "min": 0.009637000039219856, - "avg": 0.08923795599540356, - "max": 0.2840929925441742, - "p1": 0.010039549972862005, - "p5": 0.01040900032967329, - "p50": 0.10125549882650375, - "p95": 0.1760304979979992, - "p99": 0.20563554391264915 - }, - "gpu_frame_ms": { - "min": 0.06515999883413315, - "avg": 0.08948825688023061, - "max": 0.15092000365257263, - "p1": 0.06564000248908997, - "p5": 0.06591999530792236, - "p50": 0.06787999719381332, - "p95": 0.1426199972629547, - "p99": 0.1476179927587509 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 572.241988, - "avg_ms": 0.07378055544094894 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.324468, - "avg_ms": 4.1834450747808144e-5 - }, - "upload_prep": { - "total_ms": 0.774721, - "avg_ms": 9.988666838576585e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 86.77744899999999, - "avg_ms": 0.011188428184631252 - }, - "coverage": { - "total_ms": 67.77742800000001, - "avg_ms": 0.008738709128416712 - }, - "eviction": { - "total_ms": 6.359087000000001, - "avg_ms": 0.0008198925992779784 - }, - "manager_lock_wait": { - "total_ms": 3.890416, - "avg_ms": 0.0005016008251676121 - }, - "manager_lock_hold": { - "total_ms": 1138.937892, - "avg_ms": 0.14684604074265084 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 155357, - "rejected_total": 185907, - "coverage_total": 155357, - "levels": [ - { - "candidates": 124096, - "accepted": 46555, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 77541, - "rejected_chunk_coverage": 0, - "coverage_checks": 46555 - }, - { - "candidates": 31024, - "accepted": 16614, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 14410, - "rejected_chunk_coverage": 0, - "coverage_checks": 16614 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 186144, - "accepted": 92188, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 93956, - "rejected_chunk_coverage": 0, - "coverage_checks": 92188 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 609086 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000334739685059, - "upload_total_bytes": 63259200, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 809.524362, - "worker_mesh_construction_total_ms": 89.34204, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.961686, - "compact_submissions": 424710, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000302314758301, - "requested_duration_s": 5, - "sampled_frame_count": 7756, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 6907, - "duration_s": 5, - "fps": { - "min": 85.36180114746094, - "avg": 1381.3145004399344, - "max": 12713.265625, - "p1": 835.8564904785156, - "p5": 954.1486938476562, - "p50": 1470.1795654296875, - "p95": 2699.222900390625, - "p99": 7289.120234374964 - }, - "frame_ms": { - "min": 0.07865799963474274, - "avg": 0.72394809413896, - "max": 11.714841842651367, - "p1": 0.13719198018312453, - "p5": 0.37047702074050903, - "p50": 0.6801890134811401, - "p95": 1.0480547189712524, - "p99": 1.1963777971267693 - }, - "max_frame_ms": 11.714841842651367, - "cpu_ms_avg": 0.72394809413896, - "gpu_ms": { - "shadow_avg": 0.00033918488657796943, - "opaque_avg": 0.34408311573139955, - "lod_terrain_avg": 0.05868857680178507, - "lod_water_avg": 0.0076221166667639695, - "lod_culling_avg": 0, - "total_avg": 0.5357605256080904, - "total": { - "min": 0, - "avg": 0.5357605256080904, - "max": 0.6704000234603882, - "p1": 0, - "p5": 0, - "p50": 0.6589199900627136, - "p95": 0.6612000465393066, - "p99": 0.6628776037693023 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.05851839584667482, - "max": 0.06800000369548798, - "p1": 0, - "p5": 0, - "p50": 0.06196000054478645, - "p95": 0.0623599998652935, - "p99": 0.06255999952554703 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.007465417670463301, - "max": 0.009479999542236328, - "p1": 0, - "p5": 0, - "p50": 0.0075599998235702515, - "p95": 0.009080000221729279, - "p99": 0.009237599615007622 - } - }, - "draw_calls_avg": 465.30215723179384, - "vertices_avg": 606861.526856812, - "chunks_rendered_avg": 112.99898653539887, - "worst_frame": { - "frame_index": 4714, - "frame_ms": 11.714841842651367, - "gpu_total_ms": 0.6598400473594666, - "gpu_lod_terrain_ms": 0.061879999935626984, - "gpu_lod_water_ms": 0.007720000110566616, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45583999156951904, - "lod_cpu_ms": 0.0018039999999928114, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.00012100000000003774, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 6907, - "cpu_frame_ms": { - "min": 0.0009919999865815043, - "avg": 0.10042949951099713, - "max": 2.022331953048706, - "p1": 0.0013420599745586515, - "p5": 0.013638700265437363, - "p50": 0.10420600324869156, - "p95": 0.2114916950464248, - "p99": 0.3029073536396026 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06631069344205347, - "max": 0.07756000012159348, - "p1": 0, - "p5": 0, - "p50": 0.06988000124692917, - "p95": 0.0716799944639206, - "p99": 0.07187999784946442 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 573.6140610000001, - "avg_ms": 0.0830482207905024 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.291743, - "avg_ms": 4.2238743303894595e-5 - }, - "upload_prep": { - "total_ms": 0.713814, - "avg_ms": 0.0001033464601129289 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 90.28942799999999, - "avg_ms": 0.013072162733458808 - }, - "coverage": { - "total_ms": 74.31920299999999, - "avg_ms": 0.010759983060663093 - }, - "eviction": { - "total_ms": 7.090540000000001, - "avg_ms": 0.00102657304184161 - }, - "manager_lock_wait": { - "total_ms": 3.462327000000001, - "avg_ms": 0.0005012779788620242 - }, - "manager_lock_hold": { - "total_ms": 994.5286959999999, - "avg_ms": 0.14398851831475312 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 40796.17779064717, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 40796.17779064717, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 13150054.954104532, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 104290457.04589547, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 175180305.87461996, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184590172.17779064, - "max_bytes": 202160576, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 149937, - "rejected_total": 100799, - "coverage_total": 149937, - "levels": [ - { - "candidates": 75424, - "accepted": 28284, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 47140, - "rejected_chunk_coverage": 0, - "coverage_checks": 28284 - }, - { - "candidates": 18856, - "accepted": 9428, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 9428, - "rejected_chunk_coverage": 0, - "coverage_checks": 9428 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 156456, - "accepted": 112225, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 44231, - "rejected_chunk_coverage": 0, - "coverage_checks": 112225 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 562250 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000383377075195, - "upload_total_bytes": 62998848, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 796.708605, - "worker_mesh_construction_total_ms": 97.513328, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.34569, - "compact_submissions": 337800, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000305652618408, - "requested_duration_s": 5, - "sampled_frame_count": 6907, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - }, - "medium": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 1845, - "duration_s": 5, - "fps": { - "min": 252.4713134765625, - "avg": 368.81901427283316, - "max": 575.109619140625, - "p1": 283.4532458496094, - "p5": 298.6654052734375, - "p50": 363.15618896484375, - "p95": 466.5878601074219, - "p99": 481.8700122070312 - }, - "frame_ms": { - "min": 1.7387990951538086, - "avg": 2.7113569563966458, - "max": 3.960845947265625, - "p1": 2.0752491188049316, - "p5": 2.143218994140625, - "p50": 2.753636121749878, - "p95": 3.3482284069061277, - "p99": 3.5279187679290773 - }, - "max_frame_ms": 3.960845947265625, - "cpu_ms_avg": 2.7113569563966458, - "gpu_ms": { - "shadow_avg": 0.09242712748729116, - "opaque_avg": 0.8858823856364098, - "lod_terrain_avg": 0.15273537130213688, - "lod_water_avg": 0.004453485071921575, - "lod_culling_avg": 0, - "total_avg": 2.276980789567074, - "total": { - "min": 2.2415199279785156, - "avg": 2.276980789567074, - "max": 2.3204398155212402, - "p1": 2.2498704624176025, - "p5": 2.2557599544525146, - "p50": 2.275519847869873, - "p95": 2.301512098312378, - "p99": 2.3099247455596927 - }, - "lod_compact_terrain": { - "min": 0.15068000555038452, - "avg": 0.1525720757601384, - "max": 0.15768000483512878, - "p1": 0.1512800008058548, - "p5": 0.15160800218582154, - "p50": 0.15255999565124512, - "p95": 0.15355999767780304, - "p99": 0.1539199948310852 - }, - "lod_compact_water": { - "min": 0.0038399999029934406, - "avg": 0.004291533873489677, - "max": 0.010080000385642052, - "p1": 0.00395999988541007, - "p5": 0.004040000028908253, - "p50": 0.004279999993741512, - "p95": 0.004559999797493219, - "p99": 0.004800000227987766 - } - }, - "draw_calls_avg": 1753, - "vertices_avg": 1580214, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 807, - "frame_ms": 3.960845947265625, - "gpu_total_ms": 2.261119842529297, - "gpu_lod_terrain_ms": 0.1515599936246872, - "gpu_lod_water_ms": 0.004800000227987766, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8873999714851379, - "lod_cpu_ms": 0.04316200000000947, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.038032000000001176, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 24258816, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 27, - "lod_rejected_count": 43, - "lod_coverage_count": 27, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 13 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1845, - "cpu_frame_ms": { - "min": 0.020508000627160072, - "avg": 0.2130662633424125, - "max": 0.6046680212020874, - "p1": 0.021199999377131462, - "p5": 0.022081800177693366, - "p50": 0.3129790127277374, - "p95": 0.4787039935588836, - "p99": 0.5216370606422424 - }, - "gpu_frame_ms": { - "min": 0.15532000362873077, - "avg": 0.15718885623827214, - "max": 0.16307999193668365, - "p1": 0.15584000945091248, - "p5": 0.15616001188755035, - "p50": 0.15715999901294708, - "p95": 0.15820001065731049, - "p99": 0.15863999724388123 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 334.477854, - "avg_ms": 0.18128880975609754 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.08926299999999998, - "avg_ms": 4.838102981029809e-5 - }, - "upload_prep": { - "total_ms": 1.9186430000000003, - "avg_ms": 0.0010399149051490517 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 41.543409999999994, - "avg_ms": 0.022516753387533874 - }, - "coverage": { - "total_ms": 33.358927, - "avg_ms": 0.018080719241192412 - }, - "eviction": { - "total_ms": 1.5331979999999996, - "avg_ms": 0.0008310016260162599 - }, - "manager_lock_wait": { - "total_ms": 0.9776380000000007, - "avg_ms": 0.0005298850948509488 - }, - "manager_lock_hold": { - "total_ms": 305.9017, - "avg_ms": 0.16580037940379405 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 24258816, - "max_bytes": 24258816, - "last_bytes": 24258816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 39121216, - "max_bytes": 39121216, - "last_bytes": 39121216, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 95096512, - "max_bytes": 95096512, - "last_bytes": 95096512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 267860326, - "max_bytes": 267860326, - "last_bytes": 267860326, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 91367680, - "max_bytes": 91367680, - "last_bytes": 91367680, - "p50_bytes": 91367680, - "p95_bytes": 91367680, - "p99_bytes": 91367680 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 49815, - "rejected_total": 79335, - "coverage_total": 49815, - "levels": [ - { - "candidates": 22140, - "accepted": 11070, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11070, - "rejected_chunk_coverage": 0, - "coverage_checks": 11070 - }, - { - "candidates": 55350, - "accepted": 11070, - "rejected_no_draw": 0, - "rejected_not_ready": 23985, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20295, - "rejected_chunk_coverage": 0, - "coverage_checks": 11070 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 51660, - "accepted": 27675, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 23985, - "rejected_chunk_coverage": 0, - "coverage_checks": 27675 - } - ] - }, - "pressure": { - "staging_pressure_total": 23985, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 157980 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002805709838867, - "upload_total_bytes": 166964992, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2552.651607, - "worker_mesh_construction_total_ms": 299.019317, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 3.435852, - "compact_submissions": 102630, - "pool_gpu_allocated_bytes": 39121216, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.002457141876221, - "requested_duration_s": 5, - "sampled_frame_count": 1845, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 3259, - "duration_s": 5, - "fps": { - "min": 177.8999786376953, - "avg": 651.6059044453843, - "max": 12584.9482421875, - "p1": 293.52267883300783, - "p5": 325.9034820556641, - "p50": 539.7723388671875, - "p95": 9079.93046875, - "p99": 11288.337578125 - }, - "frame_ms": { - "min": 0.07945999503135681, - "avg": 1.5346699487800868, - "max": 5.62113618850708, - "p1": 0.0885870186984539, - "p5": 0.11013300046324731, - "p50": 1.852632999420166, - "p95": 3.068393301963806, - "p99": 3.4068918657302856 - }, - "max_frame_ms": 5.62113618850708, - "cpu_ms_avg": 1.5346699487800868, - "gpu_ms": { - "shadow_avg": 0.06507133778005189, - "opaque_avg": 0.3776444797685593, - "lod_terrain_avg": 0.08142517329670601, - "lod_water_avg": 0.021002344256732756, - "lod_culling_avg": 0, - "total_avg": 1.1510211619251014, - "total": { - "min": 0, - "avg": 1.1510211619251014, - "max": 2.435959815979004, - "p1": 0, - "p5": 0, - "p50": 1.934360146522522, - "p95": 2.328399896621704, - "p99": 2.3944776248931885 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.08133726915668658, - "max": 0.17239999771118164, - "p1": 0, - "p5": 0, - "p50": 0.13252000510692596, - "p95": 0.16835999488830566, - "p99": 0.17051359981298447 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.02091381403375514, - "max": 0.06691999733448029, - "p1": 0, - "p5": 0, - "p50": 0.004679999779909849, - "p95": 0.06335999816656113, - "p99": 0.06469680041074753 - } - }, - "draw_calls_avg": 931.1279533599263, - "vertices_avg": 1897147.3936790426, - "chunks_rendered_avg": 301.56274930960416, - "worst_frame": { - "frame_index": 1377, - "frame_ms": 5.62113618850708, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.03703999999990515, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.03461500000000228, - "lod_worker_generation_ms": 9.874601999999868, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 1111872, - "lod_visible_count": 19, - "lod_rejected_count": 27, - "lod_coverage_count": 19, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 3259, - "cpu_frame_ms": { - "min": 0.0012120000319555402, - "avg": 0.2194801272055544, - "max": 2.829077959060669, - "p1": 0.0014882199722342192, - "p5": 0.0017430000007152557, - "p50": 0.18909500539302826, - "p95": 0.551383090019226, - "p99": 0.7109371042251592 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1024275175877311, - "max": 0.22784000635147095, - "p1": 0, - "p5": 0, - "p50": 0.15967999398708344, - "p95": 0.219300003349781, - "p99": 0.22497679710388185 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 637.4214729999999, - "avg_ms": 0.19558805553850872 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.134476, - "avg_ms": 4.1262964099417005e-5 - }, - "state_transition": { - "total_ms": 0.307413, - "avg_ms": 9.43274010432648e-5 - }, - "upload_prep": { - "total_ms": 0.4092380000000002, - "avg_ms": 0.00012557164774470704 - }, - "upload_submission": { - "total_ms": 4.779955000000001, - "avg_ms": 0.0014666937710954284 - }, - "visibility": { - "total_ms": 38.155923, - "avg_ms": 0.011707862227677201 - }, - "coverage": { - "total_ms": 31.72721999999999, - "avg_ms": 0.009735262350414234 - }, - "eviction": { - "total_ms": 4.247756, - "avg_ms": 0.001303392451672292 - }, - "manager_lock_wait": { - "total_ms": 1.9466160000000006, - "avg_ms": 0.0005973046946916233 - }, - "manager_lock_hold": { - "total_ms": 304.430781, - "avg_ms": 0.09341232924209882 - } - }, - "workers": { - "generation_total_ms": 811.7794739999999, - "mesh_construction_total_ms": 88.25070399999998, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.056096000000000035 - }, - "memory_bytes": { - "upload_total_bytes": 27135040, - "upload_avg_bytes": 8326.18594660939, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 23104, - "pending_cpu_upload_bytes": { - "avg_bytes": 1791.0966554157717, - "max_bytes": 533184, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 432550.9616446763, - "max_bytes": 6091584, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 432550.9616446763, - "max_bytes": 6091584, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 77270.48542497699, - "max_bytes": 2054400, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 26131250.066891685, - "max_bytes": 41698432, - "last_bytes": 22004864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 108086477.93310831, - "max_bytes": 113180928, - "last_bytes": 112212864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 174988.51917766186, - "max_bytes": 184832, - "last_bytes": 184832, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66933875.48082234, - "max_bytes": 66947136, - "last_bytes": 66924032, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 218909507.66155264, - "max_bytes": 229328446, - "last_bytes": 216775003, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67187925.5820804, - "max_bytes": 69163264, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67627456, - "p99_bytes": 69101632 - }, - "logical_vram_bytes": { - "avg_bytes": 201759142.96164468, - "max_bytes": 207418176, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 203407488, - "p99_bytes": 204812160 - } - }, - "visibility": { - "visible_total": 40635, - "rejected_total": 55043, - "coverage_total": 40652, - "levels": [ - { - "candidates": 24486, - "accepted": 12483, - "rejected_no_draw": 0, - "rejected_not_ready": 105, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11882, - "rejected_chunk_coverage": 16, - "coverage_checks": 12499 - }, - { - "candidates": 18360, - "accepted": 2047, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 16312, - "rejected_chunk_coverage": 1, - "coverage_checks": 2048 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 52832, - "accepted": 26105, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 26727, - "rejected_chunk_coverage": 0, - "coverage_checks": 26105 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 172000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002114295959473, - "upload_total_bytes": 147120448, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2155.26624, - "worker_mesh_construction_total_ms": 273.3304, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.447836, - "compact_submissions": 119790, - "pool_gpu_allocated_bytes": 41698432, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001491546630859, - "requested_duration_s": 5, - "sampled_frame_count": 3259, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 2009, - "duration_s": 5, - "fps": { - "min": 223.85848999023438, - "avg": 401.6403444142845, - "max": 1015.2274169921875, - "p1": 248.02737121582032, - "p5": 270.9326477050781, - "p50": 411.0755310058594, - "p95": 702.18359375, - "p99": 752.1536865234378 - }, - "frame_ms": { - "min": 0.9850009679794312, - "avg": 2.4897897183568753, - "max": 4.467107772827148, - "p1": 1.3295181703567505, - "p5": 1.4241289377212523, - "p50": 2.432642936706543, - "p95": 3.6909543037414547, - "p99": 4.031816406250001 - }, - "max_frame_ms": 4.467107772827148, - "cpu_ms_avg": 2.4897897183568753, - "gpu_ms": { - "shadow_avg": 0.13291883044363909, - "opaque_avg": 0.6351991438818192, - "lod_terrain_avg": 0.16139701346650298, - "lod_water_avg": 0.009704211054021198, - "lod_culling_avg": 0, - "total_avg": 2.018054122056102, - "total": { - "min": 1.4861600399017334, - "avg": 2.018054122056102, - "max": 2.6946799755096436, - "p1": 1.50306875705719, - "p5": 1.5230079889297485, - "p50": 1.9539600610733032, - "p95": 2.6447840213775633, - "p99": 2.671775856018067 - }, - "lod_compact_terrain": { - "min": 0.12272000312805176, - "avg": 0.16123305135413507, - "max": 0.19728000462055206, - "p1": 0.1284223985671997, - "p5": 0.13671200275421141, - "p50": 0.15992000699043274, - "p95": 0.1901840031147003, - "p99": 0.19395999610424042 - }, - "lod_compact_water": { - "min": 0.002319999970495701, - "avg": 0.009539472369288014, - "max": 0.050439998507499695, - "p1": 0.002520000096410513, - "p5": 0.0026400000788271427, - "p50": 0.004000000189989805, - "p95": 0.02468799985945224, - "p99": 0.04863360151648522 - } - }, - "draw_calls_avg": 1790.0572424091588, - "vertices_avg": 1618178.7058237928, - "chunks_rendered_avg": 301.21005475360874, - "worst_frame": { - "frame_index": 285, - "frame_ms": 4.467107772827148, - "gpu_total_ms": 2.6256799697875977, - "gpu_lod_terrain_ms": 0.1451999992132187, - "gpu_lod_water_ms": 0.019360000267624855, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8001199960708618, - "lod_cpu_ms": 0.029365000000083796, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.024896999999995728, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 18661440, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 36, - "lod_rejected_count": 57, - "lod_coverage_count": 36, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 10 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2009, - "cpu_frame_ms": { - "min": 0.019857000559568405, - "avg": 0.210660129873617, - "max": 0.915090024471283, - "p1": 0.021361720710992814, - "p5": 0.022857000306248668, - "p50": 0.29107800126075745, - "p95": 0.47116759419441223, - "p99": 0.5220105123519898 - }, - "gpu_frame_ms": { - "min": 0.14524000883102417, - "avg": 0.17110122469540792, - "max": 0.20027999579906464, - "p1": 0.14652320325374604, - "p5": 0.15007199645042418, - "p50": 0.17127999663352966, - "p95": 0.19334400296211243, - "p99": 0.19699681103229522 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 348.91114400000004, - "avg_ms": 0.17367403882528623 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.11431900000000006, - "avg_ms": 5.6903434544549555e-5 - }, - "upload_prep": { - "total_ms": 1.757701, - "avg_ms": 0.0008749133897461423 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 51.83161399999999, - "avg_ms": 0.025799708312593324 - }, - "coverage": { - "total_ms": 40.322633999999994, - "avg_ms": 0.0200709975111996 - }, - "eviction": { - "total_ms": 1.8806820000000002, - "avg_ms": 0.0009361284221005476 - }, - "manager_lock_wait": { - "total_ms": 1.0999790000000003, - "avg_ms": 0.0005475256346441017 - }, - "manager_lock_hold": { - "total_ms": 349.08679099999995, - "avg_ms": 0.173761468889995 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 18661440, - "max_bytes": 18661440, - "last_bytes": 18661440, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51616576, - "max_bytes": 51616576, - "last_bytes": 51616576, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 82601152, - "max_bytes": 82601152, - "last_bytes": 82601152, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 266939011, - "max_bytes": 266939011, - "last_bytes": 266939011, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 85770304, - "max_bytes": 85770304, - "last_bytes": 85770304, - "p50_bytes": 85770304, - "p95_bytes": 85770304, - "p99_bytes": 85770304 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 68745, - "rejected_total": 118092, - "coverage_total": 68745, - "levels": [ - { - "candidates": 76342, - "accepted": 28248, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 48094, - "rejected_chunk_coverage": 0, - "coverage_checks": 28248 - }, - { - "candidates": 54243, - "accepted": 13852, - "rejected_no_draw": 0, - "rejected_not_ready": 20090, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20301, - "rejected_chunk_coverage": 0, - "coverage_checks": 13852 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 56252, - "accepted": 26645, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 29607, - "rejected_chunk_coverage": 0, - "coverage_checks": 26645 - } - ] - }, - "pressure": { - "staging_pressure_total": 20090, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 160000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002923965454102, - "upload_total_bytes": 138864640, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2666.261265, - "worker_mesh_construction_total_ms": 305.255347, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.591241, - "compact_submissions": 106710, - "pool_gpu_allocated_bytes": 51616576, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001987457275391, - "requested_duration_s": 5, - "sampled_frame_count": 2009, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 2679, - "duration_s": 5, - "fps": { - "min": 32.680442810058594, - "avg": 535.7636935722855, - "max": 11037.52734375, - "p1": 287.34007568359374, - "p5": 311.83722534179685, - "p50": 451.15032958984375, - "p95": 6335.624169921866, - "p99": 8676.557753906245 - }, - "frame_ms": { - "min": 0.09059999883174896, - "avg": 1.8664945236067578, - "max": 30.599340438842773, - "p1": 0.11525320217013359, - "p5": 0.15784060359001162, - "p50": 2.2165560722351074, - "p95": 3.2068012237548826, - "p99": 3.4802083206176726 - }, - "max_frame_ms": 30.599340438842773, - "cpu_ms_avg": 1.8664945236067578, - "gpu_ms": { - "shadow_avg": 0.05831933567476344, - "opaque_avg": 0.5555913848812877, - "lod_terrain_avg": 0.10050563651063008, - "lod_water_avg": 0.002796386699263492, - "lod_culling_avg": 0, - "total_avg": 1.4283642033313184, - "total": { - "min": 0, - "avg": 1.4283642033313184, - "max": 2.3164000511169434, - "p1": 0, - "p5": 0, - "p50": 2.2576799392700195, - "p95": 2.290924000740051, - "p99": 2.2998303079605105 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.10039810371870601, - "max": 0.1578799933195114, - "p1": 0, - "p5": 0, - "p50": 0.15163999795913696, - "p95": 0.15323999524116516, - "p99": 0.15372000634670258 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.0026889735025942283, - "max": 0.005239999853074551, - "p1": 0, - "p5": 0, - "p50": 0.004040000028908253, - "p95": 0.004480000119656324, - "p99": 0.004720000084489584 - } - }, - "draw_calls_avg": 1154.3131765584174, - "vertices_avg": 1520312.477043673, - "chunks_rendered_avg": 292.88465845464725, - "worst_frame": { - "frame_index": 1485, - "frame_ms": 30.599340438842773, - "gpu_total_ms": 2.2994399070739746, - "gpu_lod_terrain_ms": 0.15328000485897064, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8886399865150452, - "lod_cpu_ms": 4.8241689999999835, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 4.428445000000001, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 10075968, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 5 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2679, - "cpu_frame_ms": { - "min": 0.0016639999812468886, - "avg": 0.22166922859848068, - "max": 4.824169158935547, - "p1": 0.002211800077930093, - "p5": 0.00253499997779727, - "p50": 0.1657020002603531, - "p95": 0.527750027179718, - "p99": 0.689683824777603 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1033020231618326, - "max": 0.1624400019645691, - "p1": 0, - "p5": 0, - "p50": 0.15616001188755035, - "p95": 0.1578799933195114, - "p99": 0.1584400087594986 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 510.912638, - "avg_ms": 0.19071020455393803 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.043418999999999985, - "avg_ms": 1.6207166853303464e-5 - }, - "state_transition": { - "total_ms": 0.19633899999999993, - "avg_ms": 7.328816722657705e-5 - }, - "upload_prep": { - "total_ms": 1.7156190000000002, - "avg_ms": 0.0006403952967525197 - }, - "upload_submission": { - "total_ms": 4.645470000000003, - "avg_ms": 0.0017340313549832039 - }, - "visibility": { - "total_ms": 46.282848, - "avg_ms": 0.017276165733482642 - }, - "coverage": { - "total_ms": 36.42455100000001, - "avg_ms": 0.013596323628219487 - }, - "eviction": { - "total_ms": 7.439381000000001, - "avg_ms": 0.00277692459873087 - }, - "manager_lock_wait": { - "total_ms": 1.2448480000000002, - "avg_ms": 0.0004646689063083241 - }, - "manager_lock_hold": { - "total_ms": 312.600053, - "avg_ms": 0.11668535013064577 - } - }, - "workers": { - "generation_total_ms": 385.735357, - "mesh_construction_total_ms": 35.43381700000003, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.23533199999999999 - }, - "memory_bytes": { - "upload_total_bytes": 25886608, - "upload_avg_bytes": 9662.787607316162, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 75088, - "pending_cpu_upload_bytes": { - "avg_bytes": 9008956.010451661, - "max_bytes": 10578432, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 535383.0055991041, - "max_bytes": 51515328, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 535383.0055991041, - "max_bytes": 51515328, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 19301.339305711088, - "max_bytes": 1067520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 48041091.98954834, - "max_bytes": 56271824, - "last_bytes": 10937744, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 86176636.01045166, - "max_bytes": 123413616, - "last_bytes": 123279984, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 194473.75886524824, - "max_bytes": 236816, - "last_bytes": 236816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66914390.241134755, - "max_bytes": 66947136, - "last_bytes": 66872048, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 236048618.23366928, - "max_bytes": 252037164, - "last_bytes": 208200443, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 76137121.34975737, - "max_bytes": 77687296, - "last_bytes": 67108864, - "p50_bytes": 77184832, - "p95_bytes": 77184832, - "p99_bytes": 77184832 - }, - "logical_vram_bytes": { - "avg_bytes": 201861975.0055991, - "max_bytes": 252841920, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201846912, - "p99_bytes": 212408832 - } - }, - "visibility": { - "visible_total": 62869, - "rejected_total": 84528, - "coverage_total": 62871, - "levels": [ - { - "candidates": 61498, - "accepted": 24933, - "rejected_no_draw": 0, - "rejected_not_ready": 33, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 36530, - "rejected_chunk_coverage": 2, - "coverage_checks": 24935 - }, - { - "candidates": 32675, - "accepted": 7425, - "rejected_no_draw": 0, - "rejected_not_ready": 7430, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 17820, - "rejected_chunk_coverage": 0, - "coverage_checks": 7425 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 53224, - "accepted": 30511, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 22713, - "rejected_chunk_coverage": 0, - "coverage_checks": 30511 - } - ] - }, - "pressure": { - "staging_pressure_total": 11970, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 164882 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000948905944824, - "upload_total_bytes": 133665664, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2461.144028, - "worker_mesh_construction_total_ms": 302.198718, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.422658, - "compact_submissions": 103860, - "pool_gpu_allocated_bytes": 51677056, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.0003437995910645, - "requested_duration_s": 5, - "sampled_frame_count": 2679, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - }, - "high": { - "stationary": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 1176, - "duration_s": 5, - "fps": { - "min": 180.67156982421875, - "avg": 235.0108824301382, - "max": 321.070068359375, - "p1": 198.10243225097656, - "p5": 204.06834411621094, - "p50": 234.00827026367188, - "p95": 270.5585174560547, - "p99": 283.3115692138672 - }, - "frame_ms": { - "min": 3.1145851612091064, - "avg": 4.255122101834031, - "max": 5.534904956817627, - "p1": 3.529685080051422, - "p5": 3.69605815410614, - "p50": 4.273353576660156, - "p95": 4.9003190994262695, - "p99": 5.047894358634949 - }, - "max_frame_ms": 5.534904956817627, - "cpu_ms_avg": 4.255122101834031, - "gpu_ms": { - "shadow_avg": 0.14965712605994574, - "opaque_avg": 1.6747419381020021, - "lod_terrain_avg": 0.3585533676352225, - "lod_water_avg": 0.004667074827011675, - "lod_culling_avg": 0, - "total_avg": 3.6897719684912236, - "total": { - "min": 3.6471199989318848, - "avg": 3.6897719684912236, - "max": 3.7381601333618164, - "p1": 3.662030041217804, - "p5": 3.6687899231910706, - "p50": 3.6887999773025513, - "p95": 3.7144100666046143, - "p99": 3.7256199717521667 - }, - "lod_compact_terrain": { - "min": 0.3529999852180481, - "avg": 0.35838955830858676, - "max": 0.3654400110244751, - "p1": 0.3549000024795532, - "p5": 0.3557500094175339, - "p50": 0.3584800064563751, - "p95": 0.3608799874782562, - "p99": 0.36176998913288116 - }, - "lod_compact_water": { - "min": 0.003800000064074993, - "avg": 0.00450244897677164, - "max": 0.005799999926239252, - "p1": 0.003920000046491623, - "p5": 0.004040000028908253, - "p50": 0.004480000119656324, - "p95": 0.005040000192821026, - "p99": 0.005319999996572733 - } - }, - "draw_calls_avg": 2223, - "vertices_avg": 1926852, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 249, - "frame_ms": 5.534904956817627, - "gpu_total_ms": 3.676759958267212, - "gpu_lod_terrain_ms": 0.358599990606308, - "gpu_lod_water_ms": 0.00443999981507659, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.670240044593811, - "lod_cpu_ms": 0.058519999999901984, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.047449000000000296, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 40818624, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 28, - "lod_rejected_count": 57, - "lod_coverage_count": 28, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1176, - "cpu_frame_ms": { - "min": 0.03574699908494949, - "avg": 0.24098753586702912, - "max": 0.6619240045547485, - "p1": 0.037187001667916775, - "p5": 0.03833224903792143, - "p50": 0.2131355032324791, - "p95": 0.5194087624549866, - "p99": 0.5639000087976456 - }, - "gpu_frame_ms": { - "min": 0.35731998085975647, - "avg": 0.3632204429734321, - "max": 0.3702000081539154, - "p1": 0.3596699982881546, - "p5": 0.36052000522613525, - "p50": 0.3632799983024597, - "p95": 0.36577001959085464, - "p99": 0.36653000861406326 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 220.78726700000004, - "avg_ms": 0.18774427465986399 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.05986199999999986, - "avg_ms": 5.0903061224489675e-5 - }, - "upload_prep": { - "total_ms": 2.223355999999999, - "avg_ms": 0.0018906088435374142 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 46.287925, - "avg_ms": 0.039360480442176875 - }, - "coverage": { - "total_ms": 40.198766000000006, - "avg_ms": 0.03418262414965987 - }, - "eviction": { - "total_ms": 1.1708609999999986, - "avg_ms": 0.0009956301020408152 - }, - "manager_lock_wait": { - "total_ms": 0.648231, - "avg_ms": 0.0005512168367346939 - }, - "manager_lock_hold": { - "total_ms": 216.267701, - "avg_ms": 0.183901106292517 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 40818624, - "max_bytes": 40818624, - "last_bytes": 40818624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 66416512, - "max_bytes": 66416512, - "last_bytes": 66416512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 101355648, - "max_bytes": 101355648, - "last_bytes": 101355648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379854763, - "max_bytes": 379854763, - "last_bytes": 379854763, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 141481920, - "max_bytes": 141481920, - "last_bytes": 141481920, - "p50_bytes": 141481920, - "p95_bytes": 141481920, - "p99_bytes": 141481920 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 32928, - "rejected_total": 67032, - "coverage_total": 32928, - "levels": [ - { - "candidates": 2352, - "accepted": 1176, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1176, - "rejected_chunk_coverage": 0, - "coverage_checks": 1176 - }, - { - "candidates": 45864, - "accepted": 4704, - "rejected_no_draw": 0, - "rejected_not_ready": 25872, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15288, - "rejected_chunk_coverage": 0, - "coverage_checks": 4704 - }, - { - "candidates": 18816, - "accepted": 9408, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 9408, - "rejected_chunk_coverage": 0, - "coverage_checks": 9408 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 32928, - "accepted": 17640, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15288, - "rejected_chunk_coverage": 0, - "coverage_checks": 17640 - } - ] - }, - "pressure": { - "staging_pressure_total": 25872, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 98700 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003300666809082, - "upload_total_bytes": 305257792, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5412.615515, - "worker_mesh_construction_total_ms": 624.595113, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.386749, - "compact_submissions": 63420, - "pool_gpu_allocated_bytes": 66416512, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.004021167755127, - "requested_duration_s": 5, - "sampled_frame_count": 1176, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "traversal": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 2541, - "duration_s": 5, - "fps": { - "min": 44.96759796142578, - "avg": 507.83055899871556, - "max": 11475.390625, - "p1": 207.59676513671877, - "p5": 222.84242248535156, - "p50": 1198.6556396484375, - "p95": 8959.7705078125, - "p99": 10636.897070312498 - }, - "frame_ms": { - "min": 0.08714300394058228, - "avg": 1.9691607412749836, - "max": 22.23823356628418, - "p1": 0.09401239901781082, - "p5": 0.11161000281572342, - "p50": 0.8342679738998413, - "p95": 4.487475872039795, - "p99": 4.8170310974121096 - }, - "max_frame_ms": 22.23823356628418, - "cpu_ms_avg": 1.9691607412749836, - "gpu_ms": { - "shadow_avg": 0.08502999994286586, - "opaque_avg": 0.5742008976934464, - "lod_terrain_avg": 0.16234521846375283, - "lod_water_avg": 0.019463250679079412, - "lod_culling_avg": 0, - "total_avg": 1.5101331068464547, - "total": { - "min": 0, - "avg": 1.5101331068464547, - "max": 3.9170799255371094, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.736599922180176, - "p99": 3.809071922302246 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.16227504132489246, - "max": 0.4055199921131134, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3958800137042999, - "p99": 0.40315199494361875 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.019392144839623513, - "max": 0.0868000015616417, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.07620000094175339, - "p99": 0.07995199859142302 - } - }, - "draw_calls_avg": 929.7233372687919, - "vertices_avg": 2305162.3636363638, - "chunks_rendered_avg": 370.60881542699724, - "worst_frame": { - "frame_index": 1187, - "frame_ms": 22.23823356628418, - "gpu_total_ms": 3.6388001441955566, - "gpu_lod_terrain_ms": 0.3967599868774414, - "gpu_lod_water_ms": 0.07767999917268753, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.3217999935150146, - "lod_cpu_ms": 0.5838380000000143, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.4780479999999443, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43783488, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 17, - "lod_rejected_count": 66, - "lod_coverage_count": 17, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2541, - "cpu_frame_ms": { - "min": 0.003025999991223216, - "avg": 0.29364178042594213, - "max": 19.009004592895508, - "p1": 0.003527000080794096, - "p5": 0.003976999782025814, - "p50": 0.27960601449012756, - "p95": 0.6701599955558777, - "p99": 0.8939743995666503 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1818084691932285, - "max": 0.4883599877357483, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.46855998039245605, - "p99": 0.4819999933242798 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 621.2910639999999, - "avg_ms": 0.24450651869342774 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.1173519999999999, - "avg_ms": 4.618339236521051e-5 - }, - "upload_prep": { - "total_ms": 4.939024999999999, - "avg_ms": 0.0019437327823691457 - }, - "upload_submission": { - "total_ms": 38.38460600000002, - "avg_ms": 0.01510610232192051 - }, - "visibility": { - "total_ms": 50.45694499999999, - "avg_ms": 0.019857121212121208 - }, - "coverage": { - "total_ms": 45.441856999999985, - "avg_ms": 0.01788345415190869 - }, - "eviction": { - "total_ms": 2.570843, - "avg_ms": 0.0010117445887445886 - }, - "manager_lock_wait": { - "total_ms": 0.8730919999999998, - "avg_ms": 0.0003436017316017315 - }, - "manager_lock_hold": { - "total_ms": 221.684642, - "avg_ms": 0.0872430704447068 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 71534208, - "upload_avg_bytes": 28151.990554899647, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 47359590.309327036, - "max_bytes": 52910976, - "last_bytes": 43783488, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 107377.49232585596, - "max_bytes": 4218240, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 107377.49232585596, - "max_bytes": 4218240, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 62535596.051948056, - "max_bytes": 65715136, - "last_bytes": 61726528, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 105236563.94805194, - "max_bytes": 107769408, - "last_bytes": 106045632, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 388237395.03974813, - "max_bytes": 399279492, - "last_bytes": 382168405, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 148022886.30932704, - "max_bytes": 153574272, - "last_bytes": 144446784, - "p50_bytes": 144446784, - "p95_bytes": 153574272, - "p99_bytes": 153574272 - }, - "logical_vram_bytes": { - "avg_bytes": 234988401.49232584, - "max_bytes": 239099264, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 235434368, - "p99_bytes": 237107456 - } - }, - "visibility": { - "visible_total": 22997, - "rejected_total": 72006, - "coverage_total": 22999, - "levels": [ - { - "candidates": 2621, - "accepted": 1371, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1250, - "rejected_chunk_coverage": 0, - "coverage_checks": 1371 - }, - { - "candidates": 46476, - "accepted": 3712, - "rejected_no_draw": 0, - "rejected_not_ready": 27344, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15418, - "rejected_chunk_coverage": 2, - "coverage_checks": 3714 - }, - { - "candidates": 15302, - "accepted": 3307, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11995, - "rejected_chunk_coverage": 0, - "coverage_checks": 3307 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30604, - "accepted": 14607, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15997, - "rejected_chunk_coverage": 0, - "coverage_checks": 14607 - } - ] - }, - "pressure": { - "staging_pressure_total": 63562, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 101904 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00096321105957, - "upload_total_bytes": 294608896, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5368.528002, - "worker_mesh_construction_total_ms": 624.550607, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.439669, - "compact_submissions": 72690, - "pool_gpu_allocated_bytes": 65715136, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.0036396980285645, - "requested_duration_s": 5, - "sampled_frame_count": 2541, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "rapid-turn": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 1374, - "duration_s": 5, - "fps": { - "min": 170.5203094482422, - "avg": 274.6797719867, - "max": 545.2063598632812, - "p1": 185.25044860839841, - "p5": 196.55439605712888, - "p50": 276.25950622558594, - "p95": 429.8601562499999, - "p99": 474.99169616699214 - }, - "frame_ms": { - "min": 1.8341679573059082, - "avg": 3.640602992958724, - "max": 5.864404201507568, - "p1": 2.1053041982650758, - "p5": 2.3263444542884826, - "p50": 3.619784951210022, - "p95": 5.087650108337402, - "p99": 5.398097462654114 - }, - "max_frame_ms": 5.864404201507568, - "cpu_ms_avg": 3.640602992958724, - "gpu_ms": { - "shadow_avg": 0.21849913423368503, - "opaque_avg": 1.2221394766886682, - "lod_terrain_avg": 0.40564864594082645, - "lod_water_avg": 0.011743609910577352, - "lod_culling_avg": 0, - "total_avg": 3.317318278629186, - "total": { - "min": 2.3347201347351074, - "avg": 3.317318278629186, - "max": 4.500400066375732, - "p1": 2.3534930992126464, - "p5": 2.416252017021179, - "p50": 3.237239956855774, - "p95": 4.40301034450531, - "p99": 4.4709290027618405 - }, - "lod_compact_terrain": { - "min": 0.3062399923801422, - "avg": 0.4054847453142011, - "max": 0.5277600288391113, - "p1": 0.31024759948253633, - "p5": 0.3210059881210327, - "p50": 0.39573998749256134, - "p95": 0.5035880029201507, - "p99": 0.5217264074087142 - }, - "lod_compact_water": { - "min": 0.002360000042244792, - "avg": 0.011578457059577281, - "max": 0.07716000080108643, - "p1": 0.002520000096410513, - "p5": 0.0026400000788271427, - "p50": 0.004140000091865659, - "p95": 0.03337400015443563, - "p99": 0.06188279829919335 - } - }, - "draw_calls_avg": 2244.6062590975253, - "vertices_avg": 1960022.1004366812, - "chunks_rendered_avg": 375.4912663755459, - "worst_frame": { - "frame_index": 256, - "frame_ms": 5.864404201507568, - "gpu_total_ms": 4.295919895172119, - "gpu_lod_terrain_ms": 0.4960800111293793, - "gpu_lod_water_ms": 0.024480000138282776, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5948400497436523, - "lod_cpu_ms": 0.05737699999995982, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.05168700000000115, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43378560, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 27, - "lod_rejected_count": 56, - "lod_coverage_count": 27, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1374, - "cpu_frame_ms": { - "min": 0.030466999858617783, - "avg": 0.2391349640898569, - "max": 0.6051470041275024, - "p1": 0.033031199164688586, - "p5": 0.038061998784542084, - "p50": 0.20986950770020485, - "p95": 0.5276125103235245, - "p99": 0.5625341874361038 - }, - "gpu_frame_ms": { - "min": 0.30928000807762146, - "avg": 0.4173922556326379, - "max": 0.5557999610900879, - "p1": 0.31321839451789857, - "p5": 0.3246640026569367, - "p50": 0.3997200131416321, - "p95": 0.5300800204277039, - "p99": 0.5499715882539749 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 255.04924000000005, - "avg_ms": 0.1856253566229986 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.07941500000000001, - "avg_ms": 5.779839883551675e-5 - }, - "upload_prep": { - "total_ms": 2.594778, - "avg_ms": 0.001888484716157205 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 55.38022499999998, - "avg_ms": 0.0403058406113537 - }, - "coverage": { - "total_ms": 49.052981999999986, - "avg_ms": 0.03570086026200872 - }, - "eviction": { - "total_ms": 1.3640599999999985, - "avg_ms": 0.0009927656477438126 - }, - "manager_lock_wait": { - "total_ms": 0.7176900000000002, - "avg_ms": 0.0005223362445414848 - }, - "manager_lock_hold": { - "total_ms": 255.65183199999996, - "avg_ms": 0.18606392430858804 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 43378560, - "max_bytes": 43378560, - "last_bytes": 43378560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 59821696, - "max_bytes": 59821696, - "last_bytes": 59821696, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 107950464, - "max_bytes": 107950464, - "last_bytes": 107950464, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379888149, - "max_bytes": 379888149, - "last_bytes": 379888149, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 144041856, - "max_bytes": 144041856, - "last_bytes": 144041856, - "p50_bytes": 144041856, - "p95_bytes": 144041856, - "p99_bytes": 144041856 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 36687, - "rejected_total": 77355, - "coverage_total": 36687, - "levels": [ - { - "candidates": 2748, - "accepted": 918, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1830, - "rejected_chunk_coverage": 0, - "coverage_checks": 918 - }, - { - "candidates": 54960, - "accepted": 8094, - "rejected_no_draw": 0, - "rejected_not_ready": 31602, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15264, - "rejected_chunk_coverage": 0, - "coverage_checks": 8094 - }, - { - "candidates": 17862, - "accepted": 9437, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 8425, - "rejected_chunk_coverage": 0, - "coverage_checks": 9437 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 38472, - "accepted": 18238, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20234, - "rejected_chunk_coverage": 0, - "coverage_checks": 18238 - } - ] - }, - "pressure": { - "staging_pressure_total": 31602, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 108416 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000357627868652, - "upload_total_bytes": 329082112, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5372.631438, - "worker_mesh_construction_total_ms": 623.011737, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.447592, - "compact_submissions": 71940, - "pool_gpu_allocated_bytes": 59821696, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.002184867858887, - "requested_duration_s": 5, - "sampled_frame_count": 1374, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "teleport-eviction": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 2416, - "duration_s": 5, - "fps": { - "min": 19.90268325805664, - "avg": 483.0753789457763, - "max": 11064.39453125, - "p1": 205.51299819946288, - "p5": 212.64580917358398, - "p50": 1327.73681640625, - "p95": 7603.880859375, - "p99": 10025.670312499999 - }, - "frame_ms": { - "min": 0.09038000553846359, - "avg": 2.0700703111434025, - "max": 50.24448013305664, - "p1": 0.09974399842321872, - "p5": 0.13151249662041664, - "p50": 0.7531654834747314, - "p95": 4.702655792236328, - "p99": 4.865872168540954 - }, - "max_frame_ms": 50.24448013305664, - "cpu_ms_avg": 2.0700703111434025, - "gpu_ms": { - "shadow_avg": 0.06340334434617335, - "opaque_avg": 0.7087717379955266, - "lod_terrain_avg": 0.15568531466191574, - "lod_water_avg": 0.002088377484088048, - "lod_culling_avg": 0, - "total_avg": 1.5621965738716503, - "total": { - "min": 0, - "avg": 1.5621965738716503, - "max": 3.7239999771118164, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.69569993019104, - "p99": 3.7053137063980106 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.15561342721693958, - "max": 0.3646000027656555, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3604399859905243, - "p99": 0.36127999424934387 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.002016258277214866, - "max": 0.005880000069737434, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.005119999870657921, - "p99": 0.005439999978989363 - } - }, - "draw_calls_avg": 968.1618377483444, - "vertices_avg": 1899477.690397351, - "chunks_rendered_avg": 366.90149006622516, - "worst_frame": { - "frame_index": 948, - "frame_ms": 50.24448013305664, - "gpu_total_ms": 3.7047996520996094, - "gpu_lod_terrain_ms": 0.3583199977874756, - "gpu_lod_water_ms": 0.005280000157654285, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6794400215148926, - "lod_cpu_ms": 3.5608640000000378, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 3.2002560000000013, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 45507264, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 24 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2416, - "cpu_frame_ms": { - "min": 0.0023539999965578318, - "avg": 0.24607392386115073, - "max": 4.519947052001953, - "p1": 0.002644999884068966, - "p5": 0.0029257499263621867, - "p50": 0.23351449519395828, - "p95": 0.5540564954280853, - "p99": 0.7593400090932846 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1577736922101864, - "max": 0.36980000138282776, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3654400110244751, - "p99": 0.36636000871658325 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 498.1758249999999, - "avg_ms": 0.20619860306291388 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.01467400000000002, - "avg_ms": 6.0736754966887505e-6 - }, - "state_transition": { - "total_ms": 0.1657129999999999, - "avg_ms": 6.858981788079466e-5 - }, - "upload_prep": { - "total_ms": 4.0039940000000005, - "avg_ms": 0.0016572822847682122 - }, - "upload_submission": { - "total_ms": 13.20260399999998, - "avg_ms": 0.005464653973509925 - }, - "visibility": { - "total_ms": 49.31938199999999, - "avg_ms": 0.02041365149006622 - }, - "coverage": { - "total_ms": 42.780249999999995, - "avg_ms": 0.017707057119205297 - }, - "eviction": { - "total_ms": 8.293956000000001, - "avg_ms": 0.0034329288079470205 - }, - "manager_lock_wait": { - "total_ms": 0.8972069999999999, - "avg_ms": 0.00037136051324503306 - }, - "manager_lock_hold": { - "total_ms": 216.54849299999995, - "avg_ms": 0.08963099875827812 - } - }, - "workers": { - "generation_total_ms": 526.6374000000005, - "mesh_construction_total_ms": 54.94961599999999, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.11070800000000003 - }, - "memory_bytes": { - "upload_total_bytes": 77068048, - "upload_avg_bytes": 31899.026490066226, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 75088, - "pending_cpu_upload_bytes": { - "avg_bytes": 43605108.556291394, - "max_bytes": 45507264, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 1087210.6490066226, - "max_bytes": 36193344, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 1087210.6490066226, - "max_bytes": 36193344, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 68180585.01986755, - "max_bytes": 76949456, - "last_bytes": 63628304, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99591574.98013245, - "max_bytes": 122308400, - "last_bytes": 104143856, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 164678.15894039735, - "max_bytes": 236816, - "last_bytes": 236816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66944185.8410596, - "max_bytes": 66947136, - "last_bytes": 66872048, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 371979232.5273179, - "max_bytes": 389349230, - "last_bytes": 306839754, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 144268404.5562914, - "max_bytes": 146170560, - "last_bytes": 100663296, - "p50_bytes": 146170560, - "p95_bytes": 146170560, - "p99_bytes": 146170560 - }, - "logical_vram_bytes": { - "avg_bytes": 235968234.64900663, - "max_bytes": 271074368, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 271074368 - } - }, - "visibility": { - "visible_total": 43318, - "rejected_total": 55724, - "coverage_total": 43318, - "levels": [ - { - "candidates": 12228, - "accepted": 8118, - "rejected_no_draw": 0, - "rejected_not_ready": 48, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 4062, - "rejected_chunk_coverage": 0, - "coverage_checks": 8118 - }, - { - "candidates": 39154, - "accepted": 8605, - "rejected_no_draw": 0, - "rejected_not_ready": 22908, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 7641, - "rejected_chunk_coverage": 0, - "coverage_checks": 8605 - }, - { - "candidates": 16880, - "accepted": 9296, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 7584, - "rejected_chunk_coverage": 0, - "coverage_checks": 9296 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30780, - "accepted": 17299, - "rejected_no_draw": 0, - "rejected_not_ready": 19, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 13462, - "rejected_chunk_coverage": 0, - "coverage_checks": 17299 - } - ] - }, - "pressure": { - "staging_pressure_total": 55520, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 98258 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000935554504395, - "upload_total_bytes": 300926272, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5417.338126, - "worker_mesh_construction_total_ms": 636.438225, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.886576, - "compact_submissions": 63660, - "pool_gpu_allocated_bytes": 68260864, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001282215118408, - "requested_duration_s": 5, - "sampled_frame_count": 2416, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - } - } - }, - "compact_pairs": { - "low": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 5772, - "duration_s": 5, - "fps": { - "min": 715.5752563476562, - "avg": 1154.2138505845442, - "max": 2216.911376953125, - "p1": 833.5297723388671, - "p5": 913.1731872558594, - "p50": 1080.9522705078125, - "p95": 1527.0253051757813, - "p99": 1656.7973486328124 - }, - "frame_ms": { - "min": 0.45107799768447876, - "avg": 0.8663905735436778, - "max": 1.3974770307540894, - "p1": 0.6035740888118744, - "p5": 0.6548680037260055, - "p50": 0.9251140058040619, - "p95": 1.095082449913025, - "p99": 1.1997172367572784 - }, - "max_frame_ms": 1.3974770307540894, - "cpu_ms_avg": 0.8663905735436778, - "gpu_ms": { - "shadow_avg": 0.0003596136512460135, - "opaque_avg": 0.45659252920666255, - "lod_terrain_avg": 0.06187234931365327, - "lod_water_avg": 0.007514871794285739, - "lod_culling_avg": 0, - "total_avg": 0.660508621930663, - "total": { - "min": 0.6561999917030334, - "avg": 0.660508621930663, - "max": 0.6714000105857849, - "p1": 0.6575600928068162, - "p5": 0.6583600640296936, - "p50": 0.660319983959198, - "p95": 0.6632800698280334, - "p99": 0.6654000282287598 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 463, - "vertices_avg": 716592, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 4631, - "frame_ms": 1.3974770307540894, - "gpu_total_ms": 0.6601600050926208, - "gpu_lod_terrain_ms": 0.06191999837756157, - "gpu_lod_water_ms": 0.007400000002235174, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45631998777389526, - "lod_cpu_ms": 0.01540999999983228, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.012865000000005011, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41464128, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 43, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 5772, - "cpu_frame_ms": { - "min": 0.014596999622881413, - "avg": 0.0979599310227167, - "max": 0.31721600890159607, - "p1": 0.014948000200092793, - "p5": 0.015269000083208084, - "p50": 0.1059890016913414, - "p95": 0.197851000726223, - "p99": 0.2291328004002571 - }, - "gpu_frame_ms": { - "min": 0.06831999868154526, - "avg": 0.06938722113319057, - "max": 0.07455999404191971, - "p1": 0.06859999895095825, - "p5": 0.06880000233650208, - "p50": 0.06932000070810318, - "p95": 0.07016000151634216, - "p99": 0.07063999772071838 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 452.1070199999999, - "avg_ms": 0.07832761954261953 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.23340799999999995, - "avg_ms": 4.0437976437976426e-5 - }, - "upload_prep": { - "total_ms": 6.8931049999999985, - "avg_ms": 0.0011942316354816352 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 78.540964, - "avg_ms": 0.01360723562023562 - }, - "coverage": { - "total_ms": 61.68906199999999, - "avg_ms": 0.010687640679140678 - }, - "eviction": { - "total_ms": 4.873243999999998, - "avg_ms": 0.0008442903672903669 - }, - "manager_lock_wait": { - "total_ms": 2.936801000000001, - "avg_ms": 0.0005088012820512822 - }, - "manager_lock_hold": { - "total_ms": 457.69662400000004, - "avg_ms": 0.07929601940401941 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 41464128, - "max_bytes": 41464128, - "last_bytes": 41464128, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 33242304, - "max_bytes": 33242304, - "last_bytes": 33242304, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 33866560, - "max_bytes": 33866560, - "last_bytes": 33866560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 229398522, - "max_bytes": 229398522, - "last_bytes": 229398522, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 108572992, - "max_bytes": 108572992, - "last_bytes": 108572992, - "p50_bytes": 108572992, - "p95_bytes": 108572992, - "p99_bytes": 108572992 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 132756, - "rejected_total": 248196, - "coverage_total": 132756, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 219336, - "accepted": 46176, - "rejected_no_draw": 0, - "rejected_not_ready": 126984, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 46176, - "rejected_chunk_coverage": 0, - "coverage_checks": 46176 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 161616, - "accepted": 86580, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 75036, - "rejected_chunk_coverage": 0, - "coverage_checks": 86580 - } - ] - }, - "pressure": { - "staging_pressure_total": 126984, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00049877166748, - "upload_total_bytes": 138356928, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2225.65048, - "worker_mesh_construction_total_ms": 249.338681, - "worker_far_expanded_mesh_construction_ms": 18.585174, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33242304, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000800132751465, - "requested_duration_s": 5, - "sampled_frame_count": 5772, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 5877, - "duration_s": 5, - "fps": { - "min": 576.8047485351562, - "avg": 1175.3042256307788, - "max": 2357.56201171875, - "p1": 849.8942993164062, - "p5": 929.0026977539063, - "p50": 1114.558837890625, - "p95": 1566.268798828125, - "p99": 1705.0885937499993 - }, - "frame_ms": { - "min": 0.4241670072078705, - "avg": 0.8508435332675724, - "max": 1.7336889505386353, - "p1": 0.586480097770691, - "p5": 0.6384600043296814, - "p50": 0.8972160220146179, - "p95": 1.0764231204986572, - "p99": 1.1766173219680782 - }, - "max_frame_ms": 1.7336889505386353, - "cpu_ms_avg": 0.8508435332675724, - "gpu_ms": { - "shadow_avg": 0.0003596477786645814, - "opaque_avg": 0.45569919345126136, - "lod_terrain_avg": 0.06215789005659826, - "lod_water_avg": 0.007694027559653936, - "lod_culling_avg": 0, - "total_avg": 0.6599225825206224, - "total": { - "min": 0.6557599902153015, - "avg": 0.6599225825206224, - "max": 0.6714800596237183, - "p1": 0.6572303891181945, - "p5": 0.6579200029373169, - "p50": 0.6597599983215332, - "p95": 0.6624000668525696, - "p99": 0.6647696828842163 - }, - "lod_compact_terrain": { - "min": 0.06128000095486641, - "avg": 0.0619798740836376, - "max": 0.06620000302791595, - "p1": 0.06143999844789505, - "p5": 0.06159999966621399, - "p50": 0.06191999837756157, - "p95": 0.06247999891638756, - "p99": 0.06272000074386597 - }, - "lod_compact_water": { - "min": 0.00687999976798892, - "avg": 0.007528010883028001, - "max": 0.011839999817311764, - "p1": 0.007150400076061487, - "p5": 0.007240000180900097, - "p50": 0.007519999984651804, - "p95": 0.007888000272214406, - "p99": 0.00824000034481287 - } - }, - "draw_calls_avg": 493, - "vertices_avg": 716982, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 3255, - "frame_ms": 1.7336889505386353, - "gpu_total_ms": 0.658280074596405, - "gpu_lod_terrain_ms": 0.06196000054478645, - "gpu_lod_water_ms": 0.0074800001457333565, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45443999767303467, - "lod_cpu_ms": 0.014737000000025091, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.012813999999991665, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 23, - "lod_rejected_count": 21, - "lod_coverage_count": 23, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 5877, - "cpu_frame_ms": { - "min": 0.013353999704122543, - "avg": 0.09647072550915314, - "max": 0.9158509969711304, - "p1": 0.013546000234782696, - "p5": 0.013886000029742718, - "p50": 0.15349799394607544, - "p95": 0.19753699898719787, - "p99": 0.23047295928001402 - }, - "gpu_frame_ms": { - "min": 0.06880000233650208, - "avg": 0.06985191755555856, - "max": 0.07400000095367432, - "p1": 0.06920000165700912, - "p5": 0.06936000287532806, - "p50": 0.06980000436306, - "p95": 0.07047999650239944, - "p99": 0.0708799958229065 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 460.4110079999999, - "avg_ms": 0.07834116181725369 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.24041100000000004, - "avg_ms": 4.090709545686575e-5 - }, - "upload_prep": { - "total_ms": 0.6508689999999999, - "avg_ms": 0.00011074851114514207 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 79.31354299999998, - "avg_ms": 0.01349558329079462 - }, - "coverage": { - "total_ms": 63.585162, - "avg_ms": 0.010819323124042879 - }, - "eviction": { - "total_ms": 4.7581060000000015, - "avg_ms": 0.0008096147694401909 - }, - "manager_lock_wait": { - "total_ms": 3.005123, - "avg_ms": 0.0005113362259656287 - }, - "manager_lock_hold": { - "total_ms": 891.2458510000001, - "avg_ms": 0.15164979598434578 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 135171, - "rejected_total": 123417, - "coverage_total": 135171, - "levels": [ - { - "candidates": 94032, - "accepted": 35262, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 58770, - "rejected_chunk_coverage": 0, - "coverage_checks": 35262 - }, - { - "candidates": 23508, - "accepted": 11754, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11754, - "rejected_chunk_coverage": 0, - "coverage_checks": 11754 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 141048, - "accepted": 88155, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 52893, - "rejected_chunk_coverage": 0, - "coverage_checks": 88155 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 515340 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000269889831543, - "upload_total_bytes": 63685056, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 792.001341, - "worker_mesh_construction_total_ms": 91.195402, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.678035, - "compact_submissions": 339030, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000419616699219, - "requested_duration_s": 5, - "sampled_frame_count": 5877, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 18.585174, - "auto": 0.678035, - "status": "measured", - "reduction_fraction": 0.9635174252336836, - "delta": -17.907138999999997 - }, - "near_pool_geometry": { - "off": 33242304, - "auto": 17749824, - "status": "measured", - "delta": -15492480 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 138356928, - "auto": 63685056, - "status": "measured", - "delta": -74671872 - }, - "startup_worker_mesh_construction_ms": { - "off": 249.338681, - "auto": 91.195402, - "status": "measured", - "delta": -158.143279 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 9017, - "duration_s": 5, - "fps": { - "min": 46.6162109375, - "avg": 1803.2360854888382, - "max": 13023.5458984375, - "p1": 1006.9932177734376, - "p5": 1157.8695068359375, - "p50": 1959.5931396484375, - "p95": 8422.116406249997, - "p99": 11715.020195312503 - }, - "frame_ms": { - "min": 0.07678399980068207, - "avg": 0.5545585561687063, - "max": 21.451765060424805, - "p1": 0.08536056250333786, - "p5": 0.11873500198125839, - "p50": 0.5103099942207336, - "p95": 0.8636551976203918, - "p99": 0.9930553889274598 - }, - "max_frame_ms": 21.451765060424805, - "cpu_ms_avg": 0.5545585561687063, - "gpu_ms": { - "shadow_avg": 0.0003007918372810188, - "opaque_avg": 0.24602969500287958, - "lod_terrain_avg": 0.05195800373032218, - "lod_water_avg": 0.050585015000346, - "lod_culling_avg": 0, - "total_avg": 0.41497674216528196, - "total": { - "min": 0, - "avg": 0.41497674216528196, - "max": 0.5740399956703186, - "p1": 0, - "p5": 0, - "p50": 0.486920028924942, - "p95": 0.5458399772644044, - "p99": 0.5599472093582153 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 394.4902961073528, - "vertices_avg": 848337.5213485638, - "chunks_rendered_avg": 123.51402905622713, - "worst_frame": { - "frame_index": 3016, - "frame_ms": 21.451765060424805, - "gpu_total_ms": 0.5423600077629089, - "gpu_lod_terrain_ms": 0.06247999891638756, - "gpu_lod_water_ms": 0.05868000164628029, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.3384000062942505, - "lod_cpu_ms": 0.020618000000041548, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.016621000000014874, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 40315200, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 16, - "lod_rejected_count": 50, - "lod_coverage_count": 16, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 21 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 9017, - "cpu_frame_ms": { - "min": 0.0020540000405162573, - "avg": 0.10402668783021021, - "max": 20.95478057861328, - "p1": 0.002484160056337714, - "p5": 0.0033642000053077936, - "p50": 0.08893699944019318, - "p95": 0.24052819907665246, - "p99": 0.33905020713806155 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.102543018752513, - "max": 0.16655999422073364, - "p1": 0, - "p5": 0, - "p50": 0.1271200031042099, - "p95": 0.15751999616622925, - "p99": 0.16036000847816467 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 779.0205430000001, - "avg_ms": 0.08639464822002885 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.36193700000000006, - "avg_ms": 4.0139403349229243e-5 - }, - "upload_prep": { - "total_ms": 11.559888, - "avg_ms": 0.001282010424753244 - }, - "upload_submission": { - "total_ms": 21.089776, - "avg_ms": 0.0023388905400909396 - }, - "visibility": { - "total_ms": 86.26136100000002, - "avg_ms": 0.009566525562825777 - }, - "coverage": { - "total_ms": 67.35463700000003, - "avg_ms": 0.007469739048464015 - }, - "eviction": { - "total_ms": 7.156642999999999, - "avg_ms": 0.0007936833758456248 - }, - "manager_lock_wait": { - "total_ms": 4.1456409999999995, - "avg_ms": 0.00045975834534767656 - }, - "manager_lock_hold": { - "total_ms": 589.205183, - "avg_ms": 0.06534381534878562 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 34766016, - "upload_avg_bytes": 3855.607851835422, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 41282289.7549074, - "max_bytes": 43207488, - "last_bytes": 40315200, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 4291.20106465565, - "max_bytes": 2763840, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 4291.20106465565, - "max_bytes": 2763840, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 33327419.045802373, - "max_bytes": 33370368, - "last_bytes": 33370368, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 33781444.95419763, - "max_bytes": 33866944, - "last_bytes": 33738496, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 229636841.42652768, - "max_bytes": 232405157, - "last_bytes": 228249594, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 108391153.7549074, - "max_bytes": 110316352, - "last_bytes": 107424064, - "p50_bytes": 107424064, - "p95_bytes": 110316352, - "p99_bytes": 110316352 - }, - "logical_vram_bytes": { - "avg_bytes": 72356035.20106466, - "max_bytes": 75115584, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 116292, - "rejected_total": 383846, - "coverage_total": 116293, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 289018, - "accepted": 16508, - "rejected_no_draw": 0, - "rejected_not_ready": 163365, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 109144, - "rejected_chunk_coverage": 1, - "coverage_checks": 16509 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 211120, - "accepted": 99784, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 111336, - "rejected_chunk_coverage": 0, - "coverage_checks": 99784 - } - ] - }, - "pressure": { - "staging_pressure_total": 195390, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000455856323242, - "upload_total_bytes": 139224192, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2367.408506, - "worker_mesh_construction_total_ms": 295.737821, - "worker_far_expanded_mesh_construction_ms": 21.593913, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33241920, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000442981719971, - "requested_duration_s": 5, - "sampled_frame_count": 9017, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 9032, - "duration_s": 5, - "fps": { - "min": 267.22784423828125, - "avg": 1806.3916070976609, - "max": 13555.830078125, - "p1": 1059.5944140625002, - "p5": 1187.1000061035156, - "p50": 1966.3131103515625, - "p95": 8204.051416015622, - "p99": 12323.770722656256 - }, - "frame_ms": { - "min": 0.0737689957022667, - "avg": 0.5535898174409176, - "max": 3.7421247959136963, - "p1": 0.08114401243627072, - "p5": 0.1218909990042448, - "p50": 0.5085659921169281, - "p95": 0.8423890262842177, - "p99": 0.9437581723928461 - }, - "max_frame_ms": 3.7421247959136963, - "cpu_ms_avg": 0.5535898174409176, - "gpu_ms": { - "shadow_avg": 0.00030210031165940496, - "opaque_avg": 0.25212711247593245, - "lod_terrain_avg": 0.0522941407781388, - "lod_water_avg": 0.054026076180950726, - "lod_culling_avg": 0, - "total_avg": 0.42187778191084985, - "total": { - "min": 0, - "avg": 0.42187778191084985, - "max": 0.5989200472831726, - "p1": 0, - "p5": 0, - "p50": 0.488740012049675, - "p95": 0.5561200380325317, - "p99": 0.58761520922184 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.05214510631002651, - "max": 0.06723999977111816, - "p1": 0, - "p5": 0, - "p50": 0.06191999837756157, - "p95": 0.06239999830722809, - "p99": 0.06260000169277191 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.05388811781394697, - "max": 0.10044000297784805, - "p1": 0, - "p5": 0, - "p50": 0.06309999898076057, - "p95": 0.09551999717950821, - "p99": 0.09836000204086304 - } - }, - "draw_calls_avg": 419.1480292294066, - "vertices_avg": 847030.2914083259, - "chunks_rendered_avg": 123.42891939769707, - "worst_frame": { - "frame_index": 4384, - "frame_ms": 3.7421247959136963, - "gpu_total_ms": 0.5054000020027161, - "gpu_lod_terrain_ms": 0.06323999911546707, - "gpu_lod_water_ms": 0.07388000190258026, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.3022800087928772, - "lod_cpu_ms": 0.26661200000009444, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.23774700000012672, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 15, - "lod_rejected_count": 14, - "lod_coverage_count": 15, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 9032, - "cpu_frame_ms": { - "min": 0.0007919999770820141, - "avg": 0.09838450501137093, - "max": 1.9862940311431885, - "p1": 0.0010420000180602074, - "p5": 0.001563000027090311, - "p50": 0.12225949764251709, - "p95": 0.23271799907088261, - "p99": 0.3412468838691716 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.10632021696058468, - "max": 0.1629599928855896, - "p1": 0, - "p5": 0, - "p50": 0.12547999620437622, - "p95": 0.1578800082206726, - "p99": 0.16082760632038118 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 774.234665, - "avg_ms": 0.08572128709034543 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.35783999999999994, - "avg_ms": 3.9619131975199284e-5 - }, - "upload_prep": { - "total_ms": 0.9891729999999999, - "avg_ms": 0.00010951871124889281 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 82.873714, - "avg_ms": 0.009175566209034544 - }, - "coverage": { - "total_ms": 68.36670700000002, - "avg_ms": 0.007569387400354298 - }, - "eviction": { - "total_ms": 6.979297000000001, - "avg_ms": 0.0007727299601417184 - }, - "manager_lock_wait": { - "total_ms": 4.214599, - "avg_ms": 0.00046662965013286094 - }, - "manager_lock_hold": { - "total_ms": 1119.461299, - "avg_ms": 0.1239438993578388 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 40161.03454384411, - "max_bytes": 2980800, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 40161.03454384411, - "max_bytes": 2980800, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 10861396.76173605, - "max_bytes": 17749824, - "last_bytes": 8848704, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 106579115.23826395, - "max_bytes": 108591808, - "last_bytes": 108591808, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 174351973.05425155, - "max_bytes": 178437324, - "last_bytes": 173227548, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184589537.03454384, - "max_bytes": 187530176, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 185937152 - } - }, - "visibility": { - "visible_total": 112146, - "rejected_total": 127969, - "coverage_total": 112150, - "levels": [ - { - "candidates": 27427, - "accepted": 6958, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20465, - "rejected_chunk_coverage": 4, - "coverage_checks": 6962 - }, - { - "candidates": 30384, - "accepted": 4553, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 25831, - "rejected_chunk_coverage": 0, - "coverage_checks": 4553 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 182304, - "accepted": 100635, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 81669, - "rejected_chunk_coverage": 0, - "coverage_checks": 100635 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 625500 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000826835632324, - "upload_total_bytes": 63593664, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 770.458421, - "worker_mesh_construction_total_ms": 96.58237, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.068969, - "compact_submissions": 424230, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.00002384185791, - "requested_duration_s": 5, - "sampled_frame_count": 9032, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 21.593913, - "auto": 1.068969, - "status": "measured", - "reduction_fraction": 0.9504967441519284, - "delta": -20.524944 - }, - "near_pool_geometry": { - "off": 33241920, - "auto": 17749824, - "status": "measured", - "delta": -15492096 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 139224192, - "auto": 63593664, - "status": "measured", - "delta": -75630528 - }, - "startup_worker_mesh_construction_ms": { - "off": 295.737821, - "auto": 96.58237, - "status": "measured", - "delta": -199.155451 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 7787, - "duration_s": 5, - "fps": { - "min": 719.4073486328125, - "avg": 1557.072026244395, - "max": 3423.379638671875, - "p1": 880.0675158691406, - "p5": 931.6087585449219, - "p50": 1578.3800048828125, - "p95": 3297.837622070312, - "p99": 3378.325810546875 - }, - "frame_ms": { - "min": 0.29210901260375977, - "avg": 0.6422310484968163, - "max": 1.3900330066680908, - "p1": 0.2960045963525772, - "p5": 0.3032290071249008, - "p50": 0.6335610151290894, - "p95": 1.0734119296073914, - "p99": 1.1362768316268927 - }, - "max_frame_ms": 1.3900330066680908, - "cpu_ms_avg": 0.6422310484968163, - "gpu_ms": { - "shadow_avg": 0.0003595428276060529, - "opaque_avg": 0.30126832386365643, - "lod_terrain_avg": 0.06159224346972819, - "lod_water_avg": 0.01801808656670997, - "lod_culling_avg": 0, - "total_avg": 0.5033457179009508, - "total": { - "min": 0.3479999899864197, - "avg": 0.5033457179009508, - "max": 0.7240000367164612, - "p1": 0.3571031838655472, - "p5": 0.3637999892234802, - "p50": 0.4624800384044647, - "p95": 0.7084800422191619, - "p99": 0.7124568629264831 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 463, - "vertices_avg": 716730, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 942, - "frame_ms": 1.3900330066680908, - "gpu_total_ms": 0.711679995059967, - "gpu_lod_terrain_ms": 0.06176000088453293, - "gpu_lod_water_ms": 0.03288000077009201, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5059999823570251, - "lod_cpu_ms": 0.01677099999997722, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.014186000000023569, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43380096, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 21, - "lod_rejected_count": 46, - "lod_coverage_count": 21, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 7787, - "cpu_frame_ms": { - "min": 0.01181199960410595, - "avg": 0.09379660705059262, - "max": 0.3019070029258728, - "p1": 0.012541600205004215, - "p5": 0.013085300289094447, - "p50": 0.07635299861431122, - "p95": 0.19083500355482103, - "p99": 0.22716968238353738 - }, - "gpu_frame_ms": { - "min": 0.06468000262975693, - "avg": 0.07961032999347198, - "max": 0.12748000025749207, - "p1": 0.06487999856472015, - "p5": 0.06515999883413315, - "p50": 0.06747999787330627, - "p95": 0.11230799779295922, - "p99": 0.11661119893193247 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 588.099275, - "avg_ms": 0.07552321497367408 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.32901499999999995, - "avg_ms": 4.225182997303197e-5 - }, - "upload_prep": { - "total_ms": 9.172615, - "avg_ms": 0.0011779395145755747 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 96.30280399999998, - "avg_ms": 0.012367125208681133 - }, - "coverage": { - "total_ms": 76.23201499999999, - "avg_ms": 0.009789651341980222 - }, - "eviction": { - "total_ms": 6.6218460000000015, - "avg_ms": 0.0008503719018877619 - }, - "manager_lock_wait": { - "total_ms": 3.912656, - "avg_ms": 0.0005024599974316168 - }, - "manager_lock_hold": { - "total_ms": 611.528401, - "avg_ms": 0.07853196365737769 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 43380096, - "max_bytes": 43380096, - "last_bytes": 43380096, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 33073152, - "max_bytes": 33073152, - "last_bytes": 33073152, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 34035712, - "max_bytes": 34035712, - "last_bytes": 34035712, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 232577765, - "max_bytes": 232577765, - "last_bytes": 232577765, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 110488960, - "max_bytes": 110488960, - "last_bytes": 110488960, - "p50_bytes": 110488960, - "p95_bytes": 110488960, - "p99_bytes": 110488960 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 161496, - "rejected_total": 360233, - "coverage_total": 161496, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 303693, - "accepted": 58957, - "rejected_no_draw": 0, - "rejected_not_ready": 179101, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 65635, - "rejected_chunk_coverage": 0, - "coverage_checks": 58957 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 218036, - "accepted": 102539, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 115497, - "rejected_chunk_coverage": 0, - "coverage_checks": 102539 - } - ] - }, - "pressure": { - "staging_pressure_total": 179101, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000199317932129, - "upload_total_bytes": 138811200, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2439.991992, - "worker_mesh_construction_total_ms": 319.562137, - "worker_far_expanded_mesh_construction_ms": 43.227848, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33073152, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001040458679199, - "requested_duration_s": 5, - "sampled_frame_count": 7787, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 7756, - "duration_s": 5, - "fps": { - "min": 756.4783325195312, - "avg": 1551.106477416076, - "max": 2810.8125, - "p1": 927.6631378173828, - "p5": 947.4012756347656, - "p50": 1585.2747192382812, - "p95": 2745.1411743164062, - "p99": 2774.517370605469 - }, - "frame_ms": { - "min": 0.35576900839805603, - "avg": 0.6447010663419178, - "max": 1.3219149112701416, - "p1": 0.36042305678129194, - "p5": 0.36427999287843704, - "p50": 0.630805492401123, - "p95": 1.0555189549922943, - "p99": 1.0779775559902192 - }, - "max_frame_ms": 1.3219149112701416, - "cpu_ms_avg": 0.6447010663419178, - "gpu_ms": { - "shadow_avg": 0.0003588383211191698, - "opaque_avg": 0.3136609128285862, - "lod_terrain_avg": 0.06177940696829477, - "lod_water_avg": 0.027708849921001696, - "lod_culling_avg": 0, - "total_avg": 0.5156604178238309, - "total": { - "min": 0.34808000922203064, - "avg": 0.5156604178238309, - "max": 0.7481600046157837, - "p1": 0.3603420123457909, - "p5": 0.3655899912118912, - "p50": 0.4660400003194809, - "p95": 0.7396000623703003, - "p99": 0.7429999977350235 - }, - "lod_compact_terrain": { - "min": 0.06072000041604042, - "avg": 0.061590928321607244, - "max": 0.06672000139951706, - "p1": 0.060920000076293945, - "p5": 0.061039999127388, - "p50": 0.06159999966621399, - "p95": 0.06212000176310539, - "p99": 0.06232000142335892 - }, - "lod_compact_water": { - "min": 0.004040000028908253, - "avg": 0.027538298085272358, - "max": 0.08820000290870667, - "p1": 0.004120000172406435, - "p5": 0.004240000154823065, - "p50": 0.005919999908655882, - "p95": 0.08037000149488449, - "p99": 0.08532000333070755 - } - }, - "draw_calls_avg": 486.77153171738007, - "vertices_avg": 716682, - "chunks_rendered_avg": 113, - "worst_frame": { - "frame_index": 862, - "frame_ms": 1.3219149112701416, - "gpu_total_ms": 0.7232400178909302, - "gpu_lod_terrain_ms": 0.06212000176310539, - "gpu_lod_water_ms": 0.07744000107049942, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.5187600255012512, - "lod_cpu_ms": 0.018584000000004153, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.016339999999985366, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 24, - "lod_rejected_count": 20, - "lod_coverage_count": 24, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 7756, - "cpu_frame_ms": { - "min": 0.009637000039219856, - "avg": 0.08923795599540356, - "max": 0.2840929925441742, - "p1": 0.010039549972862005, - "p5": 0.01040900032967329, - "p50": 0.10125549882650375, - "p95": 0.1760304979979992, - "p99": 0.20563554391264915 - }, - "gpu_frame_ms": { - "min": 0.06515999883413315, - "avg": 0.08948825688023061, - "max": 0.15092000365257263, - "p1": 0.06564000248908997, - "p5": 0.06591999530792236, - "p50": 0.06787999719381332, - "p95": 0.1426199972629547, - "p99": 0.1476179927587509 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 572.241988, - "avg_ms": 0.07378055544094894 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.324468, - "avg_ms": 4.1834450747808144e-5 - }, - "upload_prep": { - "total_ms": 0.774721, - "avg_ms": 9.988666838576585e-5 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 86.77744899999999, - "avg_ms": 0.011188428184631252 - }, - "coverage": { - "total_ms": 67.77742800000001, - "avg_ms": 0.008738709128416712 - }, - "eviction": { - "total_ms": 6.359087000000001, - "avg_ms": 0.0008198925992779784 - }, - "manager_lock_wait": { - "total_ms": 3.890416, - "avg_ms": 0.0005016008251676121 - }, - "manager_lock_hold": { - "total_ms": 1138.937892, - "avg_ms": 0.14684604074265084 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 17749824, - "max_bytes": 17749824, - "last_bytes": 17749824, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99690688, - "max_bytes": 99690688, - "last_bytes": 99690688, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 178437324, - "max_bytes": 178437324, - "last_bytes": 178437324, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184549376, - "max_bytes": 184549376, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 155357, - "rejected_total": 185907, - "coverage_total": 155357, - "levels": [ - { - "candidates": 124096, - "accepted": 46555, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 77541, - "rejected_chunk_coverage": 0, - "coverage_checks": 46555 - }, - { - "candidates": 31024, - "accepted": 16614, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 14410, - "rejected_chunk_coverage": 0, - "coverage_checks": 16614 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 186144, - "accepted": 92188, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 93956, - "rejected_chunk_coverage": 0, - "coverage_checks": 92188 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 609086 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000334739685059, - "upload_total_bytes": 63259200, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 809.524362, - "worker_mesh_construction_total_ms": 89.34204, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 2.961686, - "compact_submissions": 424710, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000302314758301, - "requested_duration_s": 5, - "sampled_frame_count": 7756, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 43.227848, - "auto": 2.961686, - "status": "measured", - "reduction_fraction": 0.9314866194588266, - "delta": -40.266162 - }, - "near_pool_geometry": { - "off": 33073152, - "auto": 17749824, - "status": "measured", - "delta": -15323328 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 138811200, - "auto": 63259200, - "status": "measured", - "delta": -75552000 - }, - "startup_worker_mesh_construction_ms": { - "off": 319.562137, - "auto": 89.34204, - "status": "measured", - "delta": -230.220097 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 6969, - "duration_s": 5, - "fps": { - "min": 98.04658508300781, - "avg": 1393.7805594311023, - "max": 12236.3076171875, - "p1": 834.8558325195313, - "p5": 932.134619140625, - "p50": 1442.1004638671875, - "p95": 3377.61376953125, - "p99": 7225.966347656238 - }, - "frame_ms": { - "min": 0.08172400295734406, - "avg": 0.7174730578880858, - "max": 10.199233055114746, - "p1": 0.13839076042175294, - "p5": 0.2960669994354248, - "p50": 0.6934329867362976, - "p95": 1.072806429862976, - "p99": 1.1978116464614867 - }, - "max_frame_ms": 10.199233055114746, - "cpu_ms_avg": 0.7174730578880858, - "gpu_ms": { - "shadow_avg": 0.00033829817674431456, - "opaque_avg": 0.3375798133896146, - "lod_terrain_avg": 0.058161917138961794, - "lod_water_avg": 0.00752623329503834, - "lod_culling_avg": 0, - "total_avg": 0.5284754333699309, - "total": { - "min": 0, - "avg": 0.5284754333699309, - "max": 0.6714400053024292, - "p1": 0, - "p5": 0, - "p50": 0.6623200178146362, - "p95": 0.6654800534248352, - "p99": 0.6677600741386414 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 430.5907590759076, - "vertices_avg": 598478.040464916, - "chunks_rendered_avg": 112.99799110345818, - "worst_frame": { - "frame_index": 4591, - "frame_ms": 10.199233055114746, - "gpu_total_ms": 0.6625999808311462, - "gpu_lod_terrain_ms": 0.06151999905705452, - "gpu_lod_water_ms": 0.00812000036239624, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.4591600000858307, - "lod_cpu_ms": 2.061374999999998, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 1.8895119999999999, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 41535744, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 6969, - "cpu_frame_ms": { - "min": 0.0020240000449121, - "avg": 0.10318724993927185, - "max": 4.010910987854004, - "p1": 0.002815400091931224, - "p5": 0.015199000015854836, - "p50": 0.12461499869823456, - "p95": 0.21777199506759642, - "p99": 0.3151879131793974 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06568815044803211, - "max": 0.10232000052928925, - "p1": 0, - "p5": 0, - "p50": 0.06948000192642212, - "p95": 0.07135999947786331, - "p99": 0.07168000191450119 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 567.5983489999999, - "avg_ms": 0.08144616860381688 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.3000759999999999, - "avg_ms": 4.3058688477543395e-5 - }, - "upload_prep": { - "total_ms": 6.498789, - "avg_ms": 0.0009325281962978907 - }, - "upload_submission": { - "total_ms": 10.765899999999988, - "avg_ms": 0.001544827091404791 - }, - "visibility": { - "total_ms": 98.929283, - "avg_ms": 0.014195621036016645 - }, - "coverage": { - "total_ms": 82.02391100000001, - "avg_ms": 0.011769825082508253 - }, - "eviction": { - "total_ms": 10.955857, - "avg_ms": 0.0015720845171473669 - }, - "manager_lock_wait": { - "total_ms": 3.4763839999999995, - "avg_ms": 0.0004988354139761801 - }, - "manager_lock_hold": { - "total_ms": 534.9383009999999, - "avg_ms": 0.07675969306930691 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 41535744, - "upload_avg_bytes": 5960.072320275506, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 29922045.961256996, - "max_bytes": 41535744, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 229760.6612139475, - "max_bytes": 33174144, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 229760.6612139475, - "max_bytes": 33174144, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 24078624.730090402, - "max_bytes": 33174144, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 43030239.2699096, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 203203972.63452432, - "max_bytes": 229470138, - "last_bytes": 139929944, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 97030909.961257, - "max_bytes": 108644608, - "last_bytes": 67108864, - "p50_bytes": 108644608, - "p95_bytes": 108644608, - "p99_bytes": 108644608 - }, - "logical_vram_bytes": { - "avg_bytes": 72581504.66121395, - "max_bytes": 105525888, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 81163200 - } - }, - "visibility": { - "visible_total": 153878, - "rejected_total": 204190, - "coverage_total": 153878, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 174696, - "accepted": 32147, - "rejected_no_draw": 0, - "rejected_not_ready": 101230, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 41319, - "rejected_chunk_coverage": 0, - "coverage_checks": 32147 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 183372, - "accepted": 121731, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 61641, - "rejected_chunk_coverage": 0, - "coverage_checks": 121731 - } - ] - }, - "pressure": { - "staging_pressure_total": 110399, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000067710876465, - "upload_total_bytes": 138608256, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 2351.508721, - "worker_mesh_construction_total_ms": 299.740443, - "worker_far_expanded_mesh_construction_ms": 23.476131, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 33174144, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000062942504883, - "requested_duration_s": 5, - "sampled_frame_count": 6969, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "low", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 6, - "gpu_memory_mb_avg": 284.845947265625, - "gpu_memory_mb_max": 284.845947265625, - "frames": 6907, - "duration_s": 5, - "fps": { - "min": 85.36180114746094, - "avg": 1381.3145004399344, - "max": 12713.265625, - "p1": 835.8564904785156, - "p5": 954.1486938476562, - "p50": 1470.1795654296875, - "p95": 2699.222900390625, - "p99": 7289.120234374964 - }, - "frame_ms": { - "min": 0.07865799963474274, - "avg": 0.72394809413896, - "max": 11.714841842651367, - "p1": 0.13719198018312453, - "p5": 0.37047702074050903, - "p50": 0.6801890134811401, - "p95": 1.0480547189712524, - "p99": 1.1963777971267693 - }, - "max_frame_ms": 11.714841842651367, - "cpu_ms_avg": 0.72394809413896, - "gpu_ms": { - "shadow_avg": 0.00033918488657796943, - "opaque_avg": 0.34408311573139955, - "lod_terrain_avg": 0.05868857680178507, - "lod_water_avg": 0.0076221166667639695, - "lod_culling_avg": 0, - "total_avg": 0.5357605256080904, - "total": { - "min": 0, - "avg": 0.5357605256080904, - "max": 0.6704000234603882, - "p1": 0, - "p5": 0, - "p50": 0.6589199900627136, - "p95": 0.6612000465393066, - "p99": 0.6628776037693023 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.05851839584667482, - "max": 0.06800000369548798, - "p1": 0, - "p5": 0, - "p50": 0.06196000054478645, - "p95": 0.0623599998652935, - "p99": 0.06255999952554703 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.007465417670463301, - "max": 0.009479999542236328, - "p1": 0, - "p5": 0, - "p50": 0.0075599998235702515, - "p95": 0.009080000221729279, - "p99": 0.009237599615007622 - } - }, - "draw_calls_avg": 465.30215723179384, - "vertices_avg": 606861.526856812, - "chunks_rendered_avg": 112.99898653539887, - "worst_frame": { - "frame_index": 4714, - "frame_ms": 11.714841842651367, - "gpu_total_ms": 0.6598400473594666, - "gpu_lod_terrain_ms": 0.061879999935626984, - "gpu_lod_water_ms": 0.007720000110566616, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.45583999156951904, - "lod_cpu_ms": 0.0018039999999928114, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.00012100000000003774, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 6907, - "cpu_frame_ms": { - "min": 0.0009919999865815043, - "avg": 0.10042949951099713, - "max": 2.022331953048706, - "p1": 0.0013420599745586515, - "p5": 0.013638700265437363, - "p50": 0.10420600324869156, - "p95": 0.2114916950464248, - "p99": 0.3029073536396026 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.06631069344205347, - "max": 0.07756000012159348, - "p1": 0, - "p5": 0, - "p50": 0.06988000124692917, - "p95": 0.0716799944639206, - "p99": 0.07187999784946442 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 573.6140610000001, - "avg_ms": 0.0830482207905024 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.291743, - "avg_ms": 4.2238743303894595e-5 - }, - "upload_prep": { - "total_ms": 0.713814, - "avg_ms": 0.0001033464601129289 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 90.28942799999999, - "avg_ms": 0.013072162733458808 - }, - "coverage": { - "total_ms": 74.31920299999999, - "avg_ms": 0.010759983060663093 - }, - "eviction": { - "total_ms": 7.090540000000001, - "avg_ms": 0.00102657304184161 - }, - "manager_lock_wait": { - "total_ms": 3.462327000000001, - "avg_ms": 0.0005012779788620242 - }, - "manager_lock_hold": { - "total_ms": 994.5286959999999, - "avg_ms": 0.14398851831475312 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 40796.17779064717, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 40796.17779064717, - "max_bytes": 17611200, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 117440512, - "max_bytes": 117440512, - "last_bytes": 117440512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 13150054.954104532, - "max_bytes": 17749824, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 104290457.04589547, - "max_bytes": 117301888, - "last_bytes": 117301888, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 138624, - "max_bytes": 138624, - "last_bytes": 138624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66970240, - "max_bytes": 66970240, - "last_bytes": 66970240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 175180305.87461996, - "max_bytes": 178437324, - "last_bytes": 168174448, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 50331648, - "max_bytes": 50331648, - "last_bytes": 50331648, - "p50_bytes": 50331648, - "p95_bytes": 50331648, - "p99_bytes": 50331648 - }, - "logical_vram_bytes": { - "avg_bytes": 184590172.17779064, - "max_bytes": 202160576, - "last_bytes": 184549376, - "p50_bytes": 184549376, - "p95_bytes": 184549376, - "p99_bytes": 184549376 - } - }, - "visibility": { - "visible_total": 149937, - "rejected_total": 100799, - "coverage_total": 149937, - "levels": [ - { - "candidates": 75424, - "accepted": 28284, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 47140, - "rejected_chunk_coverage": 0, - "coverage_checks": 28284 - }, - { - "candidates": 18856, - "accepted": 9428, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 9428, - "rejected_chunk_coverage": 0, - "coverage_checks": 9428 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 156456, - "accepted": 112225, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 44231, - "rejected_chunk_coverage": 0, - "coverage_checks": 112225 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 562250 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000383377075195, - "upload_total_bytes": 62998848, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 138624, - "worker_generation_total_ms": 796.708605, - "worker_mesh_construction_total_ms": 97.513328, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.34569, - "compact_submissions": 337800, - "pool_gpu_allocated_bytes": 17749824, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 138624, - "pool_cpu_shadow_bytes": 50331648 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000305652618408, - "requested_duration_s": 5, - "sampled_frame_count": 6907, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9735595703125, - "delta": -5104256 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 138624, - "status": "measured", - "reduction_fraction": 0.9624310542200021, - "delta": -3551232 - }, - "worker_far_representation_work": { - "off": 23.476131, - "auto": 1.34569, - "status": "measured", - "reduction_fraction": 0.9426783740472396, - "delta": -22.130440999999998 - }, - "near_pool_geometry": { - "off": 33174144, - "auto": 17749824, - "status": "measured", - "delta": -15424320 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 50331648, - "status": "measured", - "delta": -16777216 - }, - "startup_upload_bytes": { - "off": 138608256, - "auto": 62998848, - "status": "measured", - "delta": -75609408 - }, - "startup_worker_mesh_construction_ms": { - "off": 299.740443, - "auto": 97.513328, - "status": "measured", - "delta": -202.22711500000003 - } - } - } - }, - "medium": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 1800, - "duration_s": 5, - "fps": { - "min": 273.8807678222656, - "avg": 359.9557027312898, - "max": 556.0838623046875, - "p1": 282.09527282714845, - "p5": 299.73936614990237, - "p50": 351.2511444091797, - "p95": 456.796940612793, - "p99": 468.9908358764648 - }, - "frame_ms": { - "min": 1.7982898950576782, - "avg": 2.7781196197536255, - "max": 3.651224136352539, - "p1": 2.1322378134727478, - "p5": 2.1891563892364503, - "p50": 2.846972107887268, - "p95": 3.3362319350242613, - "p99": 3.5449016904830932 - }, - "max_frame_ms": 3.651224136352539, - "cpu_ms_avg": 2.7781196197536255, - "gpu_ms": { - "shadow_avg": 0.09263297779278623, - "opaque_avg": 0.886238199936019, - "lod_terrain_avg": 0.1546311779237456, - "lod_water_avg": 0.004323666655044589, - "lod_culling_avg": 0, - "total_avg": 2.278787042432361, - "total": { - "min": 2.242640256881714, - "avg": 2.278787042432361, - "max": 2.3247201442718506, - "p1": 2.2486790776252743, - "p5": 2.256154012680054, - "p50": 2.278459906578064, - "p95": 2.302164030075073, - "p99": 2.3129208922386173 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 1723, - "vertices_avg": 1580886, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 215, - "frame_ms": 3.651224136352539, - "gpu_total_ms": 2.2486000061035156, - "gpu_lod_terrain_ms": 0.1550000011920929, - "gpu_lod_water_ms": 0.004279999993741512, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8848000168800354, - "lod_cpu_ms": 0.03778099999993856, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.03303199999999151, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 13012608, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 36, - "lod_rejected_count": 54, - "lod_coverage_count": 36, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 7 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1800, - "cpu_frame_ms": { - "min": 0.027542000636458397, - "avg": 0.23386252911864883, - "max": 0.5996770262718201, - "p1": 0.02885251922532916, - "p5": 0.02960544964298606, - "p50": 0.22162149660289288, - "p95": 0.5024200022220612, - "p99": 0.5268241137266159 - }, - "gpu_frame_ms": { - "min": 0.15504001080989838, - "avg": 0.1589548450625605, - "max": 0.16175998747348785, - "p1": 0.15564000606536865, - "p5": 0.15675799325108528, - "p50": 0.15911999344825745, - "p95": 0.16052000224590302, - "p99": 0.16107998803257942 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 349.59601799999996, - "avg_ms": 0.19422000999999997 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.09293099999999999, - "avg_ms": 5.162833333333333e-5 - }, - "upload_prep": { - "total_ms": 1.6141949999999996, - "avg_ms": 0.0008967749999999998 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 49.369439, - "avg_ms": 0.02742746611111111 - }, - "coverage": { - "total_ms": 37.62725800000001, - "avg_ms": 0.02090403222222223 - }, - "eviction": { - "total_ms": 1.911804, - "avg_ms": 0.0010621133333333333 - }, - "manager_lock_wait": { - "total_ms": 1.0218570000000002, - "avg_ms": 0.0005676983333333334 - }, - "manager_lock_hold": { - "total_ms": 179.15668699999998, - "avg_ms": 0.09953149277777776 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 13012608, - "max_bytes": 13012608, - "last_bytes": 13012608, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51562560, - "max_bytes": 51562560, - "last_bytes": 51562560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 15546304, - "max_bytes": 15546304, - "last_bytes": 15546304, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 195634370, - "max_bytes": 195634370, - "last_bytes": 195634370, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 80121472, - "max_bytes": 80121472, - "last_bytes": 80121472, - "p50_bytes": 80121472, - "p95_bytes": 80121472, - "p99_bytes": 80121472 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 64800, - "rejected_total": 97200, - "coverage_total": 64800, - "levels": [ - { - "candidates": 68400, - "accepted": 27000, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 41400, - "rejected_chunk_coverage": 0, - "coverage_checks": 27000 - }, - { - "candidates": 43200, - "accepted": 10800, - "rejected_no_draw": 0, - "rejected_not_ready": 12600, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 19800, - "rejected_chunk_coverage": 0, - "coverage_checks": 10800 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 50400, - "accepted": 27000, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 23400, - "rejected_chunk_coverage": 0, - "coverage_checks": 27000 - } - ] - }, - "pressure": { - "staging_pressure_total": 12600, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001482963562012, - "upload_total_bytes": 183825216, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3180.676128, - "worker_mesh_construction_total_ms": 402.526144, - "worker_far_expanded_mesh_construction_ms": 18.274064, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51562560, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000609397888184, - "requested_duration_s": 5, - "sampled_frame_count": 1800, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 1845, - "duration_s": 5, - "fps": { - "min": 252.4713134765625, - "avg": 368.81901427283316, - "max": 575.109619140625, - "p1": 283.4532458496094, - "p5": 298.6654052734375, - "p50": 363.15618896484375, - "p95": 466.5878601074219, - "p99": 481.8700122070312 - }, - "frame_ms": { - "min": 1.7387990951538086, - "avg": 2.7113569563966458, - "max": 3.960845947265625, - "p1": 2.0752491188049316, - "p5": 2.143218994140625, - "p50": 2.753636121749878, - "p95": 3.3482284069061277, - "p99": 3.5279187679290773 - }, - "max_frame_ms": 3.960845947265625, - "cpu_ms_avg": 2.7113569563966458, - "gpu_ms": { - "shadow_avg": 0.09242712748729116, - "opaque_avg": 0.8858823856364098, - "lod_terrain_avg": 0.15273537130213688, - "lod_water_avg": 0.004453485071921575, - "lod_culling_avg": 0, - "total_avg": 2.276980789567074, - "total": { - "min": 2.2415199279785156, - "avg": 2.276980789567074, - "max": 2.3204398155212402, - "p1": 2.2498704624176025, - "p5": 2.2557599544525146, - "p50": 2.275519847869873, - "p95": 2.301512098312378, - "p99": 2.3099247455596927 - }, - "lod_compact_terrain": { - "min": 0.15068000555038452, - "avg": 0.1525720757601384, - "max": 0.15768000483512878, - "p1": 0.1512800008058548, - "p5": 0.15160800218582154, - "p50": 0.15255999565124512, - "p95": 0.15355999767780304, - "p99": 0.1539199948310852 - }, - "lod_compact_water": { - "min": 0.0038399999029934406, - "avg": 0.004291533873489677, - "max": 0.010080000385642052, - "p1": 0.00395999988541007, - "p5": 0.004040000028908253, - "p50": 0.004279999993741512, - "p95": 0.004559999797493219, - "p99": 0.004800000227987766 - } - }, - "draw_calls_avg": 1753, - "vertices_avg": 1580214, - "chunks_rendered_avg": 293, - "worst_frame": { - "frame_index": 807, - "frame_ms": 3.960845947265625, - "gpu_total_ms": 2.261119842529297, - "gpu_lod_terrain_ms": 0.1515599936246872, - "gpu_lod_water_ms": 0.004800000227987766, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8873999714851379, - "lod_cpu_ms": 0.04316200000000947, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.038032000000001176, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 24258816, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 27, - "lod_rejected_count": 43, - "lod_coverage_count": 27, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 13 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1845, - "cpu_frame_ms": { - "min": 0.020508000627160072, - "avg": 0.2130662633424125, - "max": 0.6046680212020874, - "p1": 0.021199999377131462, - "p5": 0.022081800177693366, - "p50": 0.3129790127277374, - "p95": 0.4787039935588836, - "p99": 0.5216370606422424 - }, - "gpu_frame_ms": { - "min": 0.15532000362873077, - "avg": 0.15718885623827214, - "max": 0.16307999193668365, - "p1": 0.15584000945091248, - "p5": 0.15616001188755035, - "p50": 0.15715999901294708, - "p95": 0.15820001065731049, - "p99": 0.15863999724388123 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 334.477854, - "avg_ms": 0.18128880975609754 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.08926299999999998, - "avg_ms": 4.838102981029809e-5 - }, - "upload_prep": { - "total_ms": 1.9186430000000003, - "avg_ms": 0.0010399149051490517 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 41.543409999999994, - "avg_ms": 0.022516753387533874 - }, - "coverage": { - "total_ms": 33.358927, - "avg_ms": 0.018080719241192412 - }, - "eviction": { - "total_ms": 1.5331979999999996, - "avg_ms": 0.0008310016260162599 - }, - "manager_lock_wait": { - "total_ms": 0.9776380000000007, - "avg_ms": 0.0005298850948509488 - }, - "manager_lock_hold": { - "total_ms": 305.9017, - "avg_ms": 0.16580037940379405 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 24258816, - "max_bytes": 24258816, - "last_bytes": 24258816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 39121216, - "max_bytes": 39121216, - "last_bytes": 39121216, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 95096512, - "max_bytes": 95096512, - "last_bytes": 95096512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 267860326, - "max_bytes": 267860326, - "last_bytes": 267860326, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 91367680, - "max_bytes": 91367680, - "last_bytes": 91367680, - "p50_bytes": 91367680, - "p95_bytes": 91367680, - "p99_bytes": 91367680 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 49815, - "rejected_total": 79335, - "coverage_total": 49815, - "levels": [ - { - "candidates": 22140, - "accepted": 11070, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11070, - "rejected_chunk_coverage": 0, - "coverage_checks": 11070 - }, - { - "candidates": 55350, - "accepted": 11070, - "rejected_no_draw": 0, - "rejected_not_ready": 23985, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20295, - "rejected_chunk_coverage": 0, - "coverage_checks": 11070 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 51660, - "accepted": 27675, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 23985, - "rejected_chunk_coverage": 0, - "coverage_checks": 27675 - } - ] - }, - "pressure": { - "staging_pressure_total": 23985, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 157980 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002805709838867, - "upload_total_bytes": 166964992, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2552.651607, - "worker_mesh_construction_total_ms": 299.019317, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 3.435852, - "compact_submissions": 102630, - "pool_gpu_allocated_bytes": 39121216, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.002457141876221, - "requested_duration_s": 5, - "sampled_frame_count": 1845, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 18.274064, - "auto": 3.435852, - "status": "measured", - "reduction_fraction": 0.8119820528153999, - "delta": -14.838211999999999 - }, - "near_pool_geometry": { - "off": 51562560, - "auto": 39121216, - "status": "measured", - "delta": -12441344 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 183825216, - "auto": 166964992, - "status": "measured", - "delta": -16860224 - }, - "startup_worker_mesh_construction_ms": { - "off": 402.526144, - "auto": 299.019317, - "status": "measured", - "delta": -103.50682699999999 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 242.31269959456813, - "gpu_memory_mb_max": 242.595947265625, - "frames": 3113, - "duration_s": 5, - "fps": { - "min": 47.43296813964844, - "avg": 622.3617939288537, - "max": 13153.9130859375, - "p1": 290.5672509765625, - "p5": 326.4207702636719, - "p50": 521.5219116210938, - "p95": 8723.959765624993, - "p99": 11154.474257812502 - }, - "frame_ms": { - "min": 0.07602300494909286, - "avg": 1.6067824370888626, - "max": 21.082382202148438, - "p1": 0.08965020269155503, - "p5": 0.11462699919939041, - "p50": 1.9174649715423584, - "p95": 3.063530445098877, - "p99": 3.441556339263918 - }, - "max_frame_ms": 21.082382202148438, - "cpu_ms_avg": 1.6067824370888626, - "gpu_ms": { - "shadow_avg": 0.06684731124845533, - "opaque_avg": 0.3844914747096318, - "lod_terrain_avg": 0.083554847525547, - "lod_water_avg": 0.01867716029777625, - "lod_culling_avg": 0, - "total_avg": 1.1796880219858097, - "total": { - "min": 0, - "avg": 1.1796880219858097, - "max": 2.422879934310913, - "p1": 0, - "p5": 0, - "p50": 1.9518399238586426, - "p95": 2.310080003738403, - "p99": 2.3647457695007326 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 939.7404433022807, - "vertices_avg": 1893698.9392868616, - "chunks_rendered_avg": 301.67555412785094, - "worst_frame": { - "frame_index": 1139, - "frame_ms": 21.082382202148438, - "gpu_total_ms": 2.2943997383117676, - "gpu_lod_terrain_ms": 0.16468000411987305, - "gpu_lod_water_ms": 0.021080000326037407, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.706279993057251, - "lod_cpu_ms": 0.4390250000001288, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.36224099999981263, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 2027904, - "lod_deferred_deletion_bytes": 996096, - "lod_visible_count": 32, - "lod_rejected_count": 49, - "lod_coverage_count": 32, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 1 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 3113, - "cpu_frame_ms": { - "min": 0.0016929999692365527, - "avg": 0.24269845011236293, - "max": 19.64948844909668, - "p1": 0.002174000022932887, - "p5": 0.0028210000600665808, - "p50": 0.18968600034713745, - "p95": 0.5941187858581539, - "p99": 0.9081706357002263 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.10223200792788387, - "max": 0.22815999388694763, - "p1": 0, - "p5": 0, - "p50": 0.16099999845027924, - "p95": 0.2056799978017807, - "p99": 0.21879840672016204 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 605.279883, - "avg_ms": 0.19443619755862512 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.24924799999999997, - "avg_ms": 8.006681657565048e-5 - }, - "state_transition": { - "total_ms": 0.730959, - "avg_ms": 0.00023480854481207838 - }, - "upload_prep": { - "total_ms": 1.6238659999999996, - "avg_ms": 0.0005216402184388049 - }, - "upload_submission": { - "total_ms": 52.450531999999995, - "avg_ms": 0.0168488699004176 - }, - "visibility": { - "total_ms": 43.696995999999984, - "avg_ms": 0.01403694057179569 - }, - "coverage": { - "total_ms": 34.866738, - "avg_ms": 0.011200365563764856 - }, - "eviction": { - "total_ms": 8.713397, - "avg_ms": 0.002799035335689046 - }, - "manager_lock_wait": { - "total_ms": 2.7562230000000003, - "avg_ms": 0.0008853912624477996 - }, - "manager_lock_hold": { - "total_ms": 189.41240000000005, - "avg_ms": 0.06084561516222295 - } - }, - "workers": { - "generation_total_ms": 1792.7459370000001, - "mesh_construction_total_ms": 208.280524, - "far_expanded_mesh_construction_total_ms": 1.3787520000000022, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 142693440, - "upload_avg_bytes": 45837.91840668166, - "far_expanded_upload_total_bytes": 514560, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 5413644.952136203, - "max_bytes": 13179264, - "last_bytes": 2027904, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 917311.2393189849, - "max_bytes": 5990400, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 917311.2393189849, - "max_bytes": 5990400, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 156065.3466109862, - "max_bytes": 1608000, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 50747071.05428847, - "max_bytes": 57327168, - "last_bytes": 51465792, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 16361792.945711533, - "max_bytes": 24226816, - "last_bytes": 15643072, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5732305.290073884, - "max_bytes": 6029312, - "last_bytes": 6029312, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 182315928.4837777, - "max_bytes": 195801026, - "last_bytes": 178209938, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 72678574.29874718, - "max_bytes": 80288128, - "last_bytes": 69136768, - "p50_bytes": 69595072, - "p95_bytes": 80288128, - "p99_bytes": 80288128 - }, - "logical_vram_bytes": { - "avg_bytes": 73758480.52939287, - "max_bytes": 78735360, - "last_bytes": 73138176, - "p50_bytes": 73138176, - "p95_bytes": 77915328, - "p99_bytes": 78735360 - } - }, - "visibility": { - "visible_total": 54365, - "rejected_total": 90842, - "coverage_total": 54402, - "levels": [ - { - "candidates": 57532, - "accepted": 24879, - "rejected_no_draw": 0, - "rejected_not_ready": 147, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 32472, - "rejected_chunk_coverage": 34, - "coverage_checks": 24913 - }, - { - "candidates": 35357, - "accepted": 3592, - "rejected_no_draw": 0, - "rejected_not_ready": 5173, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 26589, - "rejected_chunk_coverage": 3, - "coverage_checks": 3595 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 52318, - "accepted": 25894, - "rejected_no_draw": 0, - "rejected_not_ready": 2, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 26422, - "rejected_chunk_coverage": 0, - "coverage_checks": 25894 - } - ] - }, - "pressure": { - "staging_pressure_total": 8772, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001436233520508, - "upload_total_bytes": 182509632, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3036.376638, - "worker_mesh_construction_total_ms": 397.214605, - "worker_far_expanded_mesh_construction_ms": 17.453636, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51384384, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001917839050293, - "requested_duration_s": 5, - "sampled_frame_count": 3113, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 3259, - "duration_s": 5, - "fps": { - "min": 177.8999786376953, - "avg": 651.6059044453843, - "max": 12584.9482421875, - "p1": 293.52267883300783, - "p5": 325.9034820556641, - "p50": 539.7723388671875, - "p95": 9079.93046875, - "p99": 11288.337578125 - }, - "frame_ms": { - "min": 0.07945999503135681, - "avg": 1.5346699487800868, - "max": 5.62113618850708, - "p1": 0.0885870186984539, - "p5": 0.11013300046324731, - "p50": 1.852632999420166, - "p95": 3.068393301963806, - "p99": 3.4068918657302856 - }, - "max_frame_ms": 5.62113618850708, - "cpu_ms_avg": 1.5346699487800868, - "gpu_ms": { - "shadow_avg": 0.06507133778005189, - "opaque_avg": 0.3776444797685593, - "lod_terrain_avg": 0.08142517329670601, - "lod_water_avg": 0.021002344256732756, - "lod_culling_avg": 0, - "total_avg": 1.1510211619251014, - "total": { - "min": 0, - "avg": 1.1510211619251014, - "max": 2.435959815979004, - "p1": 0, - "p5": 0, - "p50": 1.934360146522522, - "p95": 2.328399896621704, - "p99": 2.3944776248931885 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.08133726915668658, - "max": 0.17239999771118164, - "p1": 0, - "p5": 0, - "p50": 0.13252000510692596, - "p95": 0.16835999488830566, - "p99": 0.17051359981298447 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.02091381403375514, - "max": 0.06691999733448029, - "p1": 0, - "p5": 0, - "p50": 0.004679999779909849, - "p95": 0.06335999816656113, - "p99": 0.06469680041074753 - } - }, - "draw_calls_avg": 931.1279533599263, - "vertices_avg": 1897147.3936790426, - "chunks_rendered_avg": 301.56274930960416, - "worst_frame": { - "frame_index": 1377, - "frame_ms": 5.62113618850708, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.03703999999990515, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.03461500000000228, - "lod_worker_generation_ms": 9.874601999999868, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 1111872, - "lod_visible_count": 19, - "lod_rejected_count": 27, - "lod_coverage_count": 19, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 3259, - "cpu_frame_ms": { - "min": 0.0012120000319555402, - "avg": 0.2194801272055544, - "max": 2.829077959060669, - "p1": 0.0014882199722342192, - "p5": 0.0017430000007152557, - "p50": 0.18909500539302826, - "p95": 0.551383090019226, - "p99": 0.7109371042251592 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1024275175877311, - "max": 0.22784000635147095, - "p1": 0, - "p5": 0, - "p50": 0.15967999398708344, - "p95": 0.219300003349781, - "p99": 0.22497679710388185 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 637.4214729999999, - "avg_ms": 0.19558805553850872 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.134476, - "avg_ms": 4.1262964099417005e-5 - }, - "state_transition": { - "total_ms": 0.307413, - "avg_ms": 9.43274010432648e-5 - }, - "upload_prep": { - "total_ms": 0.4092380000000002, - "avg_ms": 0.00012557164774470704 - }, - "upload_submission": { - "total_ms": 4.779955000000001, - "avg_ms": 0.0014666937710954284 - }, - "visibility": { - "total_ms": 38.155923, - "avg_ms": 0.011707862227677201 - }, - "coverage": { - "total_ms": 31.72721999999999, - "avg_ms": 0.009735262350414234 - }, - "eviction": { - "total_ms": 4.247756, - "avg_ms": 0.001303392451672292 - }, - "manager_lock_wait": { - "total_ms": 1.9466160000000006, - "avg_ms": 0.0005973046946916233 - }, - "manager_lock_hold": { - "total_ms": 304.430781, - "avg_ms": 0.09341232924209882 - } - }, - "workers": { - "generation_total_ms": 811.7794739999999, - "mesh_construction_total_ms": 88.25070399999998, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.056096000000000035 - }, - "memory_bytes": { - "upload_total_bytes": 27135040, - "upload_avg_bytes": 8326.18594660939, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 23104, - "pending_cpu_upload_bytes": { - "avg_bytes": 1791.0966554157717, - "max_bytes": 533184, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 432550.9616446763, - "max_bytes": 6091584, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 432550.9616446763, - "max_bytes": 6091584, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 77270.48542497699, - "max_bytes": 2054400, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 26131250.066891685, - "max_bytes": 41698432, - "last_bytes": 22004864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 108086477.93310831, - "max_bytes": 113180928, - "last_bytes": 112212864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 174988.51917766186, - "max_bytes": 184832, - "last_bytes": 184832, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66933875.48082234, - "max_bytes": 66947136, - "last_bytes": 66924032, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 218909507.66155264, - "max_bytes": 229328446, - "last_bytes": 216775003, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 67187925.5820804, - "max_bytes": 69163264, - "last_bytes": 67108864, - "p50_bytes": 67108864, - "p95_bytes": 67627456, - "p99_bytes": 69101632 - }, - "logical_vram_bytes": { - "avg_bytes": 201759142.96164468, - "max_bytes": 207418176, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 203407488, - "p99_bytes": 204812160 - } - }, - "visibility": { - "visible_total": 40635, - "rejected_total": 55043, - "coverage_total": 40652, - "levels": [ - { - "candidates": 24486, - "accepted": 12483, - "rejected_no_draw": 0, - "rejected_not_ready": 105, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11882, - "rejected_chunk_coverage": 16, - "coverage_checks": 12499 - }, - { - "candidates": 18360, - "accepted": 2047, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 16312, - "rejected_chunk_coverage": 1, - "coverage_checks": 2048 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 52832, - "accepted": 26105, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 26727, - "rejected_chunk_coverage": 0, - "coverage_checks": 26105 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 172000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002114295959473, - "upload_total_bytes": 147120448, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2155.26624, - "worker_mesh_construction_total_ms": 273.3304, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.447836, - "compact_submissions": 119790, - "pool_gpu_allocated_bytes": 41698432, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001491546630859, - "requested_duration_s": 5, - "sampled_frame_count": 3259, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 17.453636, - "auto": 0.447836, - "status": "measured", - "reduction_fraction": 0.9743413922462919, - "delta": -17.0058 - }, - "near_pool_geometry": { - "off": 51384384, - "auto": 41698432, - "status": "measured", - "delta": -9685952 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 182509632, - "auto": 147120448, - "status": "measured", - "delta": -35389184 - }, - "startup_worker_mesh_construction_ms": { - "off": 397.214605, - "auto": 273.3304, - "status": "measured", - "delta": -123.88420500000001 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 241.845947265625, - "gpu_memory_mb_max": 241.845947265625, - "frames": 2117, - "duration_s": 5, - "fps": { - "min": 237.54188537597656, - "avg": 423.252210586372, - "max": 896.9929809570312, - "p1": 281.3946813964844, - "p5": 296.2925109863281, - "p50": 429.5261535644531, - "p95": 684.821337890625, - "p99": 710.4905810546875 - }, - "frame_ms": { - "min": 1.1148359775543213, - "avg": 2.362657477003142, - "max": 4.209784030914307, - "p1": 1.4074782848358154, - "p5": 1.4602348566055299, - "p50": 2.3281469345092773, - "p95": 3.3750431060791013, - "p99": 3.5537274169921877 - }, - "max_frame_ms": 4.209784030914307, - "cpu_ms_avg": 2.362657477003142, - "gpu_ms": { - "shadow_avg": 0.13267671238731313, - "opaque_avg": 0.6374586489778338, - "lod_terrain_avg": 0.1608081246406389, - "lod_water_avg": 0.008558790742766761, - "lod_culling_avg": 0, - "total_avg": 2.021048392954445, - "total": { - "min": 1.4833199977874756, - "avg": 2.021048392954445, - "max": 2.690959930419922, - "p1": 1.5013327026367187, - "p5": 1.524616003036499, - "p50": 1.9550399780273438, - "p95": 2.6407360553741457, - "p99": 2.6671775341033936 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 1763.1048653755315, - "vertices_avg": 1618568.7264997638, - "chunks_rendered_avg": 301.09258384506376, - "worst_frame": { - "frame_index": 310, - "frame_ms": 4.209784030914307, - "gpu_total_ms": 2.61303973197937, - "gpu_lod_terrain_ms": 0.14399999380111694, - "gpu_lod_water_ms": 0.018880000337958336, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8026000261306763, - "lod_cpu_ms": 0.039244000000039136, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.035607000000013045, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 13168128, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 36, - "lod_rejected_count": 54, - "lod_coverage_count": 36, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 7 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2117, - "cpu_frame_ms": { - "min": 0.020018000155687332, - "avg": 0.1928358708240398, - "max": 0.6586380004882812, - "p1": 0.021748000606894493, - "p5": 0.02332399971783161, - "p50": 0.0814640000462532, - "p95": 0.45987799763679504, - "p99": 0.5029698491096497 - }, - "gpu_frame_ms": { - "min": 0.14183999598026276, - "avg": 0.1693669155936901, - "max": 0.19839999079704285, - "p1": 0.14548640608787536, - "p5": 0.14851199388504027, - "p50": 0.1695600003004074, - "p95": 0.19204799830913544, - "p99": 0.19590720474720003 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 337.026344, - "avg_ms": 0.15919997354747284 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.102526, - "avg_ms": 4.84298535663675e-5 - }, - "upload_prep": { - "total_ms": 1.7289840000000005, - "avg_ms": 0.0008167142182333493 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 49.20908899999999, - "avg_ms": 0.02324472791686348 - }, - "coverage": { - "total_ms": 37.753517, - "avg_ms": 0.01783349881908361 - }, - "eviction": { - "total_ms": 2.0645160000000002, - "avg_ms": 0.0009752083136513936 - }, - "manager_lock_wait": { - "total_ms": 1.118563, - "avg_ms": 0.0005283717524799244 - }, - "manager_lock_hold": { - "total_ms": 201.41780700000004, - "avg_ms": 0.09514303589985831 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 13168128, - "max_bytes": 13168128, - "last_bytes": 13168128, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51395520, - "max_bytes": 51395520, - "last_bytes": 51395520, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 15713344, - "max_bytes": 15713344, - "last_bytes": 15713344, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 195789890, - "max_bytes": 195789890, - "last_bytes": 195789890, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 80276992, - "max_bytes": 80276992, - "last_bytes": 80276992, - "p50_bytes": 80276992, - "p95_bytes": 80276992, - "p99_bytes": 80276992 - }, - "logical_vram_bytes": { - "avg_bytes": 72351744, - "max_bytes": 72351744, - "last_bytes": 72351744, - "p50_bytes": 72351744, - "p95_bytes": 72351744, - "p99_bytes": 72351744 - } - }, - "visibility": { - "visible_total": 72172, - "rejected_total": 118358, - "coverage_total": 72172, - "levels": [ - { - "candidates": 80446, - "accepted": 29797, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 50649, - "rejected_chunk_coverage": 0, - "coverage_checks": 29797 - }, - { - "candidates": 50808, - "accepted": 14268, - "rejected_no_draw": 0, - "rejected_not_ready": 14819, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 21721, - "rejected_chunk_coverage": 0, - "coverage_checks": 14268 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 59276, - "accepted": 28107, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 31169, - "rejected_chunk_coverage": 0, - "coverage_checks": 28107 - } - ] - }, - "pressure": { - "staging_pressure_total": 14819, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00235652923584, - "upload_total_bytes": 184232448, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3083.737364, - "worker_mesh_construction_total_ms": 408.728512, - "worker_far_expanded_mesh_construction_ms": 31.206863, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51395520, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001747131347656, - "requested_duration_s": 5, - "sampled_frame_count": 2117, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 2009, - "duration_s": 5, - "fps": { - "min": 223.85848999023438, - "avg": 401.6403444142845, - "max": 1015.2274169921875, - "p1": 248.02737121582032, - "p5": 270.9326477050781, - "p50": 411.0755310058594, - "p95": 702.18359375, - "p99": 752.1536865234378 - }, - "frame_ms": { - "min": 0.9850009679794312, - "avg": 2.4897897183568753, - "max": 4.467107772827148, - "p1": 1.3295181703567505, - "p5": 1.4241289377212523, - "p50": 2.432642936706543, - "p95": 3.6909543037414547, - "p99": 4.031816406250001 - }, - "max_frame_ms": 4.467107772827148, - "cpu_ms_avg": 2.4897897183568753, - "gpu_ms": { - "shadow_avg": 0.13291883044363909, - "opaque_avg": 0.6351991438818192, - "lod_terrain_avg": 0.16139701346650298, - "lod_water_avg": 0.009704211054021198, - "lod_culling_avg": 0, - "total_avg": 2.018054122056102, - "total": { - "min": 1.4861600399017334, - "avg": 2.018054122056102, - "max": 2.6946799755096436, - "p1": 1.50306875705719, - "p5": 1.5230079889297485, - "p50": 1.9539600610733032, - "p95": 2.6447840213775633, - "p99": 2.671775856018067 - }, - "lod_compact_terrain": { - "min": 0.12272000312805176, - "avg": 0.16123305135413507, - "max": 0.19728000462055206, - "p1": 0.1284223985671997, - "p5": 0.13671200275421141, - "p50": 0.15992000699043274, - "p95": 0.1901840031147003, - "p99": 0.19395999610424042 - }, - "lod_compact_water": { - "min": 0.002319999970495701, - "avg": 0.009539472369288014, - "max": 0.050439998507499695, - "p1": 0.002520000096410513, - "p5": 0.0026400000788271427, - "p50": 0.004000000189989805, - "p95": 0.02468799985945224, - "p99": 0.04863360151648522 - } - }, - "draw_calls_avg": 1790.0572424091588, - "vertices_avg": 1618178.7058237928, - "chunks_rendered_avg": 301.21005475360874, - "worst_frame": { - "frame_index": 285, - "frame_ms": 4.467107772827148, - "gpu_total_ms": 2.6256799697875977, - "gpu_lod_terrain_ms": 0.1451999992132187, - "gpu_lod_water_ms": 0.019360000267624855, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8001199960708618, - "lod_cpu_ms": 0.029365000000083796, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.024896999999995728, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 18661440, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 36, - "lod_rejected_count": 57, - "lod_coverage_count": 36, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 10 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2009, - "cpu_frame_ms": { - "min": 0.019857000559568405, - "avg": 0.210660129873617, - "max": 0.915090024471283, - "p1": 0.021361720710992814, - "p5": 0.022857000306248668, - "p50": 0.29107800126075745, - "p95": 0.47116759419441223, - "p99": 0.5220105123519898 - }, - "gpu_frame_ms": { - "min": 0.14524000883102417, - "avg": 0.17110122469540792, - "max": 0.20027999579906464, - "p1": 0.14652320325374604, - "p5": 0.15007199645042418, - "p50": 0.17127999663352966, - "p95": 0.19334400296211243, - "p99": 0.19699681103229522 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 348.91114400000004, - "avg_ms": 0.17367403882528623 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.11431900000000006, - "avg_ms": 5.6903434544549555e-5 - }, - "upload_prep": { - "total_ms": 1.757701, - "avg_ms": 0.0008749133897461423 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 51.83161399999999, - "avg_ms": 0.025799708312593324 - }, - "coverage": { - "total_ms": 40.322633999999994, - "avg_ms": 0.0200709975111996 - }, - "eviction": { - "total_ms": 1.8806820000000002, - "avg_ms": 0.0009361284221005476 - }, - "manager_lock_wait": { - "total_ms": 1.0999790000000003, - "avg_ms": 0.0005475256346441017 - }, - "manager_lock_hold": { - "total_ms": 349.08679099999995, - "avg_ms": 0.173761468889995 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 18661440, - "max_bytes": 18661440, - "last_bytes": 18661440, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 51616576, - "max_bytes": 51616576, - "last_bytes": 51616576, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 82601152, - "max_bytes": 82601152, - "last_bytes": 82601152, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 266939011, - "max_bytes": 266939011, - "last_bytes": 266939011, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 85770304, - "max_bytes": 85770304, - "last_bytes": 85770304, - "p50_bytes": 85770304, - "p95_bytes": 85770304, - "p99_bytes": 85770304 - }, - "logical_vram_bytes": { - "avg_bytes": 201326592, - "max_bytes": 201326592, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201326592, - "p99_bytes": 201326592 - } - }, - "visibility": { - "visible_total": 68745, - "rejected_total": 118092, - "coverage_total": 68745, - "levels": [ - { - "candidates": 76342, - "accepted": 28248, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 48094, - "rejected_chunk_coverage": 0, - "coverage_checks": 28248 - }, - { - "candidates": 54243, - "accepted": 13852, - "rejected_no_draw": 0, - "rejected_not_ready": 20090, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20301, - "rejected_chunk_coverage": 0, - "coverage_checks": 13852 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 56252, - "accepted": 26645, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 29607, - "rejected_chunk_coverage": 0, - "coverage_checks": 26645 - } - ] - }, - "pressure": { - "staging_pressure_total": 20090, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 160000 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002923965454102, - "upload_total_bytes": 138864640, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2666.261265, - "worker_mesh_construction_total_ms": 305.255347, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.591241, - "compact_submissions": 106710, - "pool_gpu_allocated_bytes": 51616576, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001987457275391, - "requested_duration_s": 5, - "sampled_frame_count": 2009, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 31.206863, - "auto": 0.591241, - "status": "measured", - "reduction_fraction": 0.9810541354316837, - "delta": -30.615622 - }, - "near_pool_geometry": { - "off": 51395520, - "auto": 51616576, - "status": "measured", - "delta": 221056 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 184232448, - "auto": 138864640, - "status": "measured", - "delta": -45367808 - }, - "startup_worker_mesh_construction_ms": { - "off": 408.728512, - "auto": 305.255347, - "status": "measured", - "delta": -103.47316500000005 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 242.84142556144494, - "gpu_memory_mb_max": 244.345947265625, - "frames": 2488, - "duration_s": 5, - "fps": { - "min": 24.7762451171875, - "avg": 497.51141820419355, - "max": 11009.45703125, - "p1": 282.21917327880857, - "p5": 299.9642959594727, - "p50": 427.00791931152344, - "p95": 5846.193041992188, - "p99": 9009.449609375002 - }, - "frame_ms": { - "min": 0.09083100408315659, - "avg": 2.01000411932168, - "max": 40.36124038696289, - "p1": 0.1109946022182703, - "p5": 0.1710517004132271, - "p50": 2.3418771028518677, - "p95": 3.3337301373481747, - "p99": 3.5433456039428712 - }, - "max_frame_ms": 40.36124038696289, - "cpu_ms_avg": 2.01000411932168, - "gpu_ms": { - "shadow_avg": 0.060385598947812126, - "opaque_avg": 0.5744452731162789, - "lod_terrain_avg": 0.10470175247795137, - "lod_water_avg": 0.002804180062742802, - "lod_culling_avg": 0, - "total_avg": 1.4798502263244708, - "total": { - "min": 0, - "avg": 1.4798502263244708, - "max": 2.3046000003814697, - "p1": 0, - "p5": 0, - "p50": 2.2632200717926025, - "p95": 2.288025903701782, - "p99": 2.296650540828705 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 1166.6788585209003, - "vertices_avg": 1525471.1792604502, - "chunks_rendered_avg": 292.89067524115757, - "worst_frame": { - "frame_index": 1449, - "frame_ms": 40.36124038696289, - "gpu_total_ms": 2.293679714202881, - "gpu_lod_terrain_ms": 0.15556000173091888, - "gpu_lod_water_ms": 0.0041600000113248825, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8864799737930298, - "lod_cpu_ms": 0.004529000000047745, - "dominant_lod_cpu_category": "upload_prep", - "dominant_lod_cpu_category_ms": 0.0013819999999995503, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 12963840, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 7 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2488, - "cpu_frame_ms": { - "min": 0.0017030000453814864, - "avg": 0.2600630003683998, - "max": 21.53868865966797, - "p1": 0.0030520999873988332, - "p5": 0.0038305000634863973, - "p50": 0.19481100142002106, - "p95": 0.6164609581232072, - "p99": 1.5444065749645257 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1075059330192697, - "max": 0.16572000086307526, - "p1": 0, - "p5": 0, - "p50": 0.1576799973845482, - "p95": 0.16019999980926514, - "p99": 0.16076000034809113 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 490.08309500000007, - "avg_ms": 0.19697873593247592 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.148872, - "avg_ms": 5.9836012861736335e-5 - }, - "state_transition": { - "total_ms": 0.344896, - "avg_ms": 0.00013862379421221863 - }, - "upload_prep": { - "total_ms": 2.480511, - "avg_ms": 0.0009969899517684887 - }, - "upload_submission": { - "total_ms": 43.834754000000004, - "avg_ms": 0.017618470257234727 - }, - "visibility": { - "total_ms": 44.85350299999999, - "avg_ms": 0.018027935289389063 - }, - "coverage": { - "total_ms": 35.318388999999996, - "avg_ms": 0.014195493971061091 - }, - "eviction": { - "total_ms": 10.372136000000001, - "avg_ms": 0.004168864951768489 - }, - "manager_lock_wait": { - "total_ms": 2.8227610000000003, - "avg_ms": 0.0011345502411575564 - }, - "manager_lock_hold": { - "total_ms": 192.72738500000003, - "avg_ms": 0.07746277532154341 - } - }, - "workers": { - "generation_total_ms": 2858.3647379999998, - "mesh_construction_total_ms": 350.02195000000006, - "far_expanded_mesh_construction_total_ms": 6.829737999999999, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 139212864, - "upload_avg_bytes": 55953.72347266881, - "far_expanded_upload_total_bytes": 1713984, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 15164683.678456591, - "max_bytes": 48202752, - "last_bytes": 48202752, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 1352224.6688102894, - "max_bytes": 51599808, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 1352224.6688102894, - "max_bytes": 51599808, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 123331.44694533762, - "max_bytes": 2544768, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 55098192.926045015, - "max_bytes": 66642048, - "last_bytes": 54951168, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 12010671.073954985, - "max_bytes": 58711936, - "last_bytes": 12157696, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 6286714.649517685, - "max_bytes": 7864320, - "last_bytes": 7864320, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 190919403.74758843, - "max_bytes": 245033335, - "last_bytes": 245033335, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 82396879.12540193, - "max_bytes": 115311616, - "last_bytes": 115311616, - "p50_bytes": 80072704, - "p95_bytes": 93753088, - "p99_bytes": 115311616 - }, - "logical_vram_bytes": { - "avg_bytes": 74747803.31832798, - "max_bytes": 123951552, - "last_bytes": 74973184, - "p50_bytes": 72351744, - "p95_bytes": 78126784, - "p99_bytes": 123951552 - } - }, - "visibility": { - "visible_total": 64127, - "rejected_total": 90161, - "coverage_total": 64137, - "levels": [ - { - "candidates": 63951, - "accepted": 25493, - "rejected_no_draw": 0, - "rejected_not_ready": 87, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 38361, - "rejected_chunk_coverage": 10, - "coverage_checks": 25503 - }, - { - "candidates": 39761, - "accepted": 9823, - "rejected_no_draw": 0, - "rejected_not_ready": 12599, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 17339, - "rejected_chunk_coverage": 0, - "coverage_checks": 9823 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 50576, - "accepted": 28811, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 21765, - "rejected_chunk_coverage": 0, - "coverage_checks": 28811 - } - ] - }, - "pressure": { - "staging_pressure_total": 22882, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00263786315918, - "upload_total_bytes": 180793536, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 3192.117234, - "worker_mesh_construction_total_ms": 405.495598, - "worker_far_expanded_mesh_construction_ms": 23.485904, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 51599808, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.000885963439941, - "requested_duration_s": 5, - "sampled_frame_count": 2488, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "medium", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 10, - "gpu_memory_mb_avg": 300.845947265625, - "gpu_memory_mb_max": 300.845947265625, - "frames": 2679, - "duration_s": 5, - "fps": { - "min": 32.680442810058594, - "avg": 535.7636935722855, - "max": 11037.52734375, - "p1": 287.34007568359374, - "p5": 311.83722534179685, - "p50": 451.15032958984375, - "p95": 6335.624169921866, - "p99": 8676.557753906245 - }, - "frame_ms": { - "min": 0.09059999883174896, - "avg": 1.8664945236067578, - "max": 30.599340438842773, - "p1": 0.11525320217013359, - "p5": 0.15784060359001162, - "p50": 2.2165560722351074, - "p95": 3.2068012237548826, - "p99": 3.4802083206176726 - }, - "max_frame_ms": 30.599340438842773, - "cpu_ms_avg": 1.8664945236067578, - "gpu_ms": { - "shadow_avg": 0.05831933567476344, - "opaque_avg": 0.5555913848812877, - "lod_terrain_avg": 0.10050563651063008, - "lod_water_avg": 0.002796386699263492, - "lod_culling_avg": 0, - "total_avg": 1.4283642033313184, - "total": { - "min": 0, - "avg": 1.4283642033313184, - "max": 2.3164000511169434, - "p1": 0, - "p5": 0, - "p50": 2.2576799392700195, - "p95": 2.290924000740051, - "p99": 2.2998303079605105 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.10039810371870601, - "max": 0.1578799933195114, - "p1": 0, - "p5": 0, - "p50": 0.15163999795913696, - "p95": 0.15323999524116516, - "p99": 0.15372000634670258 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.0026889735025942283, - "max": 0.005239999853074551, - "p1": 0, - "p5": 0, - "p50": 0.004040000028908253, - "p95": 0.004480000119656324, - "p99": 0.004720000084489584 - } - }, - "draw_calls_avg": 1154.3131765584174, - "vertices_avg": 1520312.477043673, - "chunks_rendered_avg": 292.88465845464725, - "worst_frame": { - "frame_index": 1485, - "frame_ms": 30.599340438842773, - "gpu_total_ms": 2.2994399070739746, - "gpu_lod_terrain_ms": 0.15328000485897064, - "gpu_lod_water_ms": 0.004360000137239695, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 0.8886399865150452, - "lod_cpu_ms": 4.8241689999999835, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 4.428445000000001, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 10075968, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 5 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2679, - "cpu_frame_ms": { - "min": 0.0016639999812468886, - "avg": 0.22166922859848068, - "max": 4.824169158935547, - "p1": 0.002211800077930093, - "p5": 0.00253499997779727, - "p50": 0.1657020002603531, - "p95": 0.527750027179718, - "p99": 0.689683824777603 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1033020231618326, - "max": 0.1624400019645691, - "p1": 0, - "p5": 0, - "p50": 0.15616001188755035, - "p95": 0.1578799933195114, - "p99": 0.1584400087594986 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 510.912638, - "avg_ms": 0.19071020455393803 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.043418999999999985, - "avg_ms": 1.6207166853303464e-5 - }, - "state_transition": { - "total_ms": 0.19633899999999993, - "avg_ms": 7.328816722657705e-5 - }, - "upload_prep": { - "total_ms": 1.7156190000000002, - "avg_ms": 0.0006403952967525197 - }, - "upload_submission": { - "total_ms": 4.645470000000003, - "avg_ms": 0.0017340313549832039 - }, - "visibility": { - "total_ms": 46.282848, - "avg_ms": 0.017276165733482642 - }, - "coverage": { - "total_ms": 36.42455100000001, - "avg_ms": 0.013596323628219487 - }, - "eviction": { - "total_ms": 7.439381000000001, - "avg_ms": 0.00277692459873087 - }, - "manager_lock_wait": { - "total_ms": 1.2448480000000002, - "avg_ms": 0.0004646689063083241 - }, - "manager_lock_hold": { - "total_ms": 312.600053, - "avg_ms": 0.11668535013064577 - } - }, - "workers": { - "generation_total_ms": 385.735357, - "mesh_construction_total_ms": 35.43381700000003, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.23533199999999999 - }, - "memory_bytes": { - "upload_total_bytes": 25886608, - "upload_avg_bytes": 9662.787607316162, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 75088, - "pending_cpu_upload_bytes": { - "avg_bytes": 9008956.010451661, - "max_bytes": 10578432, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 535383.0055991041, - "max_bytes": 51515328, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 535383.0055991041, - "max_bytes": 51515328, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 19301.339305711088, - "max_bytes": 1067520, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 48041091.98954834, - "max_bytes": 56271824, - "last_bytes": 10937744, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 86176636.01045166, - "max_bytes": 123413616, - "last_bytes": 123279984, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 194473.75886524824, - "max_bytes": 236816, - "last_bytes": 236816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66914390.241134755, - "max_bytes": 66947136, - "last_bytes": 66872048, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 236048618.23366928, - "max_bytes": 252037164, - "last_bytes": 208200443, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 76137121.34975737, - "max_bytes": 77687296, - "last_bytes": 67108864, - "p50_bytes": 77184832, - "p95_bytes": 77184832, - "p99_bytes": 77184832 - }, - "logical_vram_bytes": { - "avg_bytes": 201861975.0055991, - "max_bytes": 252841920, - "last_bytes": 201326592, - "p50_bytes": 201326592, - "p95_bytes": 201846912, - "p99_bytes": 212408832 - } - }, - "visibility": { - "visible_total": 62869, - "rejected_total": 84528, - "coverage_total": 62871, - "levels": [ - { - "candidates": 61498, - "accepted": 24933, - "rejected_no_draw": 0, - "rejected_not_ready": 33, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 36530, - "rejected_chunk_coverage": 2, - "coverage_checks": 24935 - }, - { - "candidates": 32675, - "accepted": 7425, - "rejected_no_draw": 0, - "rejected_not_ready": 7430, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 17820, - "rejected_chunk_coverage": 0, - "coverage_checks": 7425 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 53224, - "accepted": 30511, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 22713, - "rejected_chunk_coverage": 0, - "coverage_checks": 30511 - } - ] - }, - "pressure": { - "staging_pressure_total": 11970, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 164882 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000948905944824, - "upload_total_bytes": 133665664, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 2461.144028, - "worker_mesh_construction_total_ms": 302.198718, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.422658, - "compact_submissions": 103860, - "pool_gpu_allocated_bytes": 51677056, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 67108864 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.0003437995910645, - "requested_duration_s": 5, - "sampled_frame_count": 2679, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 23.485904, - "auto": 0.422658, - "status": "measured", - "reduction_fraction": 0.9820037585097854, - "delta": -23.063246000000003 - }, - "near_pool_geometry": { - "off": 51599808, - "auto": 51677056, - "status": "measured", - "delta": 77248 - }, - "cpu_expanded_shadow": { - "off": 67108864, - "auto": 67108864, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 180793536, - "auto": 133665664, - "status": "measured", - "delta": -47127872 - }, - "startup_worker_mesh_construction_ms": { - "off": 405.495598, - "auto": 302.198718, - "status": "measured", - "delta": -103.29687999999999 - } - } - } - }, - "high": { - "stationary": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.845947265625, - "gpu_memory_mb_max": 689.845947265625, - "frames": 1164, - "duration_s": 5, - "fps": { - "min": 176.40773010253906, - "avg": 232.60976686402432, - "max": 333.54949951171875, - "p1": 193.64174865722657, - "p5": 199.75974349975584, - "p50": 232.79254150390625, - "p95": 274.08194427490236, - "p99": 283.2702713012695 - }, - "frame_ms": { - "min": 2.998055934906006, - "avg": 4.299045622553613, - "max": 5.6686859130859375, - "p1": 3.5301978397369385, - "p5": 3.6485438823699954, - "p50": 4.295670509338379, - "p95": 5.006013798713684, - "p99": 5.164178075790405 - }, - "max_frame_ms": 5.6686859130859375, - "cpu_ms_avg": 4.299045622553613, - "gpu_ms": { - "shadow_avg": 0.149739089764373, - "opaque_avg": 1.6727786606939388, - "lod_terrain_avg": 0.35862058372944083, - "lod_water_avg": 0.004492405490970553, - "lod_culling_avg": 0, - "total_avg": 3.6868753943246664, - "total": { - "min": 3.646519660949707, - "avg": 3.6868753943246664, - "max": 3.730560064315796, - "p1": 3.6599022293090817, - "p5": 3.6672060966491697, - "p50": 3.6862800121307373, - "p95": 3.709713888168335, - "p99": 3.718381862640381 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 2193, - "vertices_avg": 1926678, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 1031, - "frame_ms": 5.6686859130859375, - "gpu_total_ms": 3.6781599521636963, - "gpu_lod_terrain_ms": 0.35552000999450684, - "gpu_lod_water_ms": 0.004800000227987766, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6688799858093262, - "lod_cpu_ms": 0.08694300000018984, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.07711499999999205, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 88875456, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 34, - "lod_rejected_count": 88, - "lod_coverage_count": 34, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 47 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1164, - "cpu_frame_ms": { - "min": 0.04984299838542938, - "avg": 0.2853482455027984, - "max": 0.7825310230255127, - "p1": 0.05224029991775751, - "p5": 0.053651499561965466, - "p50": 0.2676434963941574, - "p95": 0.5961081951856613, - "p99": 0.6612751734256744 - }, - "gpu_frame_ms": { - "min": 0.3577199876308441, - "avg": 0.3631129889297731, - "max": 0.3675999939441681, - "p1": 0.35915999501943585, - "p5": 0.3601259976625443, - "p50": 0.3632600009441376, - "p95": 0.3657200038433075, - "p99": 0.3664800038933754 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 252.200874, - "avg_ms": 0.2166674175257732 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.060474999999999834, - "avg_ms": 5.1954467353951747e-5 - }, - "upload_prep": { - "total_ms": 3.629244999999999, - "avg_ms": 0.0031179080756013736 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 57.26396600000001, - "avg_ms": 0.04919584707903781 - }, - "coverage": { - "total_ms": 49.39870499999999, - "avg_ms": 0.04243874999999999 - }, - "eviction": { - "total_ms": 1.3889689999999995, - "avg_ms": 0.001193272336769759 - }, - "manager_lock_wait": { - "total_ms": 0.6440629999999996, - "avg_ms": 0.0005533187285223364 - }, - "manager_lock_hold": { - "total_ms": 145.81593100000003, - "avg_ms": 0.12527141838487976 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 88875456, - "max_bytes": 88875456, - "last_bytes": 88875456, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 72192960, - "max_bytes": 72192960, - "last_bytes": 72192960, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 28470336, - "max_bytes": 28470336, - "last_bytes": 28470336, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 402472482, - "max_bytes": 402472482, - "last_bytes": 402472482, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 189538752, - "max_bytes": 189538752, - "last_bytes": 189538752, - "p50_bytes": 189538752, - "p95_bytes": 189538752, - "p99_bytes": 189538752 - }, - "logical_vram_bytes": { - "avg_bytes": 105906176, - "max_bytes": 105906176, - "last_bytes": 105906176, - "p50_bytes": 105906176, - "p95_bytes": 105906176, - "p99_bytes": 105906176 - } - }, - "visibility": { - "visible_total": 39576, - "rejected_total": 102432, - "coverage_total": 39576, - "levels": [ - { - "candidates": 15132, - "accepted": 4656, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 10476, - "rejected_chunk_coverage": 0, - "coverage_checks": 4656 - }, - { - "candidates": 74496, - "accepted": 8148, - "rejected_no_draw": 0, - "rejected_not_ready": 54708, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11640, - "rejected_chunk_coverage": 0, - "coverage_checks": 8148 - }, - { - "candidates": 19788, - "accepted": 9312, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 10476, - "rejected_chunk_coverage": 0, - "coverage_checks": 9312 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 32592, - "accepted": 17460, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15132, - "rejected_chunk_coverage": 0, - "coverage_checks": 17460 - } - ] - }, - "pressure": { - "staging_pressure_total": 54708, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003320693969727, - "upload_total_bytes": 260991168, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5602.494001, - "worker_mesh_construction_total_ms": 679.528887, - "worker_far_expanded_mesh_construction_ms": 22.177542, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 72192960, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.004089832305908, - "requested_duration_s": 5, - "sampled_frame_count": 1164, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "stationary", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 1176, - "duration_s": 5, - "fps": { - "min": 180.67156982421875, - "avg": 235.0108824301382, - "max": 321.070068359375, - "p1": 198.10243225097656, - "p5": 204.06834411621094, - "p50": 234.00827026367188, - "p95": 270.5585174560547, - "p99": 283.3115692138672 - }, - "frame_ms": { - "min": 3.1145851612091064, - "avg": 4.255122101834031, - "max": 5.534904956817627, - "p1": 3.529685080051422, - "p5": 3.69605815410614, - "p50": 4.273353576660156, - "p95": 4.9003190994262695, - "p99": 5.047894358634949 - }, - "max_frame_ms": 5.534904956817627, - "cpu_ms_avg": 4.255122101834031, - "gpu_ms": { - "shadow_avg": 0.14965712605994574, - "opaque_avg": 1.6747419381020021, - "lod_terrain_avg": 0.3585533676352225, - "lod_water_avg": 0.004667074827011675, - "lod_culling_avg": 0, - "total_avg": 3.6897719684912236, - "total": { - "min": 3.6471199989318848, - "avg": 3.6897719684912236, - "max": 3.7381601333618164, - "p1": 3.662030041217804, - "p5": 3.6687899231910706, - "p50": 3.6887999773025513, - "p95": 3.7144100666046143, - "p99": 3.7256199717521667 - }, - "lod_compact_terrain": { - "min": 0.3529999852180481, - "avg": 0.35838955830858676, - "max": 0.3654400110244751, - "p1": 0.3549000024795532, - "p5": 0.3557500094175339, - "p50": 0.3584800064563751, - "p95": 0.3608799874782562, - "p99": 0.36176998913288116 - }, - "lod_compact_water": { - "min": 0.003800000064074993, - "avg": 0.00450244897677164, - "max": 0.005799999926239252, - "p1": 0.003920000046491623, - "p5": 0.004040000028908253, - "p50": 0.004480000119656324, - "p95": 0.005040000192821026, - "p99": 0.005319999996572733 - } - }, - "draw_calls_avg": 2223, - "vertices_avg": 1926852, - "chunks_rendered_avg": 367, - "worst_frame": { - "frame_index": 249, - "frame_ms": 5.534904956817627, - "gpu_total_ms": 3.676759958267212, - "gpu_lod_terrain_ms": 0.358599990606308, - "gpu_lod_water_ms": 0.00443999981507659, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.670240044593811, - "lod_cpu_ms": 0.058519999999901984, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.047449000000000296, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 40818624, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 28, - "lod_rejected_count": 57, - "lod_coverage_count": 28, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 22 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1176, - "cpu_frame_ms": { - "min": 0.03574699908494949, - "avg": 0.24098753586702912, - "max": 0.6619240045547485, - "p1": 0.037187001667916775, - "p5": 0.03833224903792143, - "p50": 0.2131355032324791, - "p95": 0.5194087624549866, - "p99": 0.5639000087976456 - }, - "gpu_frame_ms": { - "min": 0.35731998085975647, - "avg": 0.3632204429734321, - "max": 0.3702000081539154, - "p1": 0.3596699982881546, - "p5": 0.36052000522613525, - "p50": 0.3632799983024597, - "p95": 0.36577001959085464, - "p99": 0.36653000861406326 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 220.78726700000004, - "avg_ms": 0.18774427465986399 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.05986199999999986, - "avg_ms": 5.0903061224489675e-5 - }, - "upload_prep": { - "total_ms": 2.223355999999999, - "avg_ms": 0.0018906088435374142 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 46.287925, - "avg_ms": 0.039360480442176875 - }, - "coverage": { - "total_ms": 40.198766000000006, - "avg_ms": 0.03418262414965987 - }, - "eviction": { - "total_ms": 1.1708609999999986, - "avg_ms": 0.0009956301020408152 - }, - "manager_lock_wait": { - "total_ms": 0.648231, - "avg_ms": 0.0005512168367346939 - }, - "manager_lock_hold": { - "total_ms": 216.267701, - "avg_ms": 0.183901106292517 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 40818624, - "max_bytes": 40818624, - "last_bytes": 40818624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 66416512, - "max_bytes": 66416512, - "last_bytes": 66416512, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 101355648, - "max_bytes": 101355648, - "last_bytes": 101355648, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379854763, - "max_bytes": 379854763, - "last_bytes": 379854763, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 141481920, - "max_bytes": 141481920, - "last_bytes": 141481920, - "p50_bytes": 141481920, - "p95_bytes": 141481920, - "p99_bytes": 141481920 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 32928, - "rejected_total": 67032, - "coverage_total": 32928, - "levels": [ - { - "candidates": 2352, - "accepted": 1176, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1176, - "rejected_chunk_coverage": 0, - "coverage_checks": 1176 - }, - { - "candidates": 45864, - "accepted": 4704, - "rejected_no_draw": 0, - "rejected_not_ready": 25872, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15288, - "rejected_chunk_coverage": 0, - "coverage_checks": 4704 - }, - { - "candidates": 18816, - "accepted": 9408, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 9408, - "rejected_chunk_coverage": 0, - "coverage_checks": 9408 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 32928, - "accepted": 17640, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15288, - "rejected_chunk_coverage": 0, - "coverage_checks": 17640 - } - ] - }, - "pressure": { - "staging_pressure_total": 25872, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 98700 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.003300666809082, - "upload_total_bytes": 305257792, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5412.615515, - "worker_mesh_construction_total_ms": 624.595113, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.386749, - "compact_submissions": 63420, - "pool_gpu_allocated_bytes": 66416512, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.004021167755127, - "requested_duration_s": 5, - "sampled_frame_count": 1176, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 22.177542, - "auto": 0.386749, - "status": "measured", - "reduction_fraction": 0.9825612324395554, - "delta": -21.790793 - }, - "near_pool_geometry": { - "off": 72192960, - "auto": 66416512, - "status": "measured", - "delta": -5776448 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 260991168, - "auto": 305257792, - "status": "measured", - "delta": 44266624 - }, - "startup_worker_mesh_construction_ms": { - "off": 679.528887, - "auto": 624.595113, - "status": "measured", - "delta": -54.933774000000085 - } - } - }, - "traversal": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.845947265625, - "gpu_memory_mb_max": 689.845947265625, - "frames": 2482, - "duration_s": 5, - "fps": { - "min": 43.9013557434082, - "avg": 496.1322090659925, - "max": 11543.0791015625, - "p1": 206.89385635375976, - "p5": 221.46411361694337, - "p50": 1093.4823608398438, - "p95": 8568.1763671875, - "p99": 10530.456093750001 - }, - "frame_ms": { - "min": 0.08663199841976166, - "avg": 2.015591775189476, - "max": 22.778339385986328, - "p1": 0.0949627098441124, - "p5": 0.11671095006167889, - "p50": 0.9145095348358154, - "p95": 4.515404391288757, - "p99": 4.833397426605225 - }, - "max_frame_ms": 22.778339385986328, - "cpu_ms_avg": 2.015591775189476, - "gpu_ms": { - "shadow_avg": 0.0859220708982297, - "opaque_avg": 0.5778288967084924, - "lod_terrain_avg": 0.1645816278303947, - "lod_water_avg": 0.018543513294659843, - "lod_culling_avg": 0, - "total_avg": 1.5241801912605906, - "total": { - "min": 0, - "avg": 1.5241801912605906, - "max": 3.863840103149414, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.7165461659431456, - "p99": 3.7992799735069274 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 927.2429492344883, - "vertices_avg": 2307856.82272361, - "chunks_rendered_avg": 370.585414987913, - "worst_frame": { - "frame_index": 572, - "frame_ms": 22.778339385986328, - "gpu_total_ms": 3.4709601402282715, - "gpu_lod_terrain_ms": 0.3853600025177002, - "gpu_lod_water_ms": 0.009159999899566174, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.3977999687194824, - "lod_cpu_ms": 0.47183700000017836, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.35961600000007365, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 77869056, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 28, - "lod_rejected_count": 84, - "lod_coverage_count": 28, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 41 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2482, - "cpu_frame_ms": { - "min": 0.003857000032439828, - "avg": 0.32499402393892746, - "max": 20.791183471679688, - "p1": 0.004348000045865774, - "p5": 0.004959499812684953, - "p50": 0.2863284945487976, - "p95": 0.7405363857746121, - "p99": 0.9337444108724597 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.18312514105648114, - "max": 0.48539999127388, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.4476360023021698, - "p99": 0.4817103776335716 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 653.9631589999999, - "avg_ms": 0.2634823364222401 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.116446, - "avg_ms": 4.691619661563255e-5 - }, - "upload_prep": { - "total_ms": 6.9330929999999995, - "avg_ms": 0.002793349315068493 - }, - "upload_submission": { - "total_ms": 59.120586, - "avg_ms": 0.023819736502820307 - }, - "visibility": { - "total_ms": 51.65451499999999, - "avg_ms": 0.02081164987912973 - }, - "coverage": { - "total_ms": 46.39812900000001, - "avg_ms": 0.018693847300564066 - }, - "eviction": { - "total_ms": 3.2530160000000006, - "avg_ms": 0.0013106430298146657 - }, - "manager_lock_wait": { - "total_ms": 0.8590209999999998, - "avg_ms": 0.00034610032232070903 - }, - "manager_lock_hold": { - "total_ms": 145.997272, - "avg_ms": 0.05882243029814666 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 102296832, - "upload_avg_bytes": 41215.48428686543, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 78245934.20789686, - "max_bytes": 86842560, - "last_bytes": 75742464, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 246460.08058017728, - "max_bytes": 5778432, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 246460.08058017728, - "max_bytes": 5778432, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 68258868.39645447, - "max_bytes": 74087808, - "last_bytes": 64576320, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 32404427.603545528, - "max_bytes": 36086976, - "last_bytes": 36086976, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 382267298.2425463, - "max_bytes": 400478755, - "last_bytes": 376263622, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 178909230.20789686, - "max_bytes": 187505856, - "last_bytes": 176405760, - "p50_bytes": 176405760, - "p95_bytes": 187505856, - "p99_bytes": 187505856 - }, - "logical_vram_bytes": { - "avg_bytes": 106152636.08058017, - "max_bytes": 111684608, - "last_bytes": 105906176, - "p50_bytes": 105906176, - "p95_bytes": 107938688, - "p99_bytes": 109856576 - } - }, - "visibility": { - "visible_total": 22414, - "rejected_total": 95258, - "coverage_total": 22414, - "levels": [ - { - "candidates": 5697, - "accepted": 1756, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 3941, - "rejected_chunk_coverage": 0, - "coverage_checks": 1756 - }, - { - "candidates": 63874, - "accepted": 2391, - "rejected_no_draw": 0, - "rejected_not_ready": 44831, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 16652, - "rejected_chunk_coverage": 0, - "coverage_checks": 2391 - }, - { - "candidates": 17889, - "accepted": 3859, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 14030, - "rejected_chunk_coverage": 0, - "coverage_checks": 3859 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30212, - "accepted": 14408, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15804, - "rejected_chunk_coverage": 0, - "coverage_checks": 14408 - } - ] - }, - "pressure": { - "staging_pressure_total": 102626, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000567436218262, - "upload_total_bytes": 268544256, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5469.700645, - "worker_mesh_construction_total_ms": 658.913212, - "worker_far_expanded_mesh_construction_ms": 23.691521, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 74087808, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.002696514129639, - "requested_duration_s": 5, - "sampled_frame_count": 2482, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 2541, - "duration_s": 5, - "fps": { - "min": 44.96759796142578, - "avg": 507.83055899871556, - "max": 11475.390625, - "p1": 207.59676513671877, - "p5": 222.84242248535156, - "p50": 1198.6556396484375, - "p95": 8959.7705078125, - "p99": 10636.897070312498 - }, - "frame_ms": { - "min": 0.08714300394058228, - "avg": 1.9691607412749836, - "max": 22.23823356628418, - "p1": 0.09401239901781082, - "p5": 0.11161000281572342, - "p50": 0.8342679738998413, - "p95": 4.487475872039795, - "p99": 4.8170310974121096 - }, - "max_frame_ms": 22.23823356628418, - "cpu_ms_avg": 1.9691607412749836, - "gpu_ms": { - "shadow_avg": 0.08502999994286586, - "opaque_avg": 0.5742008976934464, - "lod_terrain_avg": 0.16234521846375283, - "lod_water_avg": 0.019463250679079412, - "lod_culling_avg": 0, - "total_avg": 1.5101331068464547, - "total": { - "min": 0, - "avg": 1.5101331068464547, - "max": 3.9170799255371094, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.736599922180176, - "p99": 3.809071922302246 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.16227504132489246, - "max": 0.4055199921131134, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3958800137042999, - "p99": 0.40315199494361875 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.019392144839623513, - "max": 0.0868000015616417, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.07620000094175339, - "p99": 0.07995199859142302 - } - }, - "draw_calls_avg": 929.7233372687919, - "vertices_avg": 2305162.3636363638, - "chunks_rendered_avg": 370.60881542699724, - "worst_frame": { - "frame_index": 1187, - "frame_ms": 22.23823356628418, - "gpu_total_ms": 3.6388001441955566, - "gpu_lod_terrain_ms": 0.3967599868774414, - "gpu_lod_water_ms": 0.07767999917268753, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.3217999935150146, - "lod_cpu_ms": 0.5838380000000143, - "dominant_lod_cpu_category": "scheduling", - "dominant_lod_cpu_category_ms": 0.4780479999999443, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43783488, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 17, - "lod_rejected_count": 66, - "lod_coverage_count": 17, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2541, - "cpu_frame_ms": { - "min": 0.003025999991223216, - "avg": 0.29364178042594213, - "max": 19.009004592895508, - "p1": 0.003527000080794096, - "p5": 0.003976999782025814, - "p50": 0.27960601449012756, - "p95": 0.6701599955558777, - "p99": 0.8939743995666503 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1818084691932285, - "max": 0.4883599877357483, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.46855998039245605, - "p99": 0.4819999933242798 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 621.2910639999999, - "avg_ms": 0.24450651869342774 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.1173519999999999, - "avg_ms": 4.618339236521051e-5 - }, - "upload_prep": { - "total_ms": 4.939024999999999, - "avg_ms": 0.0019437327823691457 - }, - "upload_submission": { - "total_ms": 38.38460600000002, - "avg_ms": 0.01510610232192051 - }, - "visibility": { - "total_ms": 50.45694499999999, - "avg_ms": 0.019857121212121208 - }, - "coverage": { - "total_ms": 45.441856999999985, - "avg_ms": 0.01788345415190869 - }, - "eviction": { - "total_ms": 2.570843, - "avg_ms": 0.0010117445887445886 - }, - "manager_lock_wait": { - "total_ms": 0.8730919999999998, - "avg_ms": 0.0003436017316017315 - }, - "manager_lock_hold": { - "total_ms": 221.684642, - "avg_ms": 0.0872430704447068 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 71534208, - "upload_avg_bytes": 28151.990554899647, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 47359590.309327036, - "max_bytes": 52910976, - "last_bytes": 43783488, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 107377.49232585596, - "max_bytes": 4218240, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 107377.49232585596, - "max_bytes": 4218240, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 62535596.051948056, - "max_bytes": 65715136, - "last_bytes": 61726528, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 105236563.94805194, - "max_bytes": 107769408, - "last_bytes": 106045632, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 388237395.03974813, - "max_bytes": 399279492, - "last_bytes": 382168405, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 148022886.30932704, - "max_bytes": 153574272, - "last_bytes": 144446784, - "p50_bytes": 144446784, - "p95_bytes": 153574272, - "p99_bytes": 153574272 - }, - "logical_vram_bytes": { - "avg_bytes": 234988401.49232584, - "max_bytes": 239099264, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 235434368, - "p99_bytes": 237107456 - } - }, - "visibility": { - "visible_total": 22997, - "rejected_total": 72006, - "coverage_total": 22999, - "levels": [ - { - "candidates": 2621, - "accepted": 1371, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1250, - "rejected_chunk_coverage": 0, - "coverage_checks": 1371 - }, - { - "candidates": 46476, - "accepted": 3712, - "rejected_no_draw": 0, - "rejected_not_ready": 27344, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15418, - "rejected_chunk_coverage": 2, - "coverage_checks": 3714 - }, - { - "candidates": 15302, - "accepted": 3307, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11995, - "rejected_chunk_coverage": 0, - "coverage_checks": 3307 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30604, - "accepted": 14607, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15997, - "rejected_chunk_coverage": 0, - "coverage_checks": 14607 - } - ] - }, - "pressure": { - "staging_pressure_total": 63562, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 101904 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00096321105957, - "upload_total_bytes": 294608896, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5368.528002, - "worker_mesh_construction_total_ms": 624.550607, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.439669, - "compact_submissions": 72690, - "pool_gpu_allocated_bytes": 65715136, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.0036396980285645, - "requested_duration_s": 5, - "sampled_frame_count": 2541, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 23.691521, - "auto": 0.439669, - "status": "measured", - "reduction_fraction": 0.9814419259953804, - "delta": -23.251852000000003 - }, - "near_pool_geometry": { - "off": 74087808, - "auto": 65715136, - "status": "measured", - "delta": -8372672 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 268544256, - "auto": 294608896, - "status": "measured", - "delta": 26064640 - }, - "startup_worker_mesh_construction_ms": { - "off": 658.913212, - "auto": 624.550607, - "status": "measured", - "delta": -34.36260500000003 - } - } - }, - "rapid-turn": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.845947265625, - "gpu_memory_mb_max": 689.845947265625, - "frames": 1373, - "duration_s": 5, - "fps": { - "min": 172.8307647705078, - "avg": 274.52016948910654, - "max": 550.4349975585938, - "p1": 187.9441394042969, - "p5": 196.84291992187502, - "p50": 276.57421875, - "p95": 427.32954101562495, - "p99": 470.8325329589843 - }, - "frame_ms": { - "min": 1.8167450428009033, - "avg": 3.6427195927390024, - "max": 5.786006927490234, - "p1": 2.123914289474487, - "p5": 2.3401146888732907, - "p50": 3.615665912628174, - "p95": 5.080193519592285, - "p99": 5.32072998046875 - }, - "max_frame_ms": 5.786006927490234, - "cpu_ms_avg": 3.6427195927390024, - "gpu_ms": { - "shadow_avg": 0.21874337926105436, - "opaque_avg": 1.2229320035307232, - "lod_terrain_avg": 0.4064329504228732, - "lod_water_avg": 0.01105581938702198, - "lod_culling_avg": 0, - "total_avg": 3.3198102989599643, - "total": { - "min": 2.3350398540496826, - "avg": 3.3198102989599643, - "max": 4.499599933624268, - "p1": 2.354040002822876, - "p5": 2.416408061981201, - "p50": 3.238760232925415, - "p95": 4.4120080947875975, - "p99": 4.472540645599365 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 2218.085214857975, - "vertices_avg": 1960568.499635834, - "chunks_rendered_avg": 375.489439184268, - "worst_frame": { - "frame_index": 244, - "frame_ms": 5.786006927490234, - "gpu_total_ms": 4.312359809875488, - "gpu_lod_terrain_ms": 0.4994400143623352, - "gpu_lod_water_ms": 0.023560000583529472, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5964000225067139, - "lod_cpu_ms": 0.05494299999982388, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.04775899999999922, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 87059136, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 32, - "lod_rejected_count": 91, - "lod_coverage_count": 32, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 46 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1373, - "cpu_frame_ms": { - "min": 0.03507599979639053, - "avg": 0.2565847365816449, - "max": 0.6485689878463745, - "p1": 0.040133319646120075, - "p5": 0.044336999952793124, - "p50": 0.10368400067090988, - "p95": 0.5587703943252562, - "p99": 0.6037136626243591 - }, - "gpu_frame_ms": { - "min": 0.3096800148487091, - "avg": 0.41748876953246555, - "max": 0.5550800561904907, - "p1": 0.31371038913726806, - "p5": 0.3258320152759552, - "p50": 0.4004000127315521, - "p95": 0.5300960183143616, - "p99": 0.5488399863243103 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 262.1431299999999, - "avg_ms": 0.19092726147123082 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.073571, - "avg_ms": 5.358412235979606e-5 - }, - "upload_prep": { - "total_ms": 4.188067, - "avg_ms": 0.0030503037144938095 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 65.00543499999999, - "avg_ms": 0.04734554624908958 - }, - "coverage": { - "total_ms": 56.87308700000001, - "avg_ms": 0.04142249599417335 - }, - "eviction": { - "total_ms": 1.6563879999999997, - "avg_ms": 0.0012064005826656955 - }, - "manager_lock_wait": { - "total_ms": 0.7199120000000003, - "avg_ms": 0.0005243350327749456 - }, - "manager_lock_hold": { - "total_ms": 170.432844, - "avg_ms": 0.12413171449380916 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 87059136, - "max_bytes": 87059136, - "last_bytes": 87059136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 72745344, - "max_bytes": 72745344, - "last_bytes": 72745344, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 27917952, - "max_bytes": 27917952, - "last_bytes": 27917952, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5242880, - "max_bytes": 5242880, - "last_bytes": 5242880, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 400044109, - "max_bytes": 400044109, - "last_bytes": 400044109, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 187722432, - "max_bytes": 187722432, - "last_bytes": 187722432, - "p50_bytes": 187722432, - "p95_bytes": 187722432, - "p99_bytes": 187722432 - }, - "logical_vram_bytes": { - "avg_bytes": 105906176, - "max_bytes": 105906176, - "last_bytes": 105906176, - "p50_bytes": 105906176, - "p95_bytes": 105906176, - "p99_bytes": 105906176 - } - }, - "visibility": { - "visible_total": 46114, - "rejected_total": 122765, - "coverage_total": 46114, - "levels": [ - { - "candidates": 20595, - "accepted": 7261, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 13334, - "rejected_chunk_coverage": 0, - "coverage_checks": 7261 - }, - { - "candidates": 86499, - "accepted": 9109, - "rejected_no_draw": 0, - "rejected_not_ready": 63158, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 14232, - "rejected_chunk_coverage": 0, - "coverage_checks": 9109 - }, - { - "candidates": 23341, - "accepted": 11519, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 11822, - "rejected_chunk_coverage": 0, - "coverage_checks": 11519 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 38444, - "accepted": 18225, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20219, - "rejected_chunk_coverage": 0, - "coverage_checks": 18225 - } - ] - }, - "pressure": { - "staging_pressure_total": 63158, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.00129508972168, - "upload_total_bytes": 265804224, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5503.10483, - "worker_mesh_construction_total_ms": 651.393855, - "worker_far_expanded_mesh_construction_ms": 17.555281, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 72745344, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001452922821045, - "requested_duration_s": 5, - "sampled_frame_count": 1373, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "rapid-turn", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 1374, - "duration_s": 5, - "fps": { - "min": 170.5203094482422, - "avg": 274.6797719867, - "max": 545.2063598632812, - "p1": 185.25044860839841, - "p5": 196.55439605712888, - "p50": 276.25950622558594, - "p95": 429.8601562499999, - "p99": 474.99169616699214 - }, - "frame_ms": { - "min": 1.8341679573059082, - "avg": 3.640602992958724, - "max": 5.864404201507568, - "p1": 2.1053041982650758, - "p5": 2.3263444542884826, - "p50": 3.619784951210022, - "p95": 5.087650108337402, - "p99": 5.398097462654114 - }, - "max_frame_ms": 5.864404201507568, - "cpu_ms_avg": 3.640602992958724, - "gpu_ms": { - "shadow_avg": 0.21849913423368503, - "opaque_avg": 1.2221394766886682, - "lod_terrain_avg": 0.40564864594082645, - "lod_water_avg": 0.011743609910577352, - "lod_culling_avg": 0, - "total_avg": 3.317318278629186, - "total": { - "min": 2.3347201347351074, - "avg": 3.317318278629186, - "max": 4.500400066375732, - "p1": 2.3534930992126464, - "p5": 2.416252017021179, - "p50": 3.237239956855774, - "p95": 4.40301034450531, - "p99": 4.4709290027618405 - }, - "lod_compact_terrain": { - "min": 0.3062399923801422, - "avg": 0.4054847453142011, - "max": 0.5277600288391113, - "p1": 0.31024759948253633, - "p5": 0.3210059881210327, - "p50": 0.39573998749256134, - "p95": 0.5035880029201507, - "p99": 0.5217264074087142 - }, - "lod_compact_water": { - "min": 0.002360000042244792, - "avg": 0.011578457059577281, - "max": 0.07716000080108643, - "p1": 0.002520000096410513, - "p5": 0.0026400000788271427, - "p50": 0.004140000091865659, - "p95": 0.03337400015443563, - "p99": 0.06188279829919335 - } - }, - "draw_calls_avg": 2244.6062590975253, - "vertices_avg": 1960022.1004366812, - "chunks_rendered_avg": 375.4912663755459, - "worst_frame": { - "frame_index": 256, - "frame_ms": 5.864404201507568, - "gpu_total_ms": 4.295919895172119, - "gpu_lod_terrain_ms": 0.4960800111293793, - "gpu_lod_water_ms": 0.024480000138282776, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.5948400497436523, - "lod_cpu_ms": 0.05737699999995982, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.05168700000000115, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 43378560, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 27, - "lod_rejected_count": 56, - "lod_coverage_count": 27, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 23 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 1374, - "cpu_frame_ms": { - "min": 0.030466999858617783, - "avg": 0.2391349640898569, - "max": 0.6051470041275024, - "p1": 0.033031199164688586, - "p5": 0.038061998784542084, - "p50": 0.20986950770020485, - "p95": 0.5276125103235245, - "p99": 0.5625341874361038 - }, - "gpu_frame_ms": { - "min": 0.30928000807762146, - "avg": 0.4173922556326379, - "max": 0.5557999610900879, - "p1": 0.31321839451789857, - "p5": 0.3246640026569367, - "p50": 0.3997200131416321, - "p95": 0.5300800204277039, - "p99": 0.5499715882539749 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 255.04924000000005, - "avg_ms": 0.1856253566229986 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0.07941500000000001, - "avg_ms": 5.779839883551675e-5 - }, - "upload_prep": { - "total_ms": 2.594778, - "avg_ms": 0.001888484716157205 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 55.38022499999998, - "avg_ms": 0.0403058406113537 - }, - "coverage": { - "total_ms": 49.052981999999986, - "avg_ms": 0.03570086026200872 - }, - "eviction": { - "total_ms": 1.3640599999999985, - "avg_ms": 0.0009927656477438126 - }, - "manager_lock_wait": { - "total_ms": 0.7176900000000002, - "avg_ms": 0.0005223362445414848 - }, - "manager_lock_hold": { - "total_ms": 255.65183199999996, - "avg_ms": 0.18606392430858804 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 43378560, - "max_bytes": 43378560, - "last_bytes": 43378560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 59821696, - "max_bytes": 59821696, - "last_bytes": 59821696, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 107950464, - "max_bytes": 107950464, - "last_bytes": 107950464, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 161728, - "max_bytes": 161728, - "last_bytes": 161728, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66947136, - "max_bytes": 66947136, - "last_bytes": 66947136, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 379888149, - "max_bytes": 379888149, - "last_bytes": 379888149, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 144041856, - "max_bytes": 144041856, - "last_bytes": 144041856, - "p50_bytes": 144041856, - "p95_bytes": 144041856, - "p99_bytes": 144041856 - }, - "logical_vram_bytes": { - "avg_bytes": 234881024, - "max_bytes": 234881024, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 234881024 - } - }, - "visibility": { - "visible_total": 36687, - "rejected_total": 77355, - "coverage_total": 36687, - "levels": [ - { - "candidates": 2748, - "accepted": 918, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 1830, - "rejected_chunk_coverage": 0, - "coverage_checks": 918 - }, - { - "candidates": 54960, - "accepted": 8094, - "rejected_no_draw": 0, - "rejected_not_ready": 31602, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 15264, - "rejected_chunk_coverage": 0, - "coverage_checks": 8094 - }, - { - "candidates": 17862, - "accepted": 9437, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 8425, - "rejected_chunk_coverage": 0, - "coverage_checks": 9437 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 38472, - "accepted": 18238, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 20234, - "rejected_chunk_coverage": 0, - "coverage_checks": 18238 - } - ] - }, - "pressure": { - "staging_pressure_total": 31602, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 108416 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000357627868652, - "upload_total_bytes": 329082112, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5372.631438, - "worker_mesh_construction_total_ms": 623.011737, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0.447592, - "compact_submissions": 71940, - "pool_gpu_allocated_bytes": 59821696, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.002184867858887, - "requested_duration_s": 5, - "sampled_frame_count": 1374, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 17.555281, - "auto": 0.447592, - "status": "measured", - "reduction_fraction": 0.9745038544242043, - "delta": -17.107689 - }, - "near_pool_geometry": { - "off": 72745344, - "auto": 59821696, - "status": "measured", - "delta": -12923648 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 265804224, - "auto": 329082112, - "status": "measured", - "delta": 63277888 - }, - "startup_worker_mesh_construction_ms": { - "off": 651.393855, - "auto": 623.011737, - "status": "measured", - "delta": -28.38211799999999 - } - } - }, - "teleport-eviction": { - "off": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 689.9421774243551, - "gpu_memory_mb_max": 692.345947265625, - "frames": 2268, - "duration_s": 5, - "fps": { - "min": 19.8847599029541, - "avg": 453.3882267761799, - "max": 10451.5048828125, - "p1": 199.38819076538084, - "p5": 207.5246864318848, - "p50": 1121.2266845703125, - "p95": 7052.960424804689, - "p99": 9094.104345703121 - }, - "frame_ms": { - "min": 0.09567999839782715, - "avg": 2.2056152783465657, - "max": 50.28976821899414, - "p1": 0.10996236972510814, - "p5": 0.141784854978323, - "p50": 0.8918814957141876, - "p95": 4.818703722953797, - "p99": 5.015348615646362 - }, - "max_frame_ms": 50.28976821899414, - "cpu_ms_avg": 2.2056152783465657, - "gpu_ms": { - "shadow_avg": 0.0663054100676346, - "opaque_avg": 0.7414800184639253, - "lod_terrain_avg": 0.16274186956977088, - "lod_water_avg": 0.0020698765384130234, - "lod_culling_avg": 0, - "total_avg": 1.6350695201749315, - "total": { - "min": 0, - "avg": 1.6350695201749315, - "max": 3.729919910430908, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.7015460729599, - "p99": 3.7126389265060427 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 996.8108465608466, - "vertices_avg": 1899153.142857143, - "chunks_rendered_avg": 366.89726631393296, - "worst_frame": { - "frame_index": 930, - "frame_ms": 50.28976821899414, - "gpu_total_ms": 3.693079948425293, - "gpu_lod_terrain_ms": 0.3574399948120117, - "gpu_lod_water_ms": 0.004279999993741512, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6779199838638306, - "lod_cpu_ms": 3.6856380000001536, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 3.2742450000000005, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 85459392, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 45 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2268, - "cpu_frame_ms": { - "min": 0.003286000108346343, - "avg": 0.27708954026272137, - "max": 5.058097839355469, - "p1": 0.0037070000544190407, - "p5": 0.004037349857389927, - "p50": 0.2633100003004074, - "p95": 0.6283570021390915, - "p99": 0.9922805297374705 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.16481174627151438, - "max": 0.3696400225162506, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.36528000235557556, - "p99": 0.36613320887088774 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 513.600455, - "avg_ms": 0.22645522707231042 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.026311999999999988, - "avg_ms": 1.1601410934744262e-5 - }, - "state_transition": { - "total_ms": 0.456211, - "avg_ms": 0.00020115123456790123 - }, - "upload_prep": { - "total_ms": 6.237718000000001, - "avg_ms": 0.0027503165784832457 - }, - "upload_submission": { - "total_ms": 22.762549000000007, - "avg_ms": 0.010036397266313937 - }, - "visibility": { - "total_ms": 50.03030700000001, - "avg_ms": 0.022059218253968258 - }, - "coverage": { - "total_ms": 43.42770399999999, - "avg_ms": 0.0191480176366843 - }, - "eviction": { - "total_ms": 11.517005999999995, - "avg_ms": 0.0050780449735449715 - }, - "manager_lock_wait": { - "total_ms": 1.307705, - "avg_ms": 0.0005765895061728395 - }, - "manager_lock_hold": { - "total_ms": 141.15119900000002, - "avg_ms": 0.06223597839506174 - } - }, - "workers": { - "generation_total_ms": 1017.6671969999998, - "mesh_construction_total_ms": 116.847489, - "far_expanded_mesh_construction_total_ms": 7.422333000000002, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 127505856, - "upload_avg_bytes": 56219.51322751323, - "far_expanded_upload_total_bytes": 1713984, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 83059561.82010582, - "max_bytes": 85459392, - "last_bytes": 23625984, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 1369249.0158730159, - "max_bytes": 40462464, - "last_bytes": 15983040, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 1369249.0158730159, - "max_bytes": 40462464, - "last_bytes": 15983040, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 73205478.43386243, - "max_bytes": 93105984, - "last_bytes": 92012736, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 27457817.566137567, - "max_bytes": 37930176, - "last_bytes": 8650560, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 5343784.634920635, - "max_bytes": 7864320, - "last_bytes": 7864320, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 377338487.23015875, - "max_bytes": 396855479, - "last_bytes": 291433594, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 183722857.82010582, - "max_bytes": 186122688, - "last_bytes": 124289280, - "p50_bytes": 186122688, - "p95_bytes": 186122688, - "p99_bytes": 186122688 - }, - "logical_vram_bytes": { - "avg_bytes": 107376329.65079366, - "max_bytes": 146368640, - "last_bytes": 124510656, - "p50_bytes": 105906176, - "p95_bytes": 105906176, - "p99_bytes": 141891264 - } - }, - "visibility": { - "visible_total": 39158, - "rejected_total": 83340, - "coverage_total": 39158, - "levels": [ - { - "candidates": 15278, - "accepted": 4654, - "rejected_no_draw": 0, - "rejected_not_ready": 56, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 10568, - "rejected_chunk_coverage": 0, - "coverage_checks": 4654 - }, - { - "candidates": 58448, - "accepted": 8413, - "rejected_no_draw": 0, - "rejected_not_ready": 41641, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 8394, - "rejected_chunk_coverage": 0, - "coverage_checks": 8413 - }, - { - "candidates": 18630, - "accepted": 9120, - "rejected_no_draw": 0, - "rejected_not_ready": 2070, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 7440, - "rejected_chunk_coverage": 0, - "coverage_checks": 9120 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30142, - "accepted": 16971, - "rejected_no_draw": 0, - "rejected_not_ready": 23, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 13148, - "rejected_chunk_coverage": 0, - "coverage_checks": 16971 - } - ] - }, - "pressure": { - "staging_pressure_total": 99008, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 0 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.001251220703125, - "upload_total_bytes": 299877120, - "far_expanded_upload_bytes": 3689856, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 5524.686477, - "worker_mesh_construction_total_ms": 650.384364, - "worker_far_expanded_mesh_construction_ms": 23.700003, - "worker_compact_encode_ms": 0, - "compact_submissions": 0, - "pool_gpu_allocated_bytes": 72977664, - "direct_mesh_gpu_bytes": 5242880, - "compact_pool_allocated_bytes": 0, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.00233793258667, - "requested_duration_s": 5, - "sampled_frame_count": 2268, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "off" - }, - "auto": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "high", - "scenario": "teleport-eviction", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "overworld", - "fixture": "none", - "headless": true, - "resolution": [1920, 1080] - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 12, - "gpu_memory_mb_avg": 748.845947265625, - "gpu_memory_mb_max": 748.845947265625, - "frames": 2416, - "duration_s": 5, - "fps": { - "min": 19.90268325805664, - "avg": 483.0753789457763, - "max": 11064.39453125, - "p1": 205.51299819946288, - "p5": 212.64580917358398, - "p50": 1327.73681640625, - "p95": 7603.880859375, - "p99": 10025.670312499999 - }, - "frame_ms": { - "min": 0.09038000553846359, - "avg": 2.0700703111434025, - "max": 50.24448013305664, - "p1": 0.09974399842321872, - "p5": 0.13151249662041664, - "p50": 0.7531654834747314, - "p95": 4.702655792236328, - "p99": 4.865872168540954 - }, - "max_frame_ms": 50.24448013305664, - "cpu_ms_avg": 2.0700703111434025, - "gpu_ms": { - "shadow_avg": 0.06340334434617335, - "opaque_avg": 0.7087717379955266, - "lod_terrain_avg": 0.15568531466191574, - "lod_water_avg": 0.002088377484088048, - "lod_culling_avg": 0, - "total_avg": 1.5621965738716503, - "total": { - "min": 0, - "avg": 1.5621965738716503, - "max": 3.7239999771118164, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 3.69569993019104, - "p99": 3.7053137063980106 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.15561342721693958, - "max": 0.3646000027656555, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3604399859905243, - "p99": 0.36127999424934387 - }, - "lod_compact_water": { - "min": 0, - "avg": 0.002016258277214866, - "max": 0.005880000069737434, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.005119999870657921, - "p99": 0.005439999978989363 - } - }, - "draw_calls_avg": 968.1618377483444, - "vertices_avg": 1899477.690397351, - "chunks_rendered_avg": 366.90149006622516, - "worst_frame": { - "frame_index": 948, - "frame_ms": 50.24448013305664, - "gpu_total_ms": 3.7047996520996094, - "gpu_lod_terrain_ms": 0.3583199977874756, - "gpu_lod_water_ms": 0.005280000157654285, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "opaque", - "dominant_gpu_pass_ms": 1.6794400215148926, - "lod_cpu_ms": 3.5608640000000378, - "dominant_lod_cpu_category": "eviction", - "dominant_lod_cpu_category_ms": 3.2002560000000013, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 45507264, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 0, - "lod_rejected_count": 0, - "lod_coverage_count": 0, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 24 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 2416, - "cpu_frame_ms": { - "min": 0.0023539999965578318, - "avg": 0.24607392386115073, - "max": 4.519947052001953, - "p1": 0.002644999884068966, - "p5": 0.0029257499263621867, - "p50": 0.23351449519395828, - "p95": 0.5540564954280853, - "p99": 0.7593400090932846 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.1577736922101864, - "max": 0.36980000138282776, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0.3654400110244751, - "p99": 0.36636000871658325 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 498.1758249999999, - "avg_ms": 0.20619860306291388 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0.01467400000000002, - "avg_ms": 6.0736754966887505e-6 - }, - "state_transition": { - "total_ms": 0.1657129999999999, - "avg_ms": 6.858981788079466e-5 - }, - "upload_prep": { - "total_ms": 4.0039940000000005, - "avg_ms": 0.0016572822847682122 - }, - "upload_submission": { - "total_ms": 13.20260399999998, - "avg_ms": 0.005464653973509925 - }, - "visibility": { - "total_ms": 49.31938199999999, - "avg_ms": 0.02041365149006622 - }, - "coverage": { - "total_ms": 42.780249999999995, - "avg_ms": 0.017707057119205297 - }, - "eviction": { - "total_ms": 8.293956000000001, - "avg_ms": 0.0034329288079470205 - }, - "manager_lock_wait": { - "total_ms": 0.8972069999999999, - "avg_ms": 0.00037136051324503306 - }, - "manager_lock_hold": { - "total_ms": 216.54849299999995, - "avg_ms": 0.08963099875827812 - } - }, - "workers": { - "generation_total_ms": 526.6374000000005, - "mesh_construction_total_ms": 54.94961599999999, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0.11070800000000003 - }, - "memory_bytes": { - "upload_total_bytes": 77068048, - "upload_avg_bytes": 31899.026490066226, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 75088, - "pending_cpu_upload_bytes": { - "avg_bytes": 43605108.556291394, - "max_bytes": 45507264, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 1087210.6490066226, - "max_bytes": 36193344, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 1087210.6490066226, - "max_bytes": 36193344, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 167772160, - "max_bytes": 167772160, - "last_bytes": 167772160, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 68180585.01986755, - "max_bytes": 76949456, - "last_bytes": 63628304, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 99591574.98013245, - "max_bytes": 122308400, - "last_bytes": 104143856, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 100663296, - "max_bytes": 100663296, - "last_bytes": 100663296, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 164678.15894039735, - "max_bytes": 236816, - "last_bytes": 236816, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 66944185.8410596, - "max_bytes": 66947136, - "last_bytes": 66872048, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 371979232.5273179, - "max_bytes": 389349230, - "last_bytes": 306839754, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 144268404.5562914, - "max_bytes": 146170560, - "last_bytes": 100663296, - "p50_bytes": 146170560, - "p95_bytes": 146170560, - "p99_bytes": 146170560 - }, - "logical_vram_bytes": { - "avg_bytes": 235968234.64900663, - "max_bytes": 271074368, - "last_bytes": 234881024, - "p50_bytes": 234881024, - "p95_bytes": 234881024, - "p99_bytes": 271074368 - } - }, - "visibility": { - "visible_total": 43318, - "rejected_total": 55724, - "coverage_total": 43318, - "levels": [ - { - "candidates": 12228, - "accepted": 8118, - "rejected_no_draw": 0, - "rejected_not_ready": 48, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 4062, - "rejected_chunk_coverage": 0, - "coverage_checks": 8118 - }, - { - "candidates": 39154, - "accepted": 8605, - "rejected_no_draw": 0, - "rejected_not_ready": 22908, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 7641, - "rejected_chunk_coverage": 0, - "coverage_checks": 8605 - }, - { - "candidates": 16880, - "accepted": 9296, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 7584, - "rejected_chunk_coverage": 0, - "coverage_checks": 9296 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 30780, - "accepted": 17299, - "rejected_no_draw": 0, - "rejected_not_ready": 19, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 13462, - "rejected_chunk_coverage": 0, - "coverage_checks": 17299 - } - ] - }, - "pressure": { - "staging_pressure_total": 55520, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "compact_submissions": 98258 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.000935554504395, - "upload_total_bytes": 300926272, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 161728, - "worker_generation_total_ms": 5417.338126, - "worker_mesh_construction_total_ms": 636.438225, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 1.886576, - "compact_submissions": 63660, - "pool_gpu_allocated_bytes": 68260864, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 161728, - "pool_cpu_shadow_bytes": 100663296 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 5.001282215118408, - "requested_duration_s": 5, - "sampled_frame_count": 2416, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false - }, - "compact_mode": "auto" - }, - "comparison": { - "far_resident_geometry": { - "off": 5242880, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.96915283203125, - "delta": -5081152 - }, - "far_representation_upload_bytes": { - "off": 3689856, - "auto": 161728, - "status": "measured", - "reduction_fraction": 0.9561695632566691, - "delta": -3528128 - }, - "worker_far_representation_work": { - "off": 23.700003, - "auto": 1.886576, - "status": "measured", - "reduction_fraction": 0.9203976472070488, - "delta": -21.813426999999997 - }, - "near_pool_geometry": { - "off": 72977664, - "auto": 68260864, - "status": "measured", - "delta": -4716800 - }, - "cpu_expanded_shadow": { - "off": 100663296, - "auto": 100663296, - "status": "measured", - "delta": 0 - }, - "startup_upload_bytes": { - "off": 299877120, - "auto": 300926272, - "status": "measured", - "delta": 1049152 - }, - "startup_worker_mesh_construction_ms": { - "off": 650.384364, - "auto": 636.438225, - "status": "measured", - "delta": -13.946139000000016 - } - } - } - } - } -} diff --git a/docs/benchmarks/gpu-culling-baseline.json b/docs/benchmarks/gpu-culling-baseline.json deleted file mode 100644 index 7155bb26..00000000 --- a/docs/benchmarks/gpu-culling-baseline.json +++ /dev/null @@ -1,995 +0,0 @@ -{ - "schema_version": 3, - "artifact_type": "gpu-culling-baseline", - "status": "ready", - "generated": true, - "policy": { - "fixture": "gpu-culling-scale", - "minimum_duration_s": 60, - "minimum_horizon_distance_chunks": 4096, - "minimum_readiness_target": 1024, - "cpu_frame_p95_improvement_min_fraction": 0.01, - "cpu_frame_p99_regression_max_fraction": 0.01, - "total_gpu_p99_regression_max_fraction": 0.01 - }, - "sources": { - "cpu": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "extreme", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "flat", - "fixture": "gpu-culling-scale", - "headless": true, - "resolution": [1920, 1080], - "horizon_distance": 4096, - "benchmark_lod_memory_budget_mb": 2048, - "benchmark_require_gpu_candidates": 1024 - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 16, - "horizon_distance": 4096, - "gpu_memory_mb_avg": 652.845947265625, - "gpu_memory_mb_max": 652.845947265625, - "frames": 110363, - "duration_s": 60, - "fps": { - "min": 27.827564239501953, - "avg": 1839.1508020669025, - "max": 11949.287109375, - "p1": 80.90901992797852, - "p5": 2118.4865478515626, - "p50": 7520.4931640625, - "p95": 10620.55859375, - "p99": 11037.944082031256 - }, - "frame_ms": { - "min": 0.08368699997663498, - "avg": 0.5437292031062188, - "max": 35.93559265136719, - "p1": 0.09059657871723176, - "p5": 0.09415700286626816, - "p50": 0.13297000527381897, - "p95": 0.47203511595726005, - "p99": 12.359561996459977 - }, - "max_frame_ms": 35.93559265136719, - "cpu_ms_avg": 0.5437292031062188, - "gpu_ms": { - "shadow_avg": 0.016861780225891992, - "opaque_avg": 0.07497122472392162, - "lod_terrain_avg": 0.011290253428003648, - "lod_water_avg": 0.00034602248954778743, - "lod_culling_avg": 0, - "total_avg": 0.20336361715071483, - "total": { - "min": 0, - "avg": 0.20336361715071483, - "max": 10.813119888305664, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 8.394708957672137 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.011286136835693895, - "max": 0.9608799815177917, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0.525895177125931 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_culling": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - } - }, - "draw_calls_avg": 96.47450685465238, - "vertices_avg": 3472099.6769750733, - "chunks_rendered_avg": 610.0998432445657, - "worst_frame": { - "frame_index": 93969, - "frame_ms": 35.93559265136719, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.09360600000013619, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.07679400000006353, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 12, - "lod_rejected_count": 1012, - "lod_coverage_count": 12, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 110363, - "cpu_frame_ms": { - "min": 0.010439000092446804, - "avg": 0.02018210913885682, - "max": 2.5031158924102783, - "p1": 0.01119100023061037, - "p5": 0.011491999961435795, - "p50": 0.014476999640464783, - "p95": 0.018824100121855685, - "p99": 0.27019598782062537 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.011636275935295957, - "max": 0.9819200038909912, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0.5594399797916412 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 0, - "avg_ms": 0 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0, - "avg_ms": 0 - }, - "upload_prep": { - "total_ms": 0, - "avg_ms": 0 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 629.0684790000001, - "avg_ms": 0.005699994373114179 - }, - "coverage": { - "total_ms": 493.73936, - "avg_ms": 0.004473776174986181 - }, - "eviction": { - "total_ms": 0, - "avg_ms": 0 - }, - "manager_lock_wait": { - "total_ms": 17.273296, - "avg_ms": 0.00015651346918804307 - }, - "manager_lock_hold": { - "total_ms": 2711.88051, - "avg_ms": 0.024572370359631397 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 5914624, - "max_bytes": 5914624, - "last_bytes": 5914624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 61194240, - "max_bytes": 61194240, - "last_bytes": 61194240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 5914624, - "max_bytes": 5914624, - "last_bytes": 5914624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 61194240, - "max_bytes": 61194240, - "last_bytes": 61194240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "source_data_cpu_bytes": { - "avg_bytes": 17164288, - "max_bytes": 17164288, - "last_bytes": 17164288, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 151382016, - "max_bytes": 151382016, - "last_bytes": 151382016, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 17164288, - "max_bytes": 17164288, - "last_bytes": 17164288, - "p50_bytes": 17164288, - "p95_bytes": 17164288, - "p99_bytes": 17164288 - }, - "logical_vram_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 134217728, - "p95_bytes": 134217728, - "p99_bytes": 134217728 - } - }, - "visibility": { - "visible_total": 717809, - "rejected_total": 2137103, - "coverage_total": 717809, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2854912, - "accepted": 717809, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 2137103, - "rejected_chunk_coverage": 0, - "coverage_checks": 717809 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "gpu_culling": { - "requested": false, - "threshold": 128, - "candidate_count_total": 0, - "candidate_count_max": 0, - "draw_submission_count_total": 0, - "validation_generation": 0, - "validation_completed_generation": 0, - "validation_completed_count": 0 - }, - "compact_submissions": 1163969 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.002833366394043, - "upload_total_bytes": 0, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 0, - "worker_mesh_construction_total_ms": 0, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0, - "compact_submissions": 446160, - "pool_gpu_allocated_bytes": 5914624, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 5914624, - "pool_cpu_shadow_bytes": 0, - "lod_renderable_regions": 1024, - "gpu_culling_candidate_count": 0 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.00758361816406, - "requested_duration_s": 60, - "sampled_frame_count": 110363, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false, - "lod_renderable_region_target": 1024, - "gpu_candidate_target": 1024 - }, - "compact_mode": "auto" - }, - "gpu": { - "schema_version": 3, - "artifact_type": "benchmark-result", - "preset": "extreme", - "scenario": "traversal", - "world_seed": 12345, - "build": { - "mode": "ReleaseFast", - "world": "flat", - "fixture": "gpu-culling-scale", - "headless": true, - "resolution": [1920, 1080], - "horizon_distance": 4096, - "benchmark_lod_memory_budget_mb": 2048, - "benchmark_require_gpu_candidates": 1024 - }, - "provenance": { - "gpu_adapter": "AMD Radeon RX 5700 XT", - "gpu_driver": "Mesa RADV 25.2.6", - "runner": "local NixOS x86_64", - "zig_toolchain": "Zig 0.16.0 pinned Nix flake" - }, - "render_distance": 16, - "horizon_distance": 4096, - "gpu_memory_mb_avg": 661.4398193359375, - "gpu_memory_mb_max": 661.4398193359375, - "frames": 110916, - "duration_s": 60, - "fps": { - "min": 25.9842472076416, - "avg": 1848.1974625787664, - "max": 12031.3779296875, - "p1": 80.78307991027833, - "p5": 2166.705322265625, - "p50": 7548.36572265625, - "p95": 10641.0146484375, - "p99": 11075.5458984375 - }, - "frame_ms": { - "min": 0.08311600238084793, - "avg": 0.5410677269325501, - "max": 38.48485565185547, - "p1": 0.09028900414705276, - "p5": 0.09397599846124649, - "p50": 0.1324790120124817, - "p95": 0.46153025329113007, - "p99": 12.378830385208136 - }, - "max_frame_ms": 38.48485565185547, - "cpu_ms_avg": 0.5410677269325501, - "gpu_ms": { - "shadow_avg": 0.016139028727236583, - "opaque_avg": 0.07698180498577806, - "lod_terrain_avg": 0.015723274541034886, - "lod_water_avg": 0.00032894478730695554, - "lod_culling_avg": 8.294204604681853e-6, - "total_avg": 0.2001898994980263, - "total": { - "min": 0, - "avg": 0.2001898994980263, - "max": 10.846480369567871, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 8.452523183822647 - }, - "lod_compact_terrain": { - "min": 0, - "avg": 0.010696470113955804, - "max": 0.9539200067520142, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0.5171599984169006 - }, - "lod_compact_water": { - "min": 0, - "avg": 0, - "max": 0, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0 - }, - "lod_culling": { - "min": 0, - "avg": 8.294204604681853e-6, - "max": 0.0003600000054575503, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0.0003600000054575503 - } - }, - "draw_calls_avg": 86.52070936564607, - "vertices_avg": 3435886.6736990153, - "chunks_rendered_avg": 608.660229362761, - "worst_frame": { - "frame_index": 94799, - "frame_ms": 38.48485565185547, - "gpu_total_ms": 0, - "gpu_lod_terrain_ms": 0, - "gpu_lod_water_ms": 0, - "gpu_lod_culling_ms": 0, - "dominant_gpu_pass": "shadow", - "dominant_gpu_pass_ms": 0, - "lod_cpu_ms": 0.7820900000003803, - "dominant_lod_cpu_category": "visibility", - "dominant_lod_cpu_category_ms": 0.7610600000002705, - "lod_worker_generation_ms": 0, - "lod_worker_mesh_construction_ms": 0, - "lod_upload_bytes": 0, - "lod_pending_cpu_upload_bytes": 0, - "lod_deferred_deletion_bytes": 0, - "lod_visible_count": 1024, - "lod_rejected_count": 0, - "lod_coverage_count": 1024, - "lod_wait_idle_count": 0, - "lod_wait_idle_ms": 0, - "lod_staging_pressure_count": 0 - }, - "lod": { - "profiling_enabled": true, - "profiling_frame_count": 110916, - "cpu_frame_ms": { - "min": 0.01028900034725666, - "avg": 0.033305473302111914, - "max": 1.5304969549179077, - "p1": 0.010979999788105488, - "p5": 0.011301999911665916, - "p50": 0.014177000150084496, - "p95": 0.018735000863671303, - "p99": 0.7882990002632142 - }, - "gpu_frame_ms": { - "min": 0, - "avg": 0.0160605137702699, - "max": 1.1768399477005005, - "p1": 0, - "p5": 0, - "p50": 0, - "p95": 0, - "p99": 0.759120038151741 - }, - "cpu_categories": { - "scheduling": { - "total_ms": 0, - "avg_ms": 0 - }, - "cache": { - "total_ms": 0, - "avg_ms": 0 - }, - "generation_dispatch": { - "total_ms": 0, - "avg_ms": 0 - }, - "state_transition": { - "total_ms": 0, - "avg_ms": 0 - }, - "upload_prep": { - "total_ms": 0, - "avg_ms": 0 - }, - "upload_submission": { - "total_ms": 0, - "avg_ms": 0 - }, - "visibility": { - "total_ms": 2119.439061, - "avg_ms": 0.019108506085686466 - }, - "coverage": { - "total_ms": 1603.168095, - "avg_ms": 0.014453893892675538 - }, - "eviction": { - "total_ms": 0, - "avg_ms": 0 - }, - "manager_lock_wait": { - "total_ms": 17.979688, - "avg_ms": 0.00016210184283602005 - }, - "manager_lock_hold": { - "total_ms": 4607.113364, - "avg_ms": 0.04153695917631361 - } - }, - "workers": { - "generation_total_ms": 0, - "mesh_construction_total_ms": 0, - "far_expanded_mesh_construction_total_ms": 0, - "compact_encode_total_ms": 0 - }, - "memory_bytes": { - "upload_total_bytes": 0, - "upload_avg_bytes": 0, - "far_expanded_upload_total_bytes": 0, - "compact_upload_total_bytes": 0, - "pending_cpu_upload_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "deferred_deletion_cpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_allocated_bytes": { - "avg_bytes": 5914624, - "max_bytes": 5914624, - "last_bytes": 5914624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_gpu_slack_bytes": { - "avg_bytes": 61194240, - "max_bytes": 61194240, - "last_bytes": 61194240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "pool_cpu_shadow_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_capacity_bytes": { - "avg_bytes": 67108864, - "max_bytes": 67108864, - "last_bytes": 67108864, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_allocated_bytes": { - "avg_bytes": 5914624, - "max_bytes": 5914624, - "last_bytes": 5914624, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_free_bytes": { - "avg_bytes": 61194240, - "max_bytes": 61194240, - "last_bytes": 61194240, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "compact_pool_retired_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "direct_mesh_gpu_bytes": { - "avg_bytes": 0, - "max_bytes": 0, - "last_bytes": 0, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "source_data_cpu_bytes": { - "avg_bytes": 17164288, - "max_bytes": 17164288, - "last_bytes": 17164288, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "known_memory_bytes": { - "avg_bytes": 151382016, - "max_bytes": 151382016, - "last_bytes": 151382016, - "p50_bytes": 0, - "p95_bytes": 0, - "p99_bytes": 0 - }, - "logical_ram_bytes": { - "avg_bytes": 17164288, - "max_bytes": 17164288, - "last_bytes": 17164288, - "p50_bytes": 17164288, - "p95_bytes": 17164288, - "p99_bytes": 17164288 - }, - "logical_vram_bytes": { - "avg_bytes": 134217728, - "max_bytes": 134217728, - "last_bytes": 134217728, - "p50_bytes": 134217728, - "p95_bytes": 134217728, - "p99_bytes": 134217728 - } - }, - "visibility": { - "visible_total": 2757632, - "rejected_total": 0, - "coverage_total": 2757632, - "levels": [ - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 0, - "accepted": 0, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 0 - }, - { - "candidates": 2757632, - "accepted": 2757632, - "rejected_no_draw": 0, - "rejected_not_ready": 0, - "rejected_missing_region": 0, - "rejected_not_renderable": 0, - "rejected_finer_coverage": 0, - "rejected_range": 0, - "rejected_frustum": 0, - "rejected_chunk_coverage": 0, - "coverage_checks": 2757632 - } - ] - }, - "pressure": { - "staging_pressure_total": 0, - "wait_idle_count_total": 0, - "wait_idle_ms_total": 0, - "wait_idle_ms_avg": 0, - "gpu_culling_overflows_max": 0, - "gpu_culling_validation_mismatches_max": 0 - }, - "gpu_culling": { - "requested": true, - "threshold": 128, - "candidate_count_total": 113577984, - "candidate_count_max": 1024, - "draw_submission_count_total": 2693, - "validation_generation": 3937, - "validation_completed_generation": 3935, - "validation_completed_count": 3935 - }, - "compact_submissions": 3937 - }, - "startup_evidence": { - "readiness_observed": true, - "readiness_elapsed_s": 10.006616592407227, - "upload_total_bytes": 0, - "far_expanded_upload_bytes": 0, - "compact_upload_bytes": 0, - "worker_generation_total_ms": 0, - "worker_mesh_construction_total_ms": 0, - "worker_far_expanded_mesh_construction_ms": 0, - "worker_compact_encode_ms": 0, - "compact_submissions": 1244, - "pool_gpu_allocated_bytes": 5914624, - "direct_mesh_gpu_bytes": 0, - "compact_pool_allocated_bytes": 5914624, - "pool_cpu_shadow_bytes": 0, - "lod_renderable_regions": 1024, - "gpu_culling_candidate_count": 1024 - }, - "completion": { - "scenario_completed": true, - "sampled_duration_s": 60.01286697387695, - "requested_duration_s": 60, - "sampled_frame_count": 110916, - "evidence_mode": true, - "warmup_ready": true, - "compact_ready": true, - "warmup_timed_out": false, - "lod_renderable_region_target": 1024, - "gpu_candidate_target": 1024 - }, - "compact_mode": "auto" - } - }, - "comparison": { - "cpu_frame_ms": { - "cpu": { - "p95": 0.47203511595726005, - "p99": 12.359561996459977 - }, - "gpu": { - "p95": 0.46153025329113007, - "p99": 12.378830385208136 - }, - "p95_improvement_fraction": 0.02225440928229827, - "p99_regression_fraction": 0.001558986374572022 - }, - "gpu_total_ms": { - "cpu": { - "p99": 8.394708957672137 - }, - "gpu": { - "p99": 8.452523183822647 - }, - "p99_regression_fraction": 0.006886983985034105 - }, - "draw_calls": { - "cpu": 96.47450685465238, - "gpu": 86.52070936564607, - "delta": -9.953797489006305 - }, - "gpu_culling": { - "fixture": "gpu-culling-scale", - "horizon_distance": 4096, - "lod_memory_budget_mb": 2048, - "readiness_target": 1024, - "threshold": 128, - "candidate_count_total": 113577984, - "candidate_count_max": 1024, - "draw_submission_count_total": 2693, - "validation_generation": 3937, - "validation_completed_generation": 3935, - "validation_completed_count": 3935, - "timestamp_ms": { - "p50": 0, - "p95": 0, - "p99": 0.0003600000054575503 - } - } - } -} diff --git a/docs/decisions/917-compact-gpu-lod-tiles.md b/docs/decisions/917-compact-gpu-lod-tiles.md deleted file mode 100644 index b7dcb580..00000000 --- a/docs/decisions/917-compact-gpu-lod-tiles.md +++ /dev/null @@ -1,165 +0,0 @@ -# Decision: compact GPU-rendered far-terrain tiles - -**Issue:** [#917](https://github.com/OpenStaticFish/ZigCraft/issues/917) - -**Status:** Proposed decision with implementation foundation; not a claim that a compact GPU terrain renderer is already shipped. - -## Context and evidence - -Issue #917 is Phase 4 of the LOD GPU roadmap. It follows the completed Phase 0–3 work rather than replacing it: - -- Phase 0 added opt-in LOD CPU/upload/pressure telemetry in - [`modules/world-lod/src/lod_stats.zig`](../../modules/world-lod/src/lod_stats.zig), bounded benchmark scenarios in - [`docs/benchmarks/README.md`](../benchmarks/README.md), and the CPU-only generation probe in - [`src/lod_bench_main.zig`](../../src/lod_bench_main.zig). -- Phase 1 moves mesh generation outside the manager lock, keeps CPU generation and policy authoritative, batches MDI where possible, and retains selectable heightfield, span, and QEM CPU mesh paths in - [`lod_manager_generation_ops.zig`](../../modules/world-lod/src/lod_manager_generation_ops.zig) and - [`lod_mesh.zig`](../../modules/world-lod/src/lod_mesh.zig). -- Phase 2 gives streaming a bounded upload path and deferred retirement. Upload bytes and staging-pressure events are recorded in - [`lod_manager_upload_ops.zig`](../../modules/world-lod/src/lod_manager_upload_ops.zig); eviction defers deletion in - [`lod_manager_eviction_ops.zig`](../../modules/world-lod/src/lod_manager_eviction_ops.zig). The current shared vertex pool remains a CPU-shadowed `Vertex` pool in - [`lod_vertex_pool.zig`](../../modules/world-lod/src/lod_vertex_pool.zig). -- Phase 3 supplies CPU-approved candidates, GPU frustum compaction, and indirect terrain/water streams through - [`lod_renderer.zig`](../../modules/world-lod/src/lod_renderer.zig) and - [`modules/engine-rhi/src/culling.zig`](../../modules/engine-rhi/src/culling.zig). It intentionally falls back to CPU visibility when the workload, pooled-buffer state, or indirect-count support is unsuitable. - -The present far path still worker-builds CPU-expanded `engine-rhi.Vertex` arrays and uploads them. `Vertex` is an `extern struct` carrying position, packed colour/normal/meta, UV, and block light -([`rhi_types.zig`](../../modules/engine-rhi/src/rhi_types.zig)); `LODMesh` holds pending vertices and separate opaque/water ranges. LOD3 and coarser currently select the heightfield path, which emits top quads, stepped side faces, optional water quads, and some vegetation geometry. Rich source columns already contain height, material layers, water, lighting, vegetation, provenance, and optional vertical spans in -[`modules/world-core/src/lod_data.zig`](../../modules/world-core/src/lod_data.zig). - -This is enough evidence to define and prototype a compact representation, but not enough to assert that it wins on every GPU. Existing instrumentation measures worker mesh construction, uploads, CPU visibility, state work, staging pressure, deferred CPU vertex capacity, candidate counts, and CPU/GPU culling diagnostics. It does **not** yet separately account for compact-tile residency, tile-fetch bandwidth, tile-expansion time, or far-terrain GPU timestamps. Those measurements are rollout requirements below. No performance result is implied by this decision. - -## Decision - -Adopt **a reusable indexed grid plus compact, device-local storage-buffer tiles** as the first production GPU path for LOD3 and LOD4. The grid supplies only local sample coordinates and indices; the vertex shader (vertex pulling) obtains the selected tile's compact samples and derives final terrain vertices. The existing Phase 3 CPU-approved visibility and indirect submission remain the first submission path. - -The path is intentionally a heightfield-oriented far representation. It does not promise arbitrary overhangs, per-block side topology, or detailed tree meshes at LOD3/4. Those stay on the existing near/mid CPU mesh paths until a separately validated representation is needed. - -Compute expansion is retained as a measured follow-up option, not the initial default. Mesh shaders are an experimental, supported-device-only spike and are not a production dependency. - -### Why this is the first path - -| Option | Transfer bytes and VRAM | Shader bandwidth / work | Seams and normals | Edits | Support and maintenance | Decision | -| --- | --- | --- | --- | --- | --- | --- | -| **Reusable indexed grid + compact tile SSBO** | Uploads tile samples and metadata instead of repeated expanded vertices; one grid/index buffer is shared. Resident tile payload is proportional to samples, plus descriptors/indirection, rather than per-region vertex capacity. Actual reduction must be measured. | Each grid vertex fetches a small, regular sample neighbourhood. It trades vertex fetch/upload bandwidth for SSBO reads and derived attributes, but has no expansion pass or generated vertex pool. | A one-sample apron makes central-difference normals and edge positions deterministic; precomputed stitch/skirt variants handle topology. | Upload changed tile records or replace a tile version; no readback and no CPU vertex rebuild for the far path. | Uses conventional Vulkan graphics, storage buffers, and indirect drawing already represented by the RHI. One terrain shader variant and one tile allocator are required. | **Selected.** Lowest new synchronization and allocator surface while directly attacking repeated vertex data. | -| **Compute expansion into pooled vertex/index buffers** | Compact upload is still possible, but expanded GPU vertices/indices reintroduce substantial resident memory and pool fragmentation. | Moves expansion and side/skirt generation to compute, adds write bandwidth and compute-to-graphics barriers. It may win only when vertex pulling is demonstrated to be the bottleneck. | Can emit exact stitch/side geometry, but owns a second geometry generator whose boundary rules must stay equivalent to CPU and shader rules. | A tile edit requires re-expanding its allocation and safely retiring/reallocating its old range. | Needs a robust output-pool allocator, overflow handling, barriers, and retirement for generated ranges in addition to the input tile system. | Defer until grid-path GPU timing or feature limits demonstrate a need. | -| **Mesh shaders** | Can avoid persistent expanded geometry and can generate topology per workgroup. | Potentially efficient for culling/amplification and topology generation, but performance and limits are device-specific. | Flexible, but seam and normal code becomes mesh-shader-specific and must still match the fallback. | Tile replacement is straightforward; generated output is transient. | Requires mesh-shader capability detection, new RHI/pipeline work, and a conventional maintained renderer. It excludes too many target devices as a first path. | Research-only after the selected path is stable; never the sole renderer. | - -The comparison explicitly does not assume that shader bandwidth is free or that compute is automatically superior. The grid path is accepted only if its tile reads, vertex work, and visual quality meet the gates below; the CPU mesh path remains the safe alternative. - -## Compact semantic tile ABI - -The compact tile is a **GPU transport/render ABI**, not a replacement for `LODSimplifiedData` or its persisted source semantics on day one. CPU data and persistence remain authoritative. The foundation provides a versioned, checksummed compact wire encoding for testing and future cache integration, but the canonical LOD cache does not consume it yet; that integration requires an explicit cache-version/migration decision. - -The foundation tile is a versioned, fixed-endian 16-byte sample grid with a small serialized header (magic, ABI version, LOD, neighbor-apron mask, width/count, and payload CRC). The production renderer must add a std430-compatible GPU header/instance view that provides: - -- ABI version, tile state/flags, LOD level, region key/origin, cell size, interior grid dimensions, and the tile's generation/edit version; -- a documented height decode and quantization error bound derived from the selected level's needs (the foundation ABI uses signed 1/8-block absolute heights; a future affine/base-relative format requires an ABI version bump); -- offsets/counts into separately aligned arrays for samples and optional far-detail records, not native Zig pointers or enums; -- conservative min/max terrain and water heights, world-space bounds, and an availability/ready flag used by CPU hierarchy policy; and -- explicit feature bits for water, lighting hint, vegetation aggregate, provenance availability, apron validity, and any future extension. Unknown required bits or ABI versions reject the compact path and use CPU mesh fallback. - -The interior sample record must preserve the following semantics even if the final packing evolves: - -| Semantic | Requirement | -| --- | --- | -| Terrain height | Quantized relative to the tile decode range; decoding must reproduce the shared edge sample exactly for adjacent same-level tiles. | -| Surface/material | Stable semantic block IDs plus representative tint information sufficient for far terrain. The foundation ABI stores known seven-bit `BlockType` wire values—not atlas IDs—and reduces unknown future values to stone; changing those wire values requires an ABI version bump. | -| Colour and lighting | Packed representative colour/tint and the far lighting/AO hints required by the selected terrain shading policy. Do not silently reinterpret channel order or colour space. | -| Water | Separate surface-present/coverage classification, quantized surface height or canonical sea-level indication, and depth/colour category sufficient for the far-water pass. Terrain-floor and water-surface semantics remain distinct. | -| Vegetation | The foundation ABI stores aggregate coverage and representative height. A production renderer must either declare a folded representative terrain-colour policy or add versioned type/colour fields; it must not imply a full tree mesh. | -| Provenance | Worldgen, chunk-derived, and edited authority must be retained in CPU source data. A compact provenance class is included when needed for diagnostics and invalidation, not as permission for the GPU to resolve edits. | - -Vertical spans are deliberately outside the first LOD3/4 tile contract. A tile marked as requiring unsupported span/overhang representation is rendered through the CPU fallback (or a later explicitly versioned extension), never flattened without a visual-policy decision. - -### Edge apron and seam contract - -Every resident tile contains a **one-sample apron** around its interior height/material/water samples. Foundation generation starts with a duplicated-edge fallback and records no neighbor-valid bits. As same-level neighbors become available, the producer patches each apron from the neighbor's opposite interior edge and records the corresponding validity bit before enabling seamless compact rendering. - -- Shared same-level edges must decode to identical positions and compatible material/water classification. Adjacent uploads may arrive in any order; a tile with an invalid apron is not eligible for the seamless compact draw path. -- Vertex normals use central differences from the apron in the interior and at boundaries. They are derived in the vertex shader for the initial path; no per-vertex normal storage is required. The normal calculation uses decoded world spacing and the same height quantization policy on both sides of an edge. -- Cross-LOD boundaries use an explicit edge mask selected by CPU hierarchy policy. The initial implementation may use prebuilt indexed-grid edge/stitch variants and conservative skirts; it must not rely on a fragment discard to hide geometry gaps. Geomorphing is optional only after its transition versioning and temporal behaviour are validated. -- Skirt depth, water handoff, and unavailable-neighbour behaviour are part of the tile draw constants, not shader guesses. Existing CPU helpers in - [`lod_seam.zig`](../../modules/world-lod/src/lod_seam.zig) and - [`lod_geometry.zig`](../../modules/world-lod/src/lod_geometry.zig) are the behavioural reference to test against, not a guarantee that their current CPU mutation algorithm can be copied unchanged. - -## CPU and GPU responsibilities - -| CPU-authoritative work | GPU work for the selected path | -| --- | --- | -| Procedural sampling, chunk-derived ingestion, edit merge/provenance precedence, source persistence, invalidation, LOD hierarchy/readiness, parent fallback, coverage policy, and choosing a tile/edge variant. | Vertex-pull compact terrain samples from the reusable grid, decode heights/material attributes, calculate local normals from the apron, rasterize terrain, and submit the already-established indirect draw stream. | -| Build/version compact records; enqueue bounded uploads and tile replacement; retain CPU source until the new GPU version is known safe. | Render far water as a separate compact grid/indirect stream and apply far-only shading. GPU culling may reject CPU-approved candidates, but does not decide hierarchy correctness. | -| Determine whether a source requires the CPU mesh fallback; respond to unsupported hardware and validation failures. | No procedural world generation, persistence, edit conflict resolution, source-of-truth mutation, or GPU-to-CPU readback in normal operation. | - -### Far water - -Far water uses the tile's separate water-surface semantics and a flat/reduced grid. It is a distinct terrain-adjacent pass so opaque terrain does not need water vertices. Its first production shader must use fog, a stable water colour/depth category, inexpensive animated normal/specular treatment, and the shared edge policy. It must avoid unnecessary screen-space reflections, reflection-buffer sampling, and scene-depth/depth-thickness work for the far tier. Near water retains the existing quality path in -[`assets/shaders/vulkan/water.frag`](../../assets/shaders/vulkan/water.frag); this decision does not remove it. - -## Lifetime and synchronization - -Compact input tiles, tile-instance/indirection records, and the reusable grid have separate lifetimes. - -1. Workers produce immutable CPU tile versions from an immutable source snapshot. They do not call RHI or mutate a record visible to the render thread. -2. The render thread allocates a device-local tile range, stages the complete new version, writes it into a frame-safe instance/indirection slot, and records the required transfer-to-shader-read barrier before the draw/compute use. -3. CPU hierarchy readiness publishes the new version only after its upload has been submitted with an associated submission/timeline value. Until then it draws the old version or the parent/CPU fallback; it never points an indirect draw at partially uploaded storage. -4. Replaced or evicted ranges, descriptor/instance entries, and staging allocations are retired only after the last graphics or compute submission that can reference them has completed. Retirements are bounded, accounted for, and reclaimed by completed fence/timeline values—not `vkDeviceWaitIdle` during streaming. -5. Pool/atlas growth uses a new backing allocation and preserves old bindings until their retirement value completes. It must not require CPU shadow copies of expanded vertices. Shutdown remains the only permitted whole-device idle boundary. - -This extends the Phase 2 deferred-deletion intent to compact resources. It must also work with the existing frames-in-flight policy and the Phase 3 compute-to-indirect/storage ordering. A device loss, upload failure, allocator exhaustion, ABI mismatch, or compact-path validation failure leaves the prior safe draw active where possible and otherwise returns the region to the CPU mesh/parent fallback state. - -## Feature gates and fallback - -The feature is opt-in during rollout and is selected per device and per region. The exact public setting name may be chosen with the implementation, but it must expose `off`, `auto`, and `force` semantics and report the selected path/reason in diagnostics. Existing controls such as `ZIGCRAFT_LOD_GPU_CULLING`, `ZIGCRAFT_DISABLE_LOD_MDI`, `ZIGCRAFT_LOD_MESH_PATH_SPANS`, and `ZIGCRAFT_LOD_MESH_PATH_QEM` continue to exercise their current paths; they are not redefined by this decision. - -`auto` enables compact tiles only when the backend can create and bind the required storage buffers, supports the conventional graphics/indirect route used by the selected renderer, has sufficient descriptor/range limits for the configured tile pool, and passes startup ABI/shader self-checks. `force` is a diagnostic mode: it may fail visibly in logs and fall back, but must never disable the fallback or manufacture support. Mesh-shader availability is a separate capability and cannot enable the first production path. - -The maintained fallback is the present CPU-generated `LODMesh` heightfield path, through direct draws or MDI/CPU visibility as supported. It remains covered by unit, integration, and visual tests. LOD0–2 stay on their current quality paths; unsupported compact features (notably span/overhang source data) also use this fallback. The fallback is a release requirement, not a temporary debug option. - -## Phased rollout and validation gates - -### A. Foundation: ABI and accounting - -- Add pure CPU encode/decode tests for every semantic, alignment/offset assertion for the GPU view, version rejection, quantization error bounds, and identical shared-edge decoding. -- Add source-to-tile provenance/edit invalidation tests using `LODSimplifiedData`; do not alter persistence format yet. -- Add compact-specific counters: submitted/uploaded tile bytes, resident/allocated/free/retired tile bytes, tile count by LOD/state, tile replacement count, apron-missing/fallback count, and CPU fallback reason. Keep expanded CPU mesh and compact tile memory distinct. -- Add GPU timestamp scopes for compact terrain vertex/raster work and far water, plus optional vertex-pulling/expansion scope if a compute prototype exists. Report these separately from Phase 3 culling and the existing terrain pass. - -**Gate:** ABI tests pass; accounting includes live plus retired compact allocations; `auto` is the default but must fail closed until the active driver is qualified, with explicit `off` and diagnostic `force` overrides; normal operation makes no GPU readback and no streaming `waitIdle`. - -### B. Reusable-grid terrain prototype - -- Add a shared indexed grid and compact-tile storage/instance binding without changing CPU hierarchy or Phase 3 candidate approval. -- Render a single LOD3/4 tile beside the CPU reference using deterministic source data. Compare decoded heights, material/tint selection, water classification, and normal direction in a debug mode. -- Exercise direct and MDI submission, as well as CPU and GPU-culling selection, without requiring GPU culling to be active. - -**Gate:** GPU and CPU reference images show no unacceptable height displacement, material mismatch, or normal discontinuity; fallback renders if any capability or tile validation check fails; shader validation and RHI contract tests pass. - -### C. Edges, hierarchy, water, and edits - -- Enable adjacent same-level tiles with aprons, then cross-LOD edge variants/skirting and parent-child transition coverage. -- Add the simplified far-water pass and test water-to-terrain as well as water-to-water edges. -- Test rapid edits, chunk-derived ingestion, replacement-before-use, eviction while in flight, teleport, save/reload, and stale job/token rejection. Record edit-to-visible-version latency; do not set a target before baseline data exists. - -**Gate:** the visual matrix in [`docs/benchmarks/README.md`](../benchmarks/README.md) finds no unacceptable cracks, holes, normal seams, transition instability, stale edit, water artifact, or full-detail handoff regression. GPU validation reports no lifetime/barrier errors and retirement stays bounded. - -### D. Measured production decision - -Run comparable fixed-seed, fixed-preset, fixed-render-distance `stationary`, `traversal`, `rapid-turn`, and `teleport-eviction` scenarios, using the documented headless commands and recording build mode and hardware. Compare compact and CPU paths using: - -- per-LOD and total upload bytes, pending upload bytes, worker mesh-construction time, compact encode time, and edit replacement latency; -- compact resident/allocated/retired VRAM, CPU source memory, CPU-expanded vertex memory, grid/index memory, and staging pressure; -- compact terrain/water GPU timestamp cost, frame p50/p95/p99 and worst-frame attribution, draw/indirect counts, candidate/visible/rejected counts, and shader bandwidth proxies available from the profiler; and -- visual captures from the documented seam, transition, water, fog, and full-detail-handoff matrix on supported and fallback devices. - -**Gate:** demonstrate, with recorded artifacts rather than assumed ratios, materially lower LOD3/4 upload bytes and resident geometry memory than the comparable CPU-expanded baseline; eliminate or substantially reduce far-level worker mesh time; preserve visual quality; and keep the fallback functional. The existing benchmark regression/SLO policy remains a guardrail, but it is not evidence by itself that compact tiles are beneficial on player hardware. - -### E. Default enablement and follow-ups - -Default `auto` only after the preceding gate succeeds on the supported-device matrix and long-running robustness runs show bounded resource retirement. Keep `off` available for bisecting. Re-evaluate compute expansion only if measurements identify vertex-pulling bandwidth, topology limits, or raster work as the next constraint. Re-evaluate mesh shaders only as an optional specialization with the conventional path still maintained. - -## Consequences - -This choice reduces the first implementation's scope to a compact heightfield ABI, a reusable grid, shader vertex pulling, and resource lifetime integration. It preserves the existing CPU source, persistence, hierarchy, MDI, and GPU-culling investments, and makes quality knobs such as sample density, material aggregation, water, and vegetation independent of raw expanded-vertex uploads. - -It also introduces a durable ABI, tile allocator, descriptor/instance versioning, new shader variants, and visual boundary obligations. The work must not be described as complete merely because this decision document or its foundation code lands: issue #917's renderer, metrics, visual validation, and maintained fallback gates remain implementation work. diff --git a/docs/lod-quality-controls.md b/docs/lod-quality-controls.md deleted file mode 100644 index 46dfcb4e..00000000 --- a/docs/lod-quality-controls.md +++ /dev/null @@ -1,157 +0,0 @@ -# LOD Quality Controls - -The render distance preset seeds distant LOD quality. The World settings also -expose `Render Distance` for full-detail chunks and `Distant LOD Limit` as the -outer terrain radius. Lowering the latter reduces generation pressure; -coarse regions fill concentrically so the engine does not render isolated -outer-horizon islands before nearby fallback terrain. - -The production `Distant LOD Limit` currently supports 256 or 512 chunks. -Larger radii remain benchmark/diagnostic-only: the five-level hierarchy has no -coarser level beyond LOD4, so a contiguous 1,024-chunk disk exceeds the normal -logical-memory and compact-pool qualification budgets. - -Presets intentionally expose a small set of stable knobs: - -- `lod_radii`: chunk radii for LOD0 through LOD4. -- `horizon_radius`: the supported far-terrain horizon in chunks. -- `lod_store_size_cap_mb`: an aggregate cap across all persistent `.zlod` - containers. The cache worker evicts the oldest containers after atomic - writes and compacts live entries when sector growth reaches the cap. -- `horizontal_detail`: target horizontal detail per LOD. This is used as a floor for QEM triangle targets when the experimental QEM mesh path is enabled. -- `sample_density`: source-grid density per LOD. Every 512-chunk production - horizon uses half density for LOD4, giving the fallback 33x33 source grids - instead of 65x65 grids; finer LODs replace those 16-block cells as they - stream in. The 256-chunk Low horizon retains its denser LOD4 source grid. -- `vertical_span_budget`: enables rich column/span source data when nonzero. - The numeric values are reserved preset policy; current source allocation is - bounded by the engine-wide `MAX_LOD_VERTICAL_SPANS` limit. -- `mesh_path`: selects the rich `column_spans` path for near and mid-distance - LODs. LOD3/LOD4 deliberately fall back to heightfields to bound far-horizon - geometry and memory; `qem` remains available for controlled testing. -- `fog_start_percent`: records the intended per-level fade-band policy. Terrain - and water currently use the shared atmospheric distance-fog curve so loaded - LODs do not turn into an opaque horizon-colored shelf near the player. -- `memory_budget_mb` and `max_uploads_per_frame`: bound cache pressure and per-frame GPU upload work. - -## Supported presets - -| Preset | Horizon | Horizontal detail LOD0–4 | Span setting | RAM budget | Store cap | Uploads/frame | -| --- | ---: | --- | ---: | ---: | ---: | ---: | -| Low | 256 chunks | 33/33/33/65/65 | 2 | 128 MB | 512 MB | 4 | -| Medium | 512 chunks | 33/49/49/65/33 | 2 | 256 MB | 1,024 MB | 8 | -| High | 512 chunks | 33/65/65/97/97 | 3 | 384 MB | 1,536 MB | 8 | -| Ultra | 512 chunks | 33/65/65/129/129 | 4 | 512 MB | 3,072 MB | 12 | -| Extreme | 512 chunks | 33/65/65/129/129 | 4 | 1,024 MB | 4,096 MB | 16 | - -These values are policy inputs, not a promise that all hardware sustains the -full horizon. The memory governor shrinks refinement radii under pressure but -preserves the coarsest parent horizon so missing children do not produce holes. -The benchmark SLOs and regression thresholds are maintained in -[`benchmarks/README.md`](benchmarks/README.md). - -## Requirements and fallback behavior - -- Vulkan compute and indirect drawing are preferred for GPU LOD culling. When - unavailable or disabled, CPU visibility and indirect submission remain the - correctness fallback. -- Compact far-tile encoding rejects source columns it cannot represent (for - example, an unsupported span layout). Production rendering remains on the - CPU mesh path until compact-tile residency and shaders are enabled, so - unsupported data is never silently dropped. -- Parent regions remain visible until all four finer children are renderable - and the transition window completes. Streaming delay therefore degrades to - coarser terrain rather than a hierarchy hole. -- Expanded and compact LOD terrain and water use the same atmospheric fog - progression as their full-detail counterparts. LOD representation changes - must not introduce an additional fixed-distance fog floor. -- Pause and large traversal changes invalidate queued and in-flight worker - tokens. Per-region cancellation prevents a stale generation result from - publishing after unpause or teleport. -- Cache payloads are checksummed and keyed by seed, generator identity/version, - coordinates, and schema. Corrupt or provenance-incomplete payloads are - deleted and regenerated. Source authority (`worldgen`, `chunk_derived`, or - `edited`) is serialized even for span-less far levels. -- Persistent storage, pending cache I/O, generation admission, CPU source/mesh - memory, upload count, and upload bytes are bounded. A container that cannot - fit within the aggregate store cap is rejected rather than allowing - persistent storage to grow without limit. - -## Diagnostics - -Set `ZIGCRAFT_LOD_DIAG=1` to log LOD queue, render, and aggregate stats diagnostics. The output includes generation and upload queue depth, cache hits and misses, cache hit rate, mesh counts, vertex counts, visible region filtering, and fallback/culling reasons. - -Runtime mesh path overrides are available while alternate mesh paths stabilize: - -- `ZIGCRAFT_LOD_COMPACT=off` explicitly keeps expanded CPU LOD meshes. -- `ZIGCRAFT_LOD_COMPACT=auto` is the production capability-selected path only - when immutable terrain/water descriptor sets are available. Unsupported - mixed shoreline topology remains on the expanded mesh fallback; capability - allocation alone is not a release criterion. `off` remains the explicit - supported expanded fallback. -- `ZIGCRAFT_LOD_COMPACT=force` requests the same compact path for validation; - unrepresentable terrain, partial-water tiles, allocation pressure, and runtime - draw failures still fall back to a dedicated CPU-built GPU mesh rather than dropping a region. -- Normal expanded LOD drawing remains GPU-backed by default. The separate - `ZIGCRAFT_LOD_GPU_CULLING=1` indirect-compaction optimization is opt-in and - applies to the qualified compact-auto reload path through immutable per-layer - terrain/water descriptor sets. Unsupported shoreline topology remains on the - expanded fallback rather than sharing mutable descriptor state. -- Wet LOD3/4 regions with unsupported shoreline topology remain on the - expanded fallback mesh. The saved-world RADV qualification requires this - fallback alongside dry compact residency; compact allocation alone is not - evidence that wet compact terrain was rendered. -- `ZIGCRAFT_LOD_MESH_PATH_QEM=1` forces the QEM decimation path. -- `ZIGCRAFT_LOD_MESH_PATH_SPANS=1` forces the column/span mesh path. - -## Phase 5 visual regression gate - -`zig build phase5-visual-gate` is a bounded, headless capture gate, not a -pixel-golden test. It launches the production flat generator in expanded -(`ZIGCRAFT_LOD_COMPACT=off`) and production compact (`auto`) modes, then applies -small deterministic runtime fixtures for three separate scenes: - -- `seam` places contrasting geometry on both sides of the x=16 chunk boundary. -- `water` creates a resident full-detail water pool. The app records resident - fixture-cell observations and the checker validates the water capture ROI. -- `lod-handoff` places a fixed camera in full detail looking into the LOD - horizon, requiring both rendered full-detail chunks and compact allocation; - its `auto` capture also enables MDI/GPU culling and requires a zero-mismatch - candidate submission record. -- `lod-handoff-traversal` moves the production player 256 blocks across LOD - rings; `fog-rapid-turn` makes two deterministic horizon rotations; and - `teleport-handoff` jumps to a fixed distant pose before its full-detail/LOD - handoff settles. These compact-auto scenes emit exact run/scene motion and - readiness markers and require compact residency/submissions, zero culling - mismatches/overflows, and completed delayed GPU validation. -- `saved-world-create` writes deterministic wet and dry edits, flushes the - world save, and persists its LOD source cache in a unique per-run - `ZIGCRAFT_SAVE_DIR`. `saved-world-reload` is a second process that reloads - that exact save using `auto`, MDI/GPU culling validation, and a screenshot. - It requires explicit save-loaded, wet/dry fallback, compact-residency, - validation-complete, zero-mismatch, and zero-overflow evidence. - -The seam/water visual pairs keep LOD MDI disabled so their compact-direct image -comparison isolates the production fallback/compact paths. The GPU-culling MDI -handoff capture is kept separate because it exercises a distinct submission -stream and has its own activation/validation evidence. - -Each capture is frame- and wall-time-bounded (`PHASE5_VISUAL_CAPTURE_TIMEOUT`, -default `110s`). Captures default to frame 900 and require 180 consecutive -fixture/streaming/LOD-queue/transition-stable frames first -(`PHASE5_VISUAL_SETTLE_FRAMES`). The checker writes `zig-out/phase5-visual-smoke/manifest.json` -even when it fails. It requires compact allocation/capture counters, water -fixture evidence, whole-image and terrain ROI health, and tighter whole-image -and ROI expanded-versus-compact differences. The saved-world reload also -requires a healthy rendered image, rather than accepting save/telemetry logs -alone. Motion capture readiness additionally waits for the bounded pose script -to finish. CI allows 35 minutes for the now ten process-isolated captures and -uploads the isolated save, PNGs, logs, and manifest on both success and failure. - -The water fixture validates the production full-detail water path. The -saved-world reload is the RADV qualification for production `auto`: dry tiles -use compact residency through immutable descriptors while unsupported wet -shorelines retain the expanded fallback. `ZIGCRAFT_LOD_COMPACT=off` remains -available throughout. - -Use the stable heightfield defaults for normal gameplay. Use the override flags only when comparing visual quality or diagnosing LOD mesh regressions. diff --git a/docs/lod-water-and-latency-steering-752.md b/docs/lod-water-and-latency-steering-752.md deleted file mode 100644 index 4f3652fa..00000000 --- a/docs/lod-water-and-latency-steering-752.md +++ /dev/null @@ -1,123 +0,0 @@ -# Steering doc: water walls + slow LOD kick-in (issue #752, iteration 5) - -Audience: the agent on `feature/distant-horizons-lod-overhaul-752`. -All work continues in the **single existing PR/branch** targeting `dev`. - -## Where we are - -The detail-band restructure landed (wider fine rings, canopy at more levels, -new `radiiForDistances`). Two severe problems remain, both confirmed in the -latest screenshots and traced to code: - -1. **Oceans render as a grid of giant blue walls/slabs.** Vertical - water-colored walls appear at every LOD region border, and neighboring - regions' water planes sit at different heights, producing a tiled grid - across the whole sea. -2. **LOD takes a very long time to appear.** After world load there is a long - window with bare sky beyond the chunk ring before any LOD pops in. - ---- - -## W — Fix the ocean grid (root causes found, in `lod_mesh.zig`) - -### W1: region-border walls come from null-neighbor = "fully exposed" - -`addExposedSpanFace` (~line 1366): when the neighbor cell is outside this -region (`neighbor_gx`/`neighbor_gz` null at `gx == 0`, `gx+1 >= width-1`, -etc.), **no interval is subtracted and the whole span face is emitted**. -Combined with `collectColumnSpans` (~line 1256) synthesizing a seafloor span -from **`min_height = 0.0`** up to terrain height for every water column — -colored with `data.colors[idx]`, which is water-blue over oceans — every -region border under the sea gets a bedrock-to-seafloor blue wall. That is -the grid in the screenshot. - -Fix, in order of preference: - -1. **Sample the true neighbor.** The neighboring region's edge data is the - same worldgen/chunk source — expose a `stitchedNeighborColumn(data, dir)` - lookup (the seam-stitching machinery and `lod_seam.zig` already model - this) or have the generator fill a 1-cell border apron - (`width + 2` samples) so border cells always have a real neighbor. Apron - is the standard solution and also fixes top-face seams. -2. Until the apron exists, **clamp the border assumption**: at a null - neighbor, assume the neighbor column is identical to this one (subtract - the span's own interval → emit nothing) instead of assuming air. Distant - Horizons does exactly this: never draw side faces at section borders - against unknown neighbors. -3. The synthesized seafloor span should start at the local minimum - surrounding terrain height minus a small skirt (e.g. 8 blocks), **not - y=0**, and its color must be the **subsurface material color** - (sand/gravel via `terrainBlockForLODQuad`), never `data.colors[idx]` - (water-tinted over oceans). - -### W2: water plane height must be uniform per water body - -Adjacent regions quantize their water tops independently -(`quantizedWaterSurfaceHeightForSpan` takes a per-cell max of corner -samples), so region A's sea sits at 62 and region B's at 63 → visible tile -edges. Sea-level water should use the **worldgen sea level constant** -directly (one flat plane world-wide); only genuinely elevated water bodies -(lakes) use sampled heights, quantized from the **same source sample**, not -per-cell corner maxima. Add a regression test: two adjacent ocean regions -must emit water tops at identical heights. - -### W3: acceptance for water - -- Status: visual acceptance still needs manual verification on a live scene. -- Expected open-ocean result: one continuous flat translucent plane to the - horizon, zero grid lines, zero vertical blue walls. -- Expected coastline result: beach steps down under the water plane, seafloor - visible through shallow water. - -## L — Fix time-to-first-LOD - -The world stays empty beyond the chunk ring for a long time after load. The -recent config changes multiplied generation cost: `mesh_path = .column_spans` -with `vertical_span_budget = 4`, `horizontal_detail = 33` for **all** levels -(LOD4 was 24), and much wider fine rings. Steer toward "horizon appears -instantly, detail streams in": - -1. **Coarse-first must be visible-first.** On world load, schedule the - complete LOD4 shell (cheapest, covers everything) before *any* finer - ring, and make sure the scheduler doesn't starve it behind the huge LOD0 - queue (check the priority bias and the new horizon-protected scheduling - in `lod_manager.zig`/`lod_scheduler.zig`). Success metric: some LOD - terrain visible at the horizon within ~2 seconds of world load, refining - inward afterwards. -2. **Cheapen the coarse levels again**: LOD3/LOD4 do not need - `horizontal_detail = 33` and 4 vertical spans; heightfield or span-budget - 1–2 with detail 16–24 is indistinguishable at those distances and cuts - the initial fill time massively. Only LOD0/LOD1 need the full span - treatment. -3. **Verify the persistent store/cache actually hits** on reload - (`lod_store.zig`, `lod_cache.zig`): second load of the same world should - show LOD almost immediately. Log hit/miss counters in `LODStats` and - check them in a headless run. -4. Measure with `-Dstartup-diagnostic-seconds=10`: log LOD counts per level - at 1s intervals; attach the numbers to the PR. - -Status: automated build/test/shader validation is covered by PR verification; -manual startup timing numbers are still pending because bounded headless runs -timed out in the local environment before useful LOD diagnostics were emitted. - -## Carry-over polish (unchanged, lower priority) - -- LOD ground color still brighter than chunk grass — calibrate at the seam. -- Far bands should reach full fog at the horizon; sun-directional side-face - shading. - -## Verification - -- `devenv shell zig build test` after every change (add the W2 - regression test). -- `headless-screenshot` (`-Dskip-present -Dauto-world=normal`): open-ocean - view, coastline view, and a shot taken ~3 seconds after load to prove the - fast coarse shell. -- `headless-benchmark` on `low`: initial-fill time and frame time must not - regress; coarse-level cheapening should improve both. -- `headless-crash-test` after scheduler changes. - -Single PR, conventional commits (suggested: `fix: neighbor-aware span faces -at region borders`, `fix: uniform sea-level water plane`, `perf: coarse -LOD fill fast path`). Before/after screenshots of the ocean grid and a -load-time comparison in the PR description. diff --git a/docs/shaders/spirv-sizes.json b/docs/shaders/spirv-sizes.json index f2287b9c..b4918411 100644 --- a/docs/shaders/spirv-sizes.json +++ b/docs/shaders/spirv-sizes.json @@ -12,11 +12,6 @@ "assets/shaders/vulkan/fxaa.frag": 5916, "assets/shaders/vulkan/fxaa.vert": 1160, "assets/shaders/vulkan/g_pass.frag": 6912, - "assets/shaders/vulkan/lod_compact_terrain.frag": 5660, - "assets/shaders/vulkan/lod_compact_terrain.vert": 20656, - "assets/shaders/vulkan/lod_compact_water.frag": 5912, - "assets/shaders/vulkan/lod_compact_water.vert": 13332, - "assets/shaders/vulkan/lod_culling.comp": 14044, "assets/shaders/vulkan/lpv_inject.comp": 4844, "assets/shaders/vulkan/lpv_propagate.comp": 8316, "assets/shaders/vulkan/mesh.comp": 45244, diff --git a/docs/worldgen-biomes-and-terrain.md b/docs/worldgen-biomes-and-terrain.md index ec6acd7a..0ddb9d3c 100644 --- a/docs/worldgen-biomes-and-terrain.md +++ b/docs/worldgen-biomes-and-terrain.md @@ -79,7 +79,6 @@ There are also compatibility and utility selectors in `biome_selector.zig`: - `selectBiomeVoronoiMultiParam` compares heat, humidity, elevation, continentalness, ruggedness, and ridge mask against `BIOME_POINTS` after height, slope, and continentalness filters. - Blended selectors return primary/secondary biome IDs and a blend factor for transition-aware callers. -- `selectBiomeSimple` is reserved for simplified LOD-style selection. ## Region Roles diff --git a/modules/engine-core/src/job_system.zig b/modules/engine-core/src/job_system.zig index 0fd44994..5cf1611d 100644 --- a/modules/engine-core/src/job_system.zig +++ b/modules/engine-core/src/job_system.zig @@ -86,16 +86,9 @@ pub const Job = struct { x: i32, z: i32, job_token: u32, - lod_level: u3 = 0, - /// Converts x/z into chunk coordinates for distance reprioritization. - /// Normal chunk jobs use chunk coords directly (1); LOD jobs store - /// region coords and set this to that LOD's chunks-per-side. - coord_scale: i32 = 1, /// Keep an explicitly assigned bootstrap order instead of replacing /// its low priority bits when the player position changes. preserve_priority: bool = false, - /// Immutable LOD config snapshot captured when a generation job is queued. - lod_radius: i32 = 0, use_vertical_spans: bool = false, }; @@ -243,20 +236,13 @@ pub const JobQueue = struct { // Only update distance for chunk-based jobs if (job.getChunkCoords()) |coords| if (!job.data.chunk.preserve_priority) { - const scale: i32 = @max(updated_job.data.chunk.coord_scale, 1); - // Compute squared distance in i64 then clamp, matching - // lod_manager/lod_scheduler — i32 dx*dx can overflow at large - // render/LOD distances. - const dx: i64 = @as(i64, coords.x) * @as(i64, scale) - @as(i64, self.player_cx); - const dz: i64 = @as(i64, coords.z) * @as(i64, scale) - @as(i64, self.player_cz); + // Compute squared distance in i64 before clamping so distant + // chunk coordinates cannot overflow the queue priority. + const dx: i64 = @as(i64, coords.x) - @as(i64, self.player_cx); + const dz: i64 = @as(i64, coords.z) - @as(i64, self.player_cz); const new_dist_i64: i64 = dx * dx + dz * dz; const new_dist: i32 = @intCast(@min(new_dist_i64, @as(i64, 0x0FFFFFFF))); - // Preserve the LOD-bias high bits; only refresh the low 28 - // distance bits. Overwriting dist_sq wholesale would flatten - // all LOD levels into one distance space and break cross-level - // ordering. - const bias_bits = updated_job.dist_sq & ~@as(i32, 0x0FFFFFFF); - updated_job.dist_sq = bias_bits | new_dist; + updated_job.dist_sq = new_dist; }; temp.append(self.allocator, updated_job) catch { @@ -524,22 +510,22 @@ test "Job.cleanup idempotency survives queue drain after OOM-style drop" { try testing.expectEqual(@as(usize, 1), cleanup_count); } -test "JobQueue reprioritizes region-scaled chunk jobs" { +test "JobQueue reprioritizes chunk jobs" { var queue = JobQueue.init(testing.allocator); defer queue.deinit(); try queue.push(.{ .type = .chunk_generation, .dist_sq = 0, - .data = .{ .chunk = .{ .x = 60, .z = 0, .job_token = 1, .lod_level = 0, .coord_scale = 2 } }, + .data = .{ .chunk = .{ .x = 60, .z = 0, .job_token = 1 } }, }); try queue.push(.{ .type = .chunk_generation, .dist_sq = 0, - .data = .{ .chunk = .{ .x = 50, .z = 0, .job_token = 2, .lod_level = 0, .coord_scale = 2 } }, + .data = .{ .chunk = .{ .x = 50, .z = 0, .job_token = 2 } }, }); - try queue.updatePlayerPos(100, 0); + try queue.updatePlayerPos(52, 0); const first = queue.pop() orelse return error.TestExpectedEqual; try testing.expectEqual(@as(u32, 2), first.data.chunk.job_token); diff --git a/modules/engine-core/src/lod_types.zig b/modules/engine-core/src/lod_types.zig deleted file mode 100644 index f8c95718..00000000 --- a/modules/engine-core/src/lod_types.zig +++ /dev/null @@ -1,32 +0,0 @@ -//! Shared LOD identifiers used by renderer and world systems. - -pub const LODLevel = enum(u3) { - lod0 = 0, - lod1 = 1, - lod2 = 2, - lod3 = 3, - lod4 = 4, - - pub const count = 5; - - pub fn scale(self: LODLevel) u32 { - return @as(u32, 1) << @intFromEnum(self); - } - - pub fn chunksPerSide(self: LODLevel) u32 { - return self.scale() * 2; - } - - pub fn totalChunks(self: LODLevel) u32 { - const side = self.chunksPerSide(); - return side * side; - } - - pub fn blockSize(self: LODLevel) u32 { - return self.scale(); - } - - pub fn regionSizeBlocks(self: LODLevel, chunk_size_x: u32) u32 { - return chunk_size_x * self.chunksPerSide(); - } -}; diff --git a/modules/engine-core/src/root.zig b/modules/engine-core/src/root.zig index fa130bbd..cf60c5f2 100644 --- a/modules/engine-core/src/root.zig +++ b/modules/engine-core/src/root.zig @@ -3,7 +3,6 @@ pub const crash_handler = @import("crash_handler.zig"); pub const interfaces = @import("interfaces.zig"); pub const job_system = @import("job_system.zig"); pub const log = @import("log.zig"); -pub const lod_types = @import("lod_types.zig"); pub const ring_buffer = @import("ring_buffer.zig"); pub const runtime_env = @import("runtime_env.zig"); pub const sync = @import("sync"); @@ -12,7 +11,6 @@ pub const window = @import("window.zig"); pub const LogLevel = log.LogLevel; pub const Logger = log.Logger; -pub const LODLevel = lod_types.LODLevel; pub const IScreenManager = interfaces.IScreenManager; pub const ScreenHandle = interfaces.ScreenHandle; pub const Job = job_system.Job; diff --git a/modules/engine-ecs/src/systems/render.zig b/modules/engine-ecs/src/systems/render.zig index 6ae51242..b67b5acf 100644 --- a/modules/engine-ecs/src/systems/render.zig +++ b/modules/engine-ecs/src/systems/render.zig @@ -136,7 +136,7 @@ pub const RenderSystem = struct { const model = boxModel(registry, entity_id, transform, camera_pos); if (self.buffer_handle != rhi_pkg.InvalidBufferHandle) { - ctx.setModelMatrix(model, mesh.color, 0); + ctx.setModelMatrix(model, mesh.color); ctx.draw(self.buffer_handle, wireframe_line_vertex_count, .lines); } } @@ -160,7 +160,7 @@ pub const RenderSystem = struct { const max = min.add(size); if (max.x < caster_min.x or min.x > caster_max.x or max.y < caster_min.y or min.y > caster_max.y or max.z < caster_min.z or min.z > caster_max.z) continue; - ctx.setModelMatrix(boxModel(registry, row.entity, transform, camera_pos), Vec3.one, 0); + ctx.setModelMatrix(boxModel(registry, row.entity, transform, camera_pos), Vec3.one); ctx.drawOffset(self.solid_buffer_handle, solid_box_vertex_count, .triangles, 0); } } diff --git a/modules/engine-graphics/src/cloud_system.zig b/modules/engine-graphics/src/cloud_system.zig index 3f96e3c2..d2e645bd 100644 --- a/modules/engine-graphics/src/cloud_system.zig +++ b/modules/engine-graphics/src/cloud_system.zig @@ -78,7 +78,7 @@ pub const CloudSystem = struct { -(camera_pos.y - self.mesh_camera_pos.y), (self.origin_z - self.mesh_origin_z) - (camera_pos.z - self.mesh_camera_pos.z), ); - ctx.setModelMatrix(Mat4.translate(smooth_offset), Vec3.one, 0); + ctx.setModelMatrix(Mat4.translate(smooth_offset), Vec3.one); ctx.drawOffset(self.vertex_buffer, self.vertex_count, .triangles, 0); // Force OpaquePass to rebind terrain state after clouds use the same pipeline. ctx.setTerrainPipelineBound(false); @@ -96,7 +96,7 @@ pub const CloudSystem = struct { -(camera_pos.y - self.mesh_camera_pos.y), (self.origin_z - self.mesh_origin_z) - (camera_pos.z - self.mesh_camera_pos.z), ); - ctx.setModelMatrix(Mat4.translate(smooth_offset), Vec3.one, 0); + ctx.setModelMatrix(Mat4.translate(smooth_offset), Vec3.one); ctx.drawOffset(self.vertex_buffer, self.vertex_count, .triangles, 0); } @@ -177,12 +177,12 @@ pub const CloudSystem = struct { } fn emitQuad(self: *CloudSystem, vertices: *std.ArrayListUnmanaged(rhi.Vertex), a: [3]f32, b: [3]f32, c: [3]f32, d: [3]f32, color: [3]f32, normal: [3]f32) !void { - try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); - try vertices.append(self.allocator, rhi.Vertex.initCloud(b, color, normal, .{ 1.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); - try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); - try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); - try vertices.append(self.allocator, rhi.Vertex.initCloud(d, color, normal, .{ 0.0, 0.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); - try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.LOD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(b, color, normal, .{ 1.0, 1.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(c, color, normal, .{ 1.0, 0.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(d, color, normal, .{ 0.0, 0.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); + try vertices.append(self.allocator, rhi.Vertex.initCloud(a, color, normal, .{ 0.0, 1.0 }, rhi.Vertex.CLOUD_TILE_ID, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0)); } fn uploadMesh(self: *CloudSystem) !void { diff --git a/modules/engine-graphics/src/render_graph.zig b/modules/engine-graphics/src/render_graph.zig index 2d5be621..8759272e 100644 --- a/modules/engine-graphics/src/render_graph.zig +++ b/modules/engine-graphics/src/render_graph.zig @@ -346,8 +346,7 @@ pub const GPass = struct { const atlas = self.material_system.getAtlasHandles(ctx.env_map_handle); ctx.render_ctx.bindTexture(atlas.diffuse, 1); const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); - const render_lod = engine_core.envFlag("ZIGCRAFT_LOD_GPASS", false); - ctx.world.renderOpaque(view_proj, ctx.camera.position, render_lod); + ctx.world.renderOpaque(view_proj, ctx.camera.position); ctx.render_ctx.endGPass(); } }; @@ -474,20 +473,7 @@ pub const OpaquePass = struct { ctx.render_ctx.bindTexture(ctx.lpv_textures.green, 12); ctx.render_ctx.bindTexture(ctx.lpv_textures.blue, 13); const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); - ctx.world.renderOpaque(view_proj, ctx.camera.position, true); - } -}; - -/// Must execute while no graphics pass is active so Vulkan can place the -/// compute-to-indirect/storage barriers in the same frame command buffer. -pub const LODCullingPass = struct { - const VTABLE = IRenderPass.VTable{ .name = "LODCullingPass", .needs_main_pass = false, .execute = execute }; - pub fn pass(self: *LODCullingPass) IRenderPass { - return .{ .ptr = self, .vtable = &VTABLE }; - } - fn execute(_: *anyopaque, ctx: SceneContext) anyerror!void { - const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); - ctx.world.prepareLODCulling(view_proj, ctx.camera.position); + ctx.world.renderOpaque(view_proj, ctx.camera.position); } }; @@ -636,7 +622,7 @@ pub const WaterReflectionPass = struct { ctx.render_ctx.bindTexture(ctx.lpv_textures.green, 12); ctx.render_ctx.bindTexture(ctx.lpv_textures.blue, 13); - ctx.world.renderOpaque(reflected_vp, ctx.camera.position, false); + ctx.world.renderOpaque(reflected_vp, ctx.camera.position); } }; @@ -668,8 +654,7 @@ pub const WaterPass = struct { ctx.render_ctx.bindTexture(scene_depth_handle, 15); const view_proj = ctx.camera.getJitteredProjectionMatrixReverseZ(ctx.aspect, ctx.viewport_width, ctx.viewport_height, ctx.taa_enabled).multiply(ctx.camera.getViewMatrixOriginCentered()); - const render_lod_water = engine_core.envFlag("ZIGCRAFT_LOD_WATER", true); - ctx.world.renderFluid(view_proj, ctx.camera.position, render_lod_water); + ctx.world.renderFluid(view_proj, ctx.camera.position); } }; diff --git a/modules/engine-graphics/src/render_system.zig b/modules/engine-graphics/src/render_system.zig index f401da54..37372887 100644 --- a/modules/engine-graphics/src/render_system.zig +++ b/modules/engine-graphics/src/render_system.zig @@ -66,7 +66,6 @@ pub const RenderSystem = struct { sky_pass: render_graph_pkg.SkyPass, cloud_pass: render_graph_pkg.CloudPass, opaque_pass: render_graph_pkg.OpaquePass, - lod_culling_pass: render_graph_pkg.LODCullingPass, entity_pass: render_graph_pkg.EntityPass, taa_pass: render_graph_pkg.TAAPass, bloom_pass: render_graph_pkg.BloomPass, @@ -221,7 +220,6 @@ pub const RenderSystem = struct { .sky_pass = .{}, .cloud_pass = .{}, .opaque_pass = undefined, - .lod_culling_pass = .{}, .entity_pass = .{}, .taa_pass = .{ .enabled = !disable_taa and config.taa_enabled }, .bloom_pass = .{ .enabled = !disable_bloom and config.bloom_enabled }, @@ -279,7 +277,6 @@ pub const RenderSystem = struct { if (self.water_reflection_pass.enabled) { try self.render_graph.addPass(self.water_reflection_pass.pass()); } - try self.render_graph.addPass(self.lod_culling_pass.pass()); try self.render_graph.addPass(self.sky_pass.pass()); try self.render_graph.addPass(self.cloud_pass.pass()); try self.render_graph.addPass(self.opaque_pass.pass()); diff --git a/modules/engine-graphics/src/rhi_tests.zig b/modules/engine-graphics/src/rhi_tests.zig index d37dfc13..7590903e 100644 --- a/modules/engine-graphics/src/rhi_tests.zig +++ b/modules/engine-graphics/src/rhi_tests.zig @@ -17,6 +17,7 @@ const MockContext = struct { dynamic_resolution_max_scale: f32 = 0.0, dynamic_resolution_target_fps: u32 = 0, resolution_scale: f32 = 1.0, + clear_color: Vec3 = Vec3.zero, fn bindTexture(ptr: *anyopaque, handle: rhi.TextureHandle, slot: u32) void { const self: *MockContext = @ptrCast(@alignCast(ptr)); @@ -155,6 +156,11 @@ const MockContext = struct { return .{ .ptr = ptr, .vtable = &MOCK_STATE_VTABLE }; } + fn setClearColor(ptr: *anyopaque, color: Vec3) void { + const self: *MockContext = @ptrCast(@alignCast(ptr)); + self.clear_color = color; + } + fn isTimingEnabled(ptr: *anyopaque) bool { _ = ptr; return false; @@ -196,10 +202,6 @@ const MockContext = struct { _ = max_chunks; return null; } - fn createLODCullingSystem(_: *anyopaque, _: std.mem.Allocator, _: usize) anyerror!?rhi.ILODCullingSystem { - return null; - } - fn getRenderResolution(ptr: *anyopaque) rhi.RenderResolution { _ = ptr; return .{ .width = 1920, .height = 1080 }; @@ -335,7 +337,7 @@ const MockContext = struct { .requestSwapchainRecreate = undefined, .getEncoder = MockContext.getEncoder, .getStateContext = MockContext.getStateContext, - .setClearColor = undefined, + .setClearColor = setClearColor, }; const MOCK_PASSES_VTABLE = rhi.IPassOrchestrationContext.VTable{ @@ -478,7 +480,6 @@ const MockContext = struct { .getFrameIndex = undefined, .supportsIndirectFirstInstance = undefined, .supportsIndirectCount = undefined, - .supportsCompactLODGpuCulling = undefined, .getMaxAnisotropy = undefined, .getMaxMSAASamples = undefined, .getFaultCount = undefined, @@ -489,6 +490,24 @@ const MockContext = struct { .waitIdle = undefined, }; + const MOCK_COMPUTE_VTABLE = rhi.IComputeContext.VTable{ + .bindComputePipeline = bindComputePipeline, + .bindDescriptorSet = bindDescriptorSet, + .createComputeBuffer = createComputeBuffer, + .destroyComputeBuffer = destroyComputeBuffer, + .createComputePipeline = createComputePipeline, + .updateComputeDescriptors = updateComputeDescriptors, + .destroyComputePipeline = destroyComputePipeline, + .dispatch = dispatchCompute, + .pushConstants = pushComputeConstants, + .fillBuffer = fillComputeBuffer, + .copyBuffer = copyComputeBuffer, + .pipelineBarrier = computePipelineBarrier, + .bufferBarrier = computeBufferBarrier, + .waitForFrameFence = waitForFrameFence, + .hasCommandBuffer = hasCommandBuffer, + }; + const MOCK_SHADOW_VTABLE = rhi.IShadowContext.VTable{ .beginPass = undefined, .endPass = undefined, @@ -508,79 +527,72 @@ const MockContext = struct { .setScissorRegion = undefined, }; + const MOCK_TIMING_VTABLE = rhi.IDeviceTiming.VTable{ + .beginPassTiming = beginPassTiming, + .endPassTiming = endPassTiming, + .getTimingResults = getTimingResults, + .isTimingEnabled = isTimingEnabled, + .setTimingEnabled = setTimingEnabled, + }; + + const MOCK_QUALITY_VTABLE = rhi.IRenderQualityOptions.VTable{ + .setWireframe = undefined, + .setTexturesEnabled = undefined, + .setDebugShadowView = undefined, + .setShadowDebugChannel = undefined, + .setVSync = undefined, + .setAnisotropicFiltering = undefined, + .setVolumetricDensity = undefined, + .setShadowResolution = undefined, + .setMSAA = undefined, + .setFXAA = undefined, + .setBloom = undefined, + .setBloomIntensity = undefined, + .setVignetteEnabled = undefined, + .setVignetteIntensity = undefined, + .setFilmGrainEnabled = undefined, + .setFilmGrainIntensity = undefined, + .setColorGradingEnabled = undefined, + .setColorGradingIntensity = undefined, + .setTAABlendFactor = undefined, + .setTAAVelocityRejection = undefined, + .setDynamicResolution = setDynamicResolution, + .getResolutionScale = getResolutionScale, + }; + + const MOCK_RECOVERY_VTABLE = rhi.IDeviceRecovery.VTable{ + .recover = undefined, + }; + + const MOCK_CULLING_FACTORY_VTABLE = rhi.ICullingSystemFactory.VTable{ + .createCullingSystem = createCullingSystem, + }; + + const MOCK_SCREENSHOT_VTABLE = rhi.IScreenshotContext.VTable{ + .captureFrame = undefined, + }; + const MOCK_VULKAN_RHI_VTABLE = rhi.RHI.VTable{ .init = undefined, .deinit = undefined, - .resources = MOCK_RESOURCES_VTABLE, - .render = MOCK_RENDER_VTABLE, - .passes = MOCK_PASSES_VTABLE, - .post_process = MOCK_POST_PROCESS_VTABLE, - .effects = MOCK_EFFECTS_VTABLE, - .vulkan = MOCK_NATIVE_VTABLE, - .ssao = MOCK_SSAO_VTABLE, - .debug_overlay = MOCK_DEBUG_OVERLAY_VTABLE, - .shadow = MOCK_SHADOW_VTABLE, - .water = MOCK_WATER_VTABLE, - .compute = .{ - .bindComputePipeline = bindComputePipeline, - .bindDescriptorSet = bindDescriptorSet, - .createComputeBuffer = createComputeBuffer, - .destroyComputeBuffer = destroyComputeBuffer, - .createComputePipeline = createComputePipeline, - .updateComputeDescriptors = updateComputeDescriptors, - .destroyComputePipeline = destroyComputePipeline, - .dispatch = dispatchCompute, - .pushConstants = pushComputeConstants, - .fillBuffer = fillComputeBuffer, - .copyBuffer = copyComputeBuffer, - .pipelineBarrier = computePipelineBarrier, - .bufferBarrier = computeBufferBarrier, - .waitForFrameFence = waitForFrameFence, - .hasCommandBuffer = hasCommandBuffer, - }, - .ui = MOCK_UI_VTABLE, - .query = MOCK_QUERY_VTABLE, - .timing = .{ - .beginPassTiming = beginPassTiming, - .endPassTiming = endPassTiming, - .getTimingResults = getTimingResults, - .isTimingEnabled = isTimingEnabled, - .setTimingEnabled = setTimingEnabled, - }, - .quality = .{ - .setWireframe = undefined, - .setTexturesEnabled = undefined, - .setDebugShadowView = undefined, - .setShadowDebugChannel = undefined, - .setVSync = undefined, - .setAnisotropicFiltering = undefined, - .setVolumetricDensity = undefined, - .setShadowResolution = undefined, - .setMSAA = undefined, - .setFXAA = undefined, - .setBloom = undefined, - .setBloomIntensity = undefined, - .setVignetteEnabled = undefined, - .setVignetteIntensity = undefined, - .setFilmGrainEnabled = undefined, - .setFilmGrainIntensity = undefined, - .setColorGradingEnabled = undefined, - .setColorGradingIntensity = undefined, - .setTAABlendFactor = undefined, - .setTAAVelocityRejection = undefined, - .setDynamicResolution = MockContext.setDynamicResolution, - .getResolutionScale = MockContext.getResolutionScale, - }, - .recovery = .{ - .recover = undefined, - }, - .culling_factory = .{ - .createCullingSystem = MockContext.createCullingSystem, - .createLODCullingSystem = MockContext.createLODCullingSystem, - }, - .screenshot = .{ - .captureFrame = undefined, - }, + .resources = &MOCK_RESOURCES_VTABLE, + .render = &MOCK_RENDER_VTABLE, + .passes = &MOCK_PASSES_VTABLE, + .post_process = &MOCK_POST_PROCESS_VTABLE, + .effects = &MOCK_EFFECTS_VTABLE, + .vulkan = &MOCK_NATIVE_VTABLE, + .ssao = &MOCK_SSAO_VTABLE, + .debug_overlay = &MOCK_DEBUG_OVERLAY_VTABLE, + .shadow = &MOCK_SHADOW_VTABLE, + .water = &MOCK_WATER_VTABLE, + .compute = &MOCK_COMPUTE_VTABLE, + .ui = &MOCK_UI_VTABLE, + .query = &MOCK_QUERY_VTABLE, + .timing = &MOCK_TIMING_VTABLE, + .quality = &MOCK_QUALITY_VTABLE, + .recovery = &MOCK_RECOVERY_VTABLE, + .culling_factory = &MOCK_CULLING_FACTORY_VTABLE, + .screenshot = &MOCK_SCREENSHOT_VTABLE, }; const MOCK_ENCODER_VTABLE = rhi.IGraphicsCommandEncoder.VTable{ @@ -590,8 +602,6 @@ const MockContext = struct { .draw = draw, .drawOffset = drawOffset, .drawIndexed = drawIndexed, - .drawCompactLOD = undefined, - .drawCompactLODIndirectCount = undefined, .drawIndirect = drawIndirect, .drawIndirectCount = drawIndirectCount, .drawInstance = drawInstance, @@ -601,10 +611,6 @@ const MockContext = struct { const MOCK_STATE_VTABLE = rhi.IRenderStateContext.VTable{ .setModelMatrix = undefined, .setInstanceBuffer = undefined, - .setLODInstanceBuffer = undefined, - .setLODDescriptorStream = undefined, - .setLODCompactSampleBuffer = undefined, - .setLODCompactInstanceBuffer = undefined, .setTerrainPipelineBound = undefined, .setSelectionMode = undefined, .updateGlobalUniforms = undefined, @@ -636,11 +642,10 @@ test "IRenderContext getEncoder" { try testing.expectEqual(&MockContext.MOCK_STATE_VTABLE, state.vtable); } -test "indirect model uniforms use alpha sentinel without consuming mask sign" { +test "indirect model uniforms use the instance-data alpha sentinel" { const uniforms = @import("vulkan/rhi_draw_submission.zig").indirectModelUniforms(); try testing.expect(uniforms.color[3] < 0.0); - try testing.expectEqual(@as(f32, 0.0), uniforms.mask_radius); } test "AtmosphereSystem.renderSky with null handles" { diff --git a/modules/engine-graphics/src/rhi_vulkan.zig b/modules/engine-graphics/src/rhi_vulkan.zig index 4d4639b7..ea4ccabb 100644 --- a/modules/engine-graphics/src/rhi_vulkan.zig +++ b/modules/engine-graphics/src/rhi_vulkan.zig @@ -22,7 +22,6 @@ const rhi_timing = @import("vulkan/rhi_timing.zig"); const screenshot = @import("vulkan/screenshot.zig"); const Utils = @import("vulkan/utils.zig"); const CullingSystem = @import("vulkan/culling_system.zig").CullingSystem; -const lod_culling = @import("vulkan/lod_culling_system.zig"); const build_options = @import("engine_graphics_options"); const imgui_c = if (build_options.imgui) @cImport({ @@ -415,11 +414,6 @@ fn createCullingSystem(ctx_ptr: *anyopaque, allocator: std.mem.Allocator, max_ch return system.interface(); } -fn createLODCullingSystem(ctx_ptr: *anyopaque, allocator: std.mem.Allocator, max_regions: usize) anyerror!?rhi.ILODCullingSystem { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - return @as(?rhi.ILODCullingSystem, try lod_culling.create(allocator, ctx, max_regions)); -} - fn captureFrame(ctx_ptr: *anyopaque, path: []const u8) bool { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); @@ -481,9 +475,9 @@ fn updateGlobalUniforms(ctx_ptr: *anyopaque, uniforms: rhi.GlobalUniforms, frame try render_state.updateGlobalUniforms(ctx, uniforms, frame_params); } -fn setModelMatrix(ctx_ptr: *anyopaque, model: Mat4, color: Vec3, mask_radius: f32) void { +fn setModelMatrix(ctx_ptr: *anyopaque, model: Mat4, color: Vec3) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - render_state.setModelMatrix(ctx, model, color, mask_radius); + render_state.setModelMatrix(ctx, model, color); } fn setInstanceBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { @@ -491,26 +485,6 @@ fn setInstanceBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { render_state.setInstanceBuffer(ctx, handle); } -fn setLODInstanceBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - render_state.setLODInstanceBuffer(ctx, handle); -} - -fn setLODDescriptorStream(ctx_ptr: *anyopaque, stream: rhi.LODDescriptorStream) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - render_state.setLODDescriptorStream(ctx, stream); -} - -fn setLODCompactSampleBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - render_state.setLODCompactSampleBuffer(ctx, handle); -} - -fn setLODCompactInstanceBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - render_state.setLODCompactInstanceBuffer(ctx, handle); -} - fn setTerrainPipelineBound(ctx_ptr: *anyopaque, bound: bool) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); render_state.setTerrainPipelineBound(ctx, bound); @@ -561,7 +535,7 @@ fn bindTexture(ctx_ptr: *anyopaque, handle: rhi.TextureHandle, slot: u32) void { 7, 8 => ctx.draw.dummy_roughness_texture, 9 => ctx.draw.dummy_texture, // LPV shader bindings are sampler3D. A 2D fallback is invalid even - // when LPV lighting is disabled, and snapshotting it made indirect LOD + // when LPV lighting is disabled, and snapshotting it made indirect // terrain undefined on RADV. 11, 12, 13 => ctx.draw.dummy_texture_3d, 0, 1 => ctx.draw.dummy_texture, @@ -624,11 +598,6 @@ fn supportsIndirectCount(ctx_ptr: *anyopaque) bool { return state_control.supportsIndirectCount(ctx); } -fn supportsCompactLODGpuCulling(ctx_ptr: *anyopaque) bool { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - return state_control.supportsCompactLODGpuCulling(ctx); -} - fn recover(ctx_ptr: *anyopaque) anyerror!void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); try state_control.recover(ctx); @@ -724,18 +693,6 @@ fn drawIndexed(ctx_ptr: *anyopaque, vbo_handle: rhi.BufferHandle, ebo_handle: rh draw_submission.drawIndexed(ctx, vbo_handle, ebo_handle, count); } -fn drawCompactLOD(ctx_ptr: *anyopaque, index_handle: rhi.BufferHandle, index_count: u32, params: rhi.CompactLODDraw) bool { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - ctx.mutex.lock(); - defer ctx.mutex.unlock(); - return draw_submission.drawCompactLOD(ctx, index_handle, index_count, params); -} - -fn drawCompactLODIndirectCount(ctx_ptr: *anyopaque, index_handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, count_buffer: rhi.BufferHandle, count_offset: usize, max_draw_count: u32) bool { - const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); - return draw_submission.drawCompactLODIndirectCount(ctx, index_handle, command_buffer, offset, count_buffer, count_offset, max_draw_count); -} - fn drawIndirect(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, draw_count: u32, stride: u32) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); @@ -1120,10 +1077,6 @@ fn getStateContext(ctx_ptr: *anyopaque) rhi.IRenderStateContext { const VULKAN_STATE_CONTEXT_VTABLE = rhi.IRenderStateContext.VTable{ .setModelMatrix = setModelMatrix, .setInstanceBuffer = setInstanceBuffer, - .setLODInstanceBuffer = setLODInstanceBuffer, - .setLODDescriptorStream = setLODDescriptorStream, - .setLODCompactSampleBuffer = setLODCompactSampleBuffer, - .setLODCompactInstanceBuffer = setLODCompactInstanceBuffer, .setTerrainPipelineBound = setTerrainPipelineBound, .setSelectionMode = setSelectionMode, .updateGlobalUniforms = updateGlobalUniforms, @@ -1141,8 +1094,6 @@ const VULKAN_COMMAND_ENCODER_VTABLE = rhi.IGraphicsCommandEncoder.VTable{ .draw = draw, .drawOffset = drawOffset, .drawIndexed = drawIndexed, - .drawCompactLOD = drawCompactLOD, - .drawCompactLODIndirectCount = drawCompactLODIndirectCount, .drawIndirect = drawIndirect, .drawIndirectCount = drawIndirectCount, .drawInstance = drawInstance, @@ -1492,7 +1443,6 @@ const VULKAN_DEVICE_QUERY_VTABLE = rhi.IDeviceQuery.VTable{ .getFrameIndex = getFrameIndex, .supportsIndirectFirstInstance = supportsIndirectFirstInstance, .supportsIndirectCount = supportsIndirectCount, - .supportsCompactLODGpuCulling = supportsCompactLODGpuCulling, .getMaxAnisotropy = getMaxAnisotropy, .getMaxMSAASamples = getMaxMSAASamples, .getFaultCount = getFaultCount, @@ -1542,7 +1492,6 @@ const VULKAN_DEVICE_RECOVERY_VTABLE = rhi.IDeviceRecovery.VTable{ const VULKAN_CULLING_FACTORY_VTABLE = rhi.ICullingSystemFactory.VTable{ .createCullingSystem = createCullingSystem, - .createLODCullingSystem = createLODCullingSystem, }; const VULKAN_SCREENSHOT_CONTEXT_VTABLE = rhi.IScreenshotContext.VTable{ diff --git a/modules/engine-graphics/src/root.zig b/modules/engine-graphics/src/root.zig index 54c48f80..fcb2c506 100644 --- a/modules/engine-graphics/src/root.zig +++ b/modules/engine-graphics/src/root.zig @@ -18,7 +18,6 @@ pub const render_system = @import("render_system.zig"); pub const resource_pack = @import("engine-assets").resource_pack; pub const rhi_vulkan = @import("rhi_vulkan.zig"); pub const lpv_system = @import("vulkan/lpv_system.zig"); -pub const lod_culling_system = @import("vulkan/lod_culling_system.zig"); pub const lpv_utils = @import("lpv_utils.zig"); pub const shadow_scene = @import("engine-shadows").shadow_scene; pub const shadow_system = @import("engine-shadows").shadow_system; diff --git a/modules/engine-graphics/src/vulkan/descriptor_bindings.zig b/modules/engine-graphics/src/vulkan/descriptor_bindings.zig index 17358b33..57440889 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_bindings.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_bindings.zig @@ -14,5 +14,3 @@ pub const LPV_TEXTURE_G = 12; // LPV SH Green channel pub const LPV_TEXTURE_B = 13; // LPV SH Blue channel pub const WATER_REFLECTION_TEXTURE = 14; // Water planar reflection color pub const SCENE_DEPTH_TEXTURE = 15; // Scene depth for water refraction -pub const COMPACT_LOD_SAMPLES = 16; // 16-byte far-LOD samples, vertex stage -pub const COMPACT_LOD_INSTANCES = 17; // compact indirect instances, vertex stage diff --git a/modules/engine-graphics/src/vulkan/descriptor_manager.zig b/modules/engine-graphics/src/vulkan/descriptor_manager.zig index 4fd467d7..835af57c 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_manager.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_manager.zig @@ -2,7 +2,6 @@ const std = @import("std"); const c = @import("c").c; const rhi = @import("engine-rhi").rhi; const log = @import("engine-core").log; -const rhi_types = @import("engine-rhi").rhi_types; const VulkanDevice = @import("../vulkan_device.zig").VulkanDevice; const ResourceManager = @import("resource_manager.zig").ResourceManager; const VulkanBuffer = @import("resource_manager.zig").VulkanBuffer; @@ -10,40 +9,6 @@ const Mat4 = @import("engine-math").Mat4; pub const ShadowUniforms = @import("shadow_uniforms.zig").ShadowUniforms; const Utils = @import("utils.zig"); -/// Terrain/water × standard/compact × direct/GPU streams never share a set -/// that can be updated after one of them has been recorded. -pub const LOD_DESCRIPTOR_SNAPSHOT_COUNT = 8; - -/// CPU-side mirror of stream-local descriptor bindings. It prevents an update -/// for a later stream from being elided because another stream happens to use -/// the same buffer handle. -pub const LODDescriptorSnapshotBindings = struct { - instance: [LOD_DESCRIPTOR_SNAPSHOT_COUNT]rhi.BufferHandle = [_]rhi.BufferHandle{0} ** LOD_DESCRIPTOR_SNAPSHOT_COUNT, - compact_samples: [LOD_DESCRIPTOR_SNAPSHOT_COUNT]rhi.BufferHandle = [_]rhi.BufferHandle{0} ** LOD_DESCRIPTOR_SNAPSHOT_COUNT, - compact_instances: [LOD_DESCRIPTOR_SNAPSHOT_COUNT]rhi.BufferHandle = [_]rhi.BufferHandle{0} ** LOD_DESCRIPTOR_SNAPSHOT_COUNT, -}; - -/// Tracks the one-time texture/UBO acquisition for a descriptor snapshot. -/// A changed material revision after sealing is a correctness violation, not a -/// reason to mutate commands already recorded against the set. -pub const LODDescriptorSnapshotSeal = struct { - sealed: [LOD_DESCRIPTOR_SNAPSHOT_COUNT]bool = [_]bool{false} ** LOD_DESCRIPTOR_SNAPSHOT_COUNT, - texture_revision: [LOD_DESCRIPTOR_SNAPSHOT_COUNT]u64 = [_]u64{0} ** LOD_DESCRIPTOR_SNAPSHOT_COUNT, - - /// Returns true exactly once per stream/frame-slot. A later acquisition - /// must use the same material state or select a different snapshot. - pub fn acquire(self: *LODDescriptorSnapshotSeal, stream: rhi.LODDescriptorStream, revision: u64) error{TextureStateChanged}!bool { - const index = @intFromEnum(stream); - if (self.sealed[index]) { - if (self.texture_revision[index] != revision) return error.TextureStateChanged; - return false; - } - self.sealed[index] = true; - self.texture_revision[index] = revision; - return true; - } -}; - const GlobalUniforms = extern struct { view_proj: Mat4, view_proj_prev: Mat4, @@ -71,10 +36,6 @@ pub const DescriptorManager = struct { descriptor_pool: c.VkDescriptorPool, descriptor_set_layout: c.VkDescriptorSetLayout, descriptor_sets: [rhi.MAX_FRAMES_IN_FLIGHT]c.VkDescriptorSet, - /// Retained for older initialization paths; LOD rendering uses only the - /// explicit immutable stream snapshots below. - lod_descriptor_sets: [rhi.MAX_FRAMES_IN_FLIGHT]c.VkDescriptorSet, - lod_descriptor_snapshots: [rhi.MAX_FRAMES_IN_FLIGHT][LOD_DESCRIPTOR_SNAPSHOT_COUNT]c.VkDescriptorSet = undefined, global_ubos: [rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer, global_ubos_mapped: [rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque, @@ -83,8 +44,6 @@ pub const DescriptorManager = struct { shadow_ubos_mapped: [rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque, dummy_instance_ssbo: VulkanBuffer, - dummy_compact_lod_ssbo: VulkanBuffer, - dummy_compact_lod_instance_ssbo: VulkanBuffer, // Dummy textures dummy_texture: rhi.TextureHandle, @@ -100,15 +59,11 @@ pub const DescriptorManager = struct { .descriptor_pool = null, .descriptor_set_layout = null, .descriptor_sets = undefined, - .lod_descriptor_sets = undefined, - .lod_descriptor_snapshots = undefined, .global_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), .global_ubos_mapped = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque), .shadow_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), .shadow_ubos_mapped = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque), .dummy_instance_ssbo = std.mem.zeroes(VulkanBuffer), - .dummy_compact_lod_ssbo = std.mem.zeroes(VulkanBuffer), - .dummy_compact_lod_instance_ssbo = std.mem.zeroes(VulkanBuffer), .dummy_texture = 0, .dummy_texture_3d = 0, .dummy_normal_texture = 0, @@ -134,15 +89,6 @@ pub const DescriptorManager = struct { self.deinit(); return err; }; - self.dummy_compact_lod_ssbo = Utils.createVulkanBuffer(vulkan_device, 256, c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) catch |err| { - self.deinit(); - return err; - }; - self.dummy_compact_lod_instance_ssbo = Utils.createVulkanBuffer(vulkan_device, 256, c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) catch |err| { - self.deinit(); - return err; - }; - // Create dummy textures at frame index 1 to isolate from frame 0's lifecycle. resource_manager.setCurrentFrame(1); @@ -182,9 +128,7 @@ pub const DescriptorManager = struct { var pool_sizes = [_]c.VkDescriptorPoolSize{ .{ .type = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .descriptorCount = 500 }, .{ .type = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1000 }, - // 2 frames × 8 immutable LOD snapshots × 3 storage bindings, - // plus legacy, UI, and effect descriptors. - .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 160 }, + .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 32 }, }; var pool_info = std.mem.zeroes(c.VkDescriptorPoolCreateInfo); @@ -233,10 +177,6 @@ pub const DescriptorManager = struct { .{ .binding = 14, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, // 15: Scene depth texture (for water refraction) .{ .binding = 15, .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_FRAGMENT_BIT }, - // 16: Compact LOD sample SSBO, consumed only by vertex-pulling shaders. - .{ .binding = 16, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_VERTEX_BIT }, - // 17: Compact LOD indirect instance SSBO. - .{ .binding = 17, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_VERTEX_BIT }, }; var layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); @@ -261,16 +201,6 @@ pub const DescriptorManager = struct { self.deinit(); return err; }; - Utils.checkVk(c.vkAllocateDescriptorSets(vulkan_device.vk_device, &alloc_info, &self.lod_descriptor_sets[i])) catch |err| { - self.deinit(); - return err; - }; - for (&self.lod_descriptor_snapshots[i]) |*snapshot| { - Utils.checkVk(c.vkAllocateDescriptorSets(vulkan_device.vk_device, &alloc_info, snapshot)) catch |err| { - self.deinit(); - return err; - }; - } // Write UBO descriptors immediately (they don't change) var buffer_info_global = c.VkDescriptorBufferInfo{ @@ -288,9 +218,6 @@ pub const DescriptorManager = struct { .offset = 0, .range = 256, }; - var buffer_info_compact = c.VkDescriptorBufferInfo{ .buffer = self.dummy_compact_lod_ssbo.buffer, .offset = 0, .range = 256 }; - var buffer_info_compact_instance = c.VkDescriptorBufferInfo{ .buffer = self.dummy_compact_lod_instance_ssbo.buffer, .offset = 0, .range = 256 }; - var writes = [_]c.VkWriteDescriptorSet{ .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, @@ -316,43 +243,11 @@ pub const DescriptorManager = struct { .descriptorCount = 1, .pBufferInfo = &buffer_info_instance, }, - .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.descriptor_sets[i], .dstBinding = 16, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .pBufferInfo = &buffer_info_compact }, - .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.descriptor_sets[i], .dstBinding = 17, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .pBufferInfo = &buffer_info_compact_instance }, - .{ - .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .dstSet = self.lod_descriptor_sets[i], - .dstBinding = 0, - .descriptorType = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - .descriptorCount = 1, - .pBufferInfo = &buffer_info_global, - }, - .{ - .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .dstSet = self.lod_descriptor_sets[i], - .dstBinding = 2, - .descriptorType = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, - .descriptorCount = 1, - .pBufferInfo = &buffer_info_shadow, - }, - .{ - .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - .dstSet = self.lod_descriptor_sets[i], - .dstBinding = 5, - .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, - .descriptorCount = 1, - .pBufferInfo = &buffer_info_instance, - }, - .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.lod_descriptor_sets[i], .dstBinding = 16, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .pBufferInfo = &buffer_info_compact }, - .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = self.lod_descriptor_sets[i], .dstBinding = 17, .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .pBufferInfo = &buffer_info_compact_instance }, }; c.vkUpdateDescriptorSets(vulkan_device.vk_device, writes.len, &writes[0], 0, null); - // A stream snapshot may be acquired before a material has supplied - // its first texture update. Seed every sampled binding in the - // ordinary source set with a type-correct dummy now: copying an - // unwritten descriptor into an immutable LOD set is undefined and - // in particular turns the sampler3D LPV bindings into arbitrary - // 2D views on RADV. + // Seed every sampled binding with a type-correct dummy before a + // material supplies its first texture update. const dummy_2d = self.resource_manager.textures.get(self.dummy_texture).?; const dummy_3d = self.resource_manager.textures.get(self.dummy_texture_3d).?; var dummy_2d_info = c.VkDescriptorImageInfo{ @@ -387,25 +282,6 @@ pub const DescriptorManager = struct { }; } c.vkUpdateDescriptorSets(vulkan_device.vk_device, texture_writes.len, &texture_writes[0], 0, null); - - // Snapshot the bindings used by all LOD shaders. The render path - // subsequently updates only the matching stream's set, so a water - // update cannot retroactively change recorded terrain commands. - var snapshot_writes: [LOD_DESCRIPTOR_SNAPSHOT_COUNT * 5]c.VkWriteDescriptorSet = undefined; - var write_index: usize = 0; - for (self.lod_descriptor_snapshots[i]) |snapshot| { - for ([_]struct { binding: u32, kind: c.VkDescriptorType, info: *c.VkDescriptorBufferInfo }{ - .{ .binding = 0, .kind = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .info = &buffer_info_global }, - .{ .binding = 2, .kind = c.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .info = &buffer_info_shadow }, - .{ .binding = 5, .kind = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .info = &buffer_info_instance }, - .{ .binding = 16, .kind = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .info = &buffer_info_compact }, - .{ .binding = 17, .kind = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .info = &buffer_info_compact_instance }, - }) |entry| { - snapshot_writes[write_index] = .{ .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstSet = snapshot, .dstBinding = entry.binding, .descriptorType = entry.kind, .descriptorCount = 1, .pBufferInfo = entry.info }; - write_index += 1; - } - } - c.vkUpdateDescriptorSets(vulkan_device.vk_device, @intCast(write_index), &snapshot_writes, 0, null); } return self; @@ -434,16 +310,6 @@ pub const DescriptorManager = struct { c.vkDestroyBuffer(device, self.dummy_instance_ssbo.buffer, null); c.vkFreeMemory(device, self.dummy_instance_ssbo.memory, null); } - if (self.dummy_compact_lod_ssbo.buffer != null) { - if (self.dummy_compact_lod_ssbo.mapped_ptr != null) c.vkUnmapMemory(device, self.dummy_compact_lod_ssbo.memory); - c.vkDestroyBuffer(device, self.dummy_compact_lod_ssbo.buffer, null); - c.vkFreeMemory(device, self.dummy_compact_lod_ssbo.memory, null); - } - if (self.dummy_compact_lod_instance_ssbo.buffer != null) { - if (self.dummy_compact_lod_instance_ssbo.mapped_ptr != null) c.vkUnmapMemory(device, self.dummy_compact_lod_instance_ssbo.memory); - c.vkDestroyBuffer(device, self.dummy_compact_lod_instance_ssbo.buffer, null); - c.vkFreeMemory(device, self.dummy_compact_lod_instance_ssbo.memory, null); - } if (self.descriptor_set_layout != null) c.vkDestroyDescriptorSetLayout(device, self.descriptor_set_layout, null); if (self.descriptor_pool != null) c.vkDestroyDescriptorPool(device, self.descriptor_pool, null); @@ -466,12 +332,6 @@ pub const DescriptorManager = struct { @memcpy(@as([*]u8, @ptrCast(dest))[0..@sizeOf(ShadowUniforms)], std.mem.asBytes(data)); } - /// Returns the descriptor snapshot dedicated to this explicit LOD stream. - /// The selected set is safe to bind for the whole frame slot lifetime. - pub fn lodDescriptorSet(self: *const DescriptorManager, frame_index: usize, stream: rhi.LODDescriptorStream) c.VkDescriptorSet { - return self.lod_descriptor_snapshots[frame_index][@intFromEnum(stream)]; - } - // Additional methods for binding textures would go here // For now, we assume VulkanContext handles the complexity of gathering textures and calling a mass update }; diff --git a/modules/engine-graphics/src/vulkan/descriptor_manager_error_tests.zig b/modules/engine-graphics/src/vulkan/descriptor_manager_error_tests.zig index ea402fca..07f9bac6 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_manager_error_tests.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_manager_error_tests.zig @@ -17,7 +17,6 @@ test "DescriptorManager field types are correct for null initialization" { .descriptor_pool = null, .descriptor_set_layout = null, .descriptor_sets = undefined, - .lod_descriptor_sets = undefined, .global_ubos = undefined, .global_ubos_mapped = undefined, .shadow_ubos = undefined, @@ -43,7 +42,6 @@ test "updateGlobalUniforms returns error.UnmappedBuffer for invalid frame" { .descriptor_pool = null, .descriptor_set_layout = null, .descriptor_sets = undefined, - .lod_descriptor_sets = undefined, .global_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), .global_ubos_mapped = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque), .shadow_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), @@ -74,7 +72,6 @@ test "updateShadowUniforms returns error.UnmappedBuffer for invalid frame" { .descriptor_pool = null, .descriptor_set_layout = null, .descriptor_sets = undefined, - .lod_descriptor_sets = undefined, .global_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), .global_ubos_mapped = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque), .shadow_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]VulkanBuffer), diff --git a/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig b/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig index ba214b69..6c3da99e 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig @@ -24,8 +24,6 @@ test "DescriptorManager default state has null handles" { .descriptor_pool = null, .descriptor_set_layout = null, .descriptor_sets = undefined, - .lod_descriptor_sets = undefined, - .lod_descriptor_snapshots = undefined, .global_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]@import("resource_manager.zig").VulkanBuffer), .global_ubos_mapped = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]?*anyopaque), .shadow_ubos = std.mem.zeroes([rhi.MAX_FRAMES_IN_FLIGHT]@import("resource_manager.zig").VulkanBuffer), @@ -160,52 +158,8 @@ test "ShadowUniforms cascade array alignment" { // ============================================================================ test "descriptor sets arrays match MAX_FRAMES_IN_FLIGHT" { - // The descriptor_sets and lod_descriptor_sets arrays should match - // MAX_FRAMES_IN_FLIGHT from rhi module - const set_count = @typeInfo(@TypeOf(@as(DescriptorManager, undefined).descriptor_sets)).array.len; - const lod_set_count = @typeInfo(@TypeOf(@as(DescriptorManager, undefined).lod_descriptor_sets)).array.len; - try testing.expectEqual(@as(usize, rhi.MAX_FRAMES_IN_FLIGHT), set_count); - try testing.expectEqual(@as(usize, rhi.MAX_FRAMES_IN_FLIGHT), lod_set_count); -} - -test "LOD descriptor snapshots reserve separate terrain and water streams" { - try testing.expectEqual(@as(usize, 8), descriptor_manager.LOD_DESCRIPTOR_SNAPSHOT_COUNT); - const snapshots = @typeInfo(@TypeOf(@as(DescriptorManager, undefined).lod_descriptor_snapshots)).array; - try testing.expectEqual(@as(usize, rhi.MAX_FRAMES_IN_FLIGHT), snapshots.len); - try testing.expectEqual(@as(usize, descriptor_manager.LOD_DESCRIPTOR_SNAPSHOT_COUNT), @typeInfo(snapshots.child).array.len); -} - -test "GPU terrain snapshot bindings survive later CPU fallback and water preparation" { - var bindings = descriptor_manager.LODDescriptorSnapshotBindings{}; - const gpu_terrain = @intFromEnum(rhi.LODDescriptorStream.terrain_standard_gpu); - const cpu_terrain = @intFromEnum(rhi.LODDescriptorStream.terrain_standard_direct); - const gpu_water = @intFromEnum(rhi.LODDescriptorStream.water_compact_gpu); - - bindings.instance[gpu_terrain] = 101; - bindings.compact_samples[gpu_terrain] = 102; - bindings.compact_instances[gpu_terrain] = 103; - - // These are the updates that previously shared and mutated terrain's set. - bindings.instance[cpu_terrain] = 201; - bindings.compact_samples[gpu_water] = 301; - bindings.compact_instances[gpu_water] = 302; - - try testing.expectEqual(@as(rhi.BufferHandle, 101), bindings.instance[gpu_terrain]); - try testing.expectEqual(@as(rhi.BufferHandle, 102), bindings.compact_samples[gpu_terrain]); - try testing.expectEqual(@as(rhi.BufferHandle, 103), bindings.compact_instances[gpu_terrain]); -} - -test "LOD texture snapshot seals once and rejects a later material revision" { - var seal = descriptor_manager.LODDescriptorSnapshotSeal{}; - try testing.expect(try seal.acquire(.terrain_standard_gpu, 41)); - try testing.expect(!(try seal.acquire(.terrain_standard_gpu, 41))); - try testing.expectError(error.TextureStateChanged, seal.acquire(.terrain_standard_gpu, 42)); - - // Later water preparation is independent and does not reopen GPU terrain. - try testing.expect(try seal.acquire(.water_compact_direct, 42)); - try testing.expectError(error.TextureStateChanged, seal.acquire(.terrain_standard_gpu, 42)); } test "UBO arrays match MAX_FRAMES_IN_FLIGHT" { diff --git a/modules/engine-graphics/src/vulkan/fxaa_system.zig b/modules/engine-graphics/src/vulkan/fxaa_system.zig index 1e8c91bd..4b63e95d 100644 --- a/modules/engine-graphics/src/vulkan/fxaa_system.zig +++ b/modules/engine-graphics/src/vulkan/fxaa_system.zig @@ -86,6 +86,8 @@ pub const FXAASystem = struct { dependency.srcSubpass = c.VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; dependency.srcStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + // The intermediate pass reuses the display post-process pipeline. + dependency.srcAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; dependency.dstStageMask = c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; diff --git a/modules/engine-graphics/src/vulkan/lod_culling_system.zig b/modules/engine-graphics/src/vulkan/lod_culling_system.zig deleted file mode 100644 index 685c89da..00000000 --- a/modules/engine-graphics/src/vulkan/lod_culling_system.zig +++ /dev/null @@ -1,601 +0,0 @@ -//! Dedicated same-frame compute culling and MDI compaction for distant terrain. -const std = @import("std"); -const fs = @import("fs"); -const c = @import("c").c; -const rhi = @import("engine-rhi"); -const culling = rhi.culling; -const log = @import("engine-core").log; -const VulkanContext = @import("rhi_context_types.zig").VulkanContext; -const Utils = @import("utils.zig"); - -pub const SHADER_PATH = "assets/shaders/vulkan/lod_culling.comp.spv"; -pub const WORKGROUP_SIZE: u32 = 64; -pub const MAX_LOD_LEVELS: usize = 5; -const FRAME_COUNT = rhi.MAX_FRAMES_IN_FLIGHT; - -/// Fixed readback sections used only by the opt-in GPU culling validator. -/// Keeping the command and instance payloads beside the existing counters/IDs -/// lets validation distinguish a visibility error from a bad indirect ABI. -const ValidationLayout = struct { - counters_offset: usize = 0, - ids_offset: usize, - terrain_commands_offset: usize, - water_commands_offset: usize, - compact_terrain_commands_offset: usize, - compact_water_commands_offset: usize, - terrain_instances_offset: usize, - water_instances_offset: usize, - compact_terrain_instances_offset: usize, - compact_water_instances_offset: usize, - total_bytes: usize, - - fn init(capacity: usize) ValidationLayout { - const counters_bytes = MAX_LOD_LEVELS * 4 * @sizeOf(u32); - const ids_bytes = capacity * MAX_LOD_LEVELS * 4 * @sizeOf(u32); - const command_bytes = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndirectCommand); - const compact_command_bytes = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndexedIndirectCommand); - const instance_bytes = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.InstanceData); - const compact_instance_bytes = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.CompactLODInstance); - const ids_offset = counters_bytes; - const terrain_commands_offset = ids_offset + ids_bytes; - const water_commands_offset = terrain_commands_offset + command_bytes; - const compact_terrain_commands_offset = water_commands_offset + command_bytes; - const compact_water_commands_offset = compact_terrain_commands_offset + compact_command_bytes; - const terrain_instances_offset = compact_water_commands_offset + compact_command_bytes; - const water_instances_offset = terrain_instances_offset + instance_bytes; - const compact_terrain_instances_offset = water_instances_offset + instance_bytes; - const compact_water_instances_offset = compact_terrain_instances_offset + compact_instance_bytes; - return .{ - .ids_offset = ids_offset, - .terrain_commands_offset = terrain_commands_offset, - .water_commands_offset = water_commands_offset, - .compact_terrain_commands_offset = compact_terrain_commands_offset, - .compact_water_commands_offset = compact_water_commands_offset, - .terrain_instances_offset = terrain_instances_offset, - .water_instances_offset = water_instances_offset, - .compact_terrain_instances_offset = compact_terrain_instances_offset, - .compact_water_instances_offset = compact_water_instances_offset, - .total_bytes = compact_water_instances_offset + compact_instance_bytes, - }; - } -}; - -const LODCullingSystem = struct { - allocator: std.mem.Allocator, - ctx: *VulkanContext, - capacity: usize, - candidates: [FRAME_COUNT]Utils.VulkanBuffer, - counters: [FRAME_COUNT]rhi.BufferHandle, - terrain_instances: [FRAME_COUNT]rhi.BufferHandle, - water_instances: [FRAME_COUNT]rhi.BufferHandle, - terrain_indirect: [FRAME_COUNT]rhi.BufferHandle, - water_indirect: [FRAME_COUNT]rhi.BufferHandle, - compact_terrain_instances: [FRAME_COUNT]rhi.BufferHandle, - compact_water_instances: [FRAME_COUNT]rhi.BufferHandle, - compact_terrain_indirect: [FRAME_COUNT]rhi.BufferHandle, - compact_water_indirect: [FRAME_COUNT]rhi.BufferHandle, - visible_ids: [FRAME_COUNT]rhi.BufferHandle, - validation_readback: [FRAME_COUNT]Utils.VulkanBuffer, - validation_expected: [FRAME_COUNT][MAX_LOD_LEVELS * 4]u32 = [_][MAX_LOD_LEVELS * 4]u32{[_]u32{0} ** (MAX_LOD_LEVELS * 4)} ** FRAME_COUNT, - validation_expected_ids: [FRAME_COUNT][]u32, - validation_expected_candidates: [FRAME_COUNT][]culling.LODCullCandidate, - validation_candidate_count: [FRAME_COUNT]usize = [_]usize{0} ** FRAME_COUNT, - validation_layout: ValidationLayout, - validation_pending: [FRAME_COUNT]bool = [_]bool{false} ** FRAME_COUNT, - validation_pending_generation: [FRAME_COUNT]u64 = [_]u64{0} ** FRAME_COUNT, - validation_enabled: bool, - diagnostics_state: culling.LODCullDiagnostics = .{}, - descriptor_pool: c.VkDescriptorPool = null, - descriptor_layout: c.VkDescriptorSetLayout = null, - descriptor_sets: [FRAME_COUNT]c.VkDescriptorSet = std.mem.zeroes([FRAME_COUNT]c.VkDescriptorSet), - pipeline_layout: c.VkPipelineLayout = null, - pipeline: c.VkPipeline = null, - - fn init(allocator: std.mem.Allocator, ctx: *VulkanContext, requested_capacity: usize) !*LODCullingSystem { - const self = try allocator.create(LODCullingSystem); - errdefer allocator.destroy(self); - const capacity = std.math.clamp(requested_capacity, 1, 2048); - self.* = .{ - .allocator = allocator, - .ctx = ctx, - .capacity = capacity, - .candidates = std.mem.zeroes([FRAME_COUNT]Utils.VulkanBuffer), - .counters = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .terrain_instances = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .water_instances = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .terrain_indirect = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .water_indirect = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .compact_terrain_instances = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .compact_water_instances = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .compact_terrain_indirect = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .compact_water_indirect = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .visible_ids = [_]rhi.BufferHandle{0} ** FRAME_COUNT, - .validation_readback = std.mem.zeroes([FRAME_COUNT]Utils.VulkanBuffer), - .validation_expected_ids = undefined, - .validation_expected_candidates = undefined, - .validation_layout = ValidationLayout.init(capacity), - .validation_enabled = envFlag("ZIGCRAFT_LOD_GPU_CULLING_VALIDATE"), - }; - for (&self.validation_expected_ids, &self.validation_expected_candidates) |*ids, *candidates| { - ids.* = &.{}; - candidates.* = &.{}; - } - errdefer self.deinit(); - - const candidate_bytes = capacity * @sizeOf(culling.LODCullCandidate); - const stream_instances = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.InstanceData); - const compact_stream_instances = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.CompactLODInstance); - const stream_commands = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndirectCommand); - const compact_stream_commands = capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndexedIndirectCommand); - const visible_id_count = capacity * MAX_LOD_LEVELS * 4; - for (0..FRAME_COUNT) |i| { - self.candidates[i] = try Utils.createVulkanBuffer(&ctx.vulkan_device, candidate_bytes, c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); - self.counters[i] = try ctx.resources.createBuffer(MAX_LOD_LEVELS * 4 * @sizeOf(u32), .indirect); - self.terrain_instances[i] = try ctx.resources.createBuffer(stream_instances, .storage); - self.water_instances[i] = try ctx.resources.createBuffer(stream_instances, .storage); - self.terrain_indirect[i] = try ctx.resources.createBuffer(stream_commands, .indirect); - self.water_indirect[i] = try ctx.resources.createBuffer(stream_commands, .indirect); - self.compact_terrain_instances[i] = try ctx.resources.createBuffer(compact_stream_instances, .storage); - self.compact_water_instances[i] = try ctx.resources.createBuffer(compact_stream_instances, .storage); - self.compact_terrain_indirect[i] = try ctx.resources.createBuffer(compact_stream_commands, .indirect); - self.compact_water_indirect[i] = try ctx.resources.createBuffer(compact_stream_commands, .indirect); - self.visible_ids[i] = try ctx.resources.createBuffer(visible_id_count * @sizeOf(u32), .storage); - if (self.validation_enabled) { - self.validation_expected_ids[i] = try allocator.alloc(u32, visible_id_count); - self.validation_expected_candidates[i] = try allocator.alloc(culling.LODCullCandidate, capacity); - self.validation_readback[i] = try Utils.createVulkanBuffer( - &ctx.vulkan_device, - self.validation_layout.total_bytes, - c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, - c.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | c.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, - ); - } - } - try self.initPipeline(); - return self; - } - - fn deinit(self: *LODCullingSystem) void { - self.drainPendingValidation(); - self.destroyPipeline(); - const device = self.ctx.vulkan_device.vk_device; - for (0..FRAME_COUNT) |i| { - destroyNative(device, &self.candidates[i]); - if (self.counters[i] != 0) self.ctx.resources.destroyBuffer(self.counters[i]); - if (self.terrain_instances[i] != 0) self.ctx.resources.destroyBuffer(self.terrain_instances[i]); - if (self.water_instances[i] != 0) self.ctx.resources.destroyBuffer(self.water_instances[i]); - if (self.terrain_indirect[i] != 0) self.ctx.resources.destroyBuffer(self.terrain_indirect[i]); - if (self.water_indirect[i] != 0) self.ctx.resources.destroyBuffer(self.water_indirect[i]); - if (self.compact_terrain_instances[i] != 0) self.ctx.resources.destroyBuffer(self.compact_terrain_instances[i]); - if (self.compact_water_instances[i] != 0) self.ctx.resources.destroyBuffer(self.compact_water_instances[i]); - if (self.compact_terrain_indirect[i] != 0) self.ctx.resources.destroyBuffer(self.compact_terrain_indirect[i]); - if (self.compact_water_indirect[i] != 0) self.ctx.resources.destroyBuffer(self.compact_water_indirect[i]); - if (self.visible_ids[i] != 0) self.ctx.resources.destroyBuffer(self.visible_ids[i]); - destroyNative(device, &self.validation_readback[i]); - if (self.validation_expected_ids[i].len != 0) self.allocator.free(self.validation_expected_ids[i]); - if (self.validation_expected_candidates[i].len != 0) self.allocator.free(self.validation_expected_candidates[i]); - } - self.allocator.destroy(self); - } - - /// Validation readbacks normally complete when a frame slot is reused. On - /// teardown there may be no next dispatch, so wait once and account every - /// pending generation before releasing its mapped storage. - fn drainPendingValidation(self: *LODCullingSystem) void { - if (!self.validation_enabled) return; - var pending = false; - for (self.validation_pending) |slot_pending| pending = pending or slot_pending; - if (!pending) return; - if (c.vkDeviceWaitIdle(self.ctx.vulkan_device.vk_device) != c.VK_SUCCESS) { - log.log.warn("LOD GPU validation could not drain pending generations during teardown", .{}); - return; - } - for (0..FRAME_COUNT) |frame_index| self.validateCompletedFrame(frame_index); - } - - fn dispatch(self: *LODCullingSystem, frame_index: usize, input: []const culling.LODCullCandidate, config: culling.LODCullDispatch) bool { - if (frame_index >= FRAME_COUNT or input.len > self.capacity or input.len == 0) return false; - self.validateCompletedFrame(frame_index); - const command_buffer = self.ctx.frames.command_buffers[frame_index]; - if (command_buffer == null or self.candidates[frame_index].mapped_ptr == null) return false; - @memcpy(@as([*]u8, @ptrCast(self.candidates[frame_index].mapped_ptr.?))[0 .. input.len * @sizeOf(culling.LODCullCandidate)], std.mem.sliceAsBytes(input)); - - _ = self.lookup(self.terrain_instances[frame_index]) orelse return false; - _ = self.lookup(self.water_instances[frame_index]) orelse return false; - const terrain_indirect = self.lookup(self.terrain_indirect[frame_index]) orelse return false; - const water_indirect = self.lookup(self.water_indirect[frame_index]) orelse return false; - const cmd = command_buffer; - const counter_bytes = MAX_LOD_LEVELS * 4 * @sizeOf(u32); - const stream_command_bytes = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndirectCommand); - const compact_stream_command_bytes = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.DrawIndexedIndirectCommand); - const counters = self.lookup(self.counters[frame_index]) orelse return false; - const visible_ids = self.lookup(self.visible_ids[frame_index]) orelse return false; - c.vkCmdFillBuffer(cmd, counters.buffer, 0, counter_bytes, 0); - c.vkCmdFillBuffer(cmd, terrain_indirect.buffer, 0, stream_command_bytes, 0); - c.vkCmdFillBuffer(cmd, water_indirect.buffer, 0, stream_command_bytes, 0); - const compact_terrain_indirect = self.lookup(self.compact_terrain_indirect[frame_index]) orelse return false; - const compact_water_indirect = self.lookup(self.compact_water_indirect[frame_index]) orelse return false; - c.vkCmdFillBuffer(cmd, compact_terrain_indirect.buffer, 0, compact_stream_command_bytes, 0); - c.vkCmdFillBuffer(cmd, compact_water_indirect.buffer, 0, compact_stream_command_bytes, 0); - var transfer_to_compute = std.mem.zeroes(c.VkMemoryBarrier); - transfer_to_compute.sType = c.VK_STRUCTURE_TYPE_MEMORY_BARRIER; - transfer_to_compute.srcAccessMask = c.VK_ACCESS_HOST_WRITE_BIT | c.VK_ACCESS_TRANSFER_WRITE_BIT; - transfer_to_compute.dstAccessMask = c.VK_ACCESS_SHADER_READ_BIT | c.VK_ACCESS_SHADER_WRITE_BIT; - c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_TRANSFER_BIT | c.VK_PIPELINE_STAGE_HOST_BIT, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &transfer_to_compute, 0, null, 0, null); - - var push = config; - push.candidate_count = @intCast(input.len); - push.max_commands_per_lod = @intCast(self.capacity); - c.vkCmdBindPipeline(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.pipeline); - c.vkCmdBindDescriptorSets(cmd, c.VK_PIPELINE_BIND_POINT_COMPUTE, self.pipeline_layout, 0, 1, &self.descriptor_sets[frame_index], 0, null); - c.vkCmdPushConstants(cmd, self.pipeline_layout, c.VK_SHADER_STAGE_COMPUTE_BIT, 0, @sizeOf(culling.LODCullDispatch), &push); - c.vkCmdDispatch(cmd, @divFloor(@as(u32, @intCast(input.len)) + WORKGROUP_SIZE - 1, WORKGROUP_SIZE), 1, 1); - - if (self.validation_enabled) { - var compute_to_copy = std.mem.zeroes(c.VkMemoryBarrier); - compute_to_copy.sType = c.VK_STRUCTURE_TYPE_MEMORY_BARRIER; - compute_to_copy.srcAccessMask = c.VK_ACCESS_SHADER_WRITE_BIT; - compute_to_copy.dstAccessMask = c.VK_ACCESS_TRANSFER_READ_BIT; - c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &compute_to_copy, 0, null, 0, null); - const copy = c.VkBufferCopy{ .srcOffset = 0, .dstOffset = self.validation_layout.counters_offset, .size = counter_bytes }; - c.vkCmdCopyBuffer(cmd, counters.buffer, self.validation_readback[frame_index].buffer, 1, ©); - const id_bytes = self.capacity * MAX_LOD_LEVELS * 4 * @sizeOf(u32); - const id_copy = c.VkBufferCopy{ .srcOffset = 0, .dstOffset = self.validation_layout.ids_offset, .size = id_bytes }; - c.vkCmdCopyBuffer(cmd, visible_ids.buffer, self.validation_readback[frame_index].buffer, 1, &id_copy); - const copies = [_]struct { source: c.VkBuffer, destination_offset: usize, size: usize }{ - .{ .source = terrain_indirect.buffer, .destination_offset = self.validation_layout.terrain_commands_offset, .size = stream_command_bytes }, - .{ .source = water_indirect.buffer, .destination_offset = self.validation_layout.water_commands_offset, .size = stream_command_bytes }, - .{ .source = compact_terrain_indirect.buffer, .destination_offset = self.validation_layout.compact_terrain_commands_offset, .size = compact_stream_command_bytes }, - .{ .source = compact_water_indirect.buffer, .destination_offset = self.validation_layout.compact_water_commands_offset, .size = compact_stream_command_bytes }, - .{ .source = self.lookup(self.terrain_instances[frame_index]).?.buffer, .destination_offset = self.validation_layout.terrain_instances_offset, .size = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.InstanceData) }, - .{ .source = self.lookup(self.water_instances[frame_index]).?.buffer, .destination_offset = self.validation_layout.water_instances_offset, .size = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.InstanceData) }, - .{ .source = self.lookup(self.compact_terrain_instances[frame_index]).?.buffer, .destination_offset = self.validation_layout.compact_terrain_instances_offset, .size = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.CompactLODInstance) }, - .{ .source = self.lookup(self.compact_water_instances[frame_index]).?.buffer, .destination_offset = self.validation_layout.compact_water_instances_offset, .size = self.capacity * MAX_LOD_LEVELS * @sizeOf(rhi.CompactLODInstance) }, - }; - for (copies) |entry| { - const payload_copy = c.VkBufferCopy{ .srcOffset = 0, .dstOffset = @intCast(entry.destination_offset), .size = @intCast(entry.size) }; - c.vkCmdCopyBuffer(cmd, entry.source, self.validation_readback[frame_index].buffer, 1, &payload_copy); - } - var copy_to_host = std.mem.zeroes(c.VkMemoryBarrier); - copy_to_host.sType = c.VK_STRUCTURE_TYPE_MEMORY_BARRIER; - copy_to_host.srcAccessMask = c.VK_ACCESS_TRANSFER_WRITE_BIT; - copy_to_host.dstAccessMask = c.VK_ACCESS_HOST_READ_BIT; - c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_TRANSFER_BIT, c.VK_PIPELINE_STAGE_HOST_BIT, 0, 1, ©_to_host, 0, null, 0, null); - self.validation_expected[frame_index] = cpuExpectedCounts(input, push); - fillExpectedIds(self.validation_expected_ids[frame_index], input, push, self.capacity); - @memcpy(self.validation_expected_candidates[frame_index][0..input.len], input); - self.validation_candidate_count[frame_index] = input.len; - self.diagnostics_state.validation_generation +|= 1; - self.validation_pending_generation[frame_index] = self.diagnostics_state.validation_generation; - self.validation_pending[frame_index] = true; - } - - var compute_to_draw = std.mem.zeroes(c.VkMemoryBarrier); - compute_to_draw.sType = c.VK_STRUCTURE_TYPE_MEMORY_BARRIER; - compute_to_draw.srcAccessMask = c.VK_ACCESS_SHADER_WRITE_BIT; - compute_to_draw.dstAccessMask = c.VK_ACCESS_INDIRECT_COMMAND_READ_BIT | c.VK_ACCESS_SHADER_READ_BIT; - c.vkCmdPipelineBarrier(cmd, c.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, c.VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT | c.VK_PIPELINE_STAGE_VERTEX_SHADER_BIT, 0, 1, &compute_to_draw, 0, null, 0, null); - return true; - } - - fn validateCompletedFrame(self: *LODCullingSystem, frame_index: usize) void { - if (!self.validation_enabled or !self.validation_pending[frame_index]) return; - // A frame slot is dispatched only after its fence has completed, so - // coherent transfer results are safe to read here without a stall. - const mapped = self.validation_readback[frame_index].mapped_ptr orelse return; - const bytes: [*]const u8 = @ptrCast(mapped); - const actual: *const [MAX_LOD_LEVELS * 4]u32 = @ptrCast(@alignCast(bytes + self.validation_layout.counters_offset)); - var mismatch = !std.mem.eql(u32, actual, &self.validation_expected[frame_index]); - const actual_ids: [*]u32 = @ptrCast(@alignCast(@as([*]u8, @ptrCast(mapped)) + self.validation_layout.ids_offset)); - for (0..MAX_LOD_LEVELS * 4) |stream| { - const count = @min(actual[stream], @as(u32, @intCast(self.capacity))); - const start = stream * self.capacity; - for (0..count) |slot| { - const candidate_index = actual_ids[start + slot]; - if (candidate_index >= self.validation_candidate_count[frame_index]) { - mismatch = true; - continue; - } - const candidate = self.validation_expected_candidates[frame_index][candidate_index]; - if (!matchesPayload(self, bytes, stream, slot, candidate)) mismatch = true; - } - const gpu_slice = actual_ids[start .. start + count]; - const cpu_slice = self.validation_expected_ids[frame_index][start .. start + count]; - std.mem.sort(u32, gpu_slice, {}, std.sort.asc(u32)); - std.mem.sort(u32, cpu_slice, {}, std.sort.asc(u32)); - if (!std.mem.eql(u32, gpu_slice, cpu_slice)) mismatch = true; - } - if (mismatch) { - self.diagnostics_state.validation_mismatch_count +|= 1; - } - self.diagnostics_state.validation_completed_generation = self.validation_pending_generation[frame_index]; - self.diagnostics_state.validation_completed_count +|= 1; - self.validation_pending[frame_index] = false; - } - - fn matchesPayload(self: *const LODCullingSystem, bytes: [*]const u8, stream: usize, slot: usize, candidate: culling.LODCullCandidate) bool { - const lod = @min(candidate.lod_and_padding[0], MAX_LOD_LEVELS - 1); - const output_index = lod * self.capacity + slot; - const compact = stream >= MAX_LOD_LEVELS * 2; - const water = stream % (MAX_LOD_LEVELS * 2) >= MAX_LOD_LEVELS; - const command = if (water) candidate.water_command else candidate.terrain_command; - var expected_model = candidate.model; - if (!water) expected_model.data[3][1] -= 0.05; - if (!compact) { - const command_offset = (if (water) self.validation_layout.water_commands_offset else self.validation_layout.terrain_commands_offset) + output_index * @sizeOf(rhi.DrawIndirectCommand); - const actual_command: *const rhi.DrawIndirectCommand = @ptrCast(@alignCast(bytes + command_offset)); - const expected_command = rhi.DrawIndirectCommand{ - .vertexCount = command.count, - .instanceCount = command.instance_count, - .firstVertex = command.first, - .firstInstance = @intCast(output_index), - }; - if (!std.mem.eql(u8, std.mem.asBytes(actual_command), std.mem.asBytes(&expected_command))) return false; - const instance_offset = (if (water) self.validation_layout.water_instances_offset else self.validation_layout.terrain_instances_offset) + output_index * @sizeOf(rhi.InstanceData); - const actual_instance: *const rhi.InstanceData = @ptrCast(@alignCast(bytes + instance_offset)); - const expected_instance = rhi.InstanceData{ - .model = expected_model, - .mask_radius = candidate.instance_params[0], - .lod_fade = candidate.instance_params[1], - .padding = .{ candidate.instance_params[2], candidate.instance_params[3] }, - }; - return std.mem.eql(u8, std.mem.asBytes(actual_instance), std.mem.asBytes(&expected_instance)); - } - const command_offset = (if (water) self.validation_layout.compact_water_commands_offset else self.validation_layout.compact_terrain_commands_offset) + output_index * @sizeOf(rhi.DrawIndexedIndirectCommand); - const actual_command: *const rhi.DrawIndexedIndirectCommand = @ptrCast(@alignCast(bytes + command_offset)); - const expected_command = rhi.DrawIndexedIndirectCommand{ - .indexCount = command.count, - .instanceCount = command.instance_count, - .firstIndex = command.first, - .vertexOffset = command.vertex_offset, - .firstInstance = @intCast(output_index), - }; - if (!std.mem.eql(u8, std.mem.asBytes(actual_command), std.mem.asBytes(&expected_command))) return false; - const instance_offset = (if (water) self.validation_layout.compact_water_instances_offset else self.validation_layout.compact_terrain_instances_offset) + output_index * @sizeOf(rhi.CompactLODInstance); - const actual_instance: *const rhi.CompactLODInstance = @ptrCast(@alignCast(bytes + instance_offset)); - const expected_instance = rhi.CompactLODInstance{ - .model = expected_model, - .params = candidate.instance_params, - .words = candidate.compact_words, - }; - return std.mem.eql(u8, std.mem.asBytes(actual_instance), std.mem.asBytes(&expected_instance)); - } - - fn lookup(self: *LODCullingSystem, handle: rhi.BufferHandle) ?Utils.VulkanBuffer { - return self.ctx.resources.buffers.get(handle); - } - - fn initPipeline(self: *LODCullingSystem) !void { - const vk = self.ctx.vulkan_device.vk_device; - var pool_sizes = [_]c.VkDescriptorPoolSize{.{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 11 * FRAME_COUNT }}; - var pool_info = std.mem.zeroes(c.VkDescriptorPoolCreateInfo); - pool_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; - pool_info.maxSets = FRAME_COUNT; - pool_info.poolSizeCount = pool_sizes.len; - pool_info.pPoolSizes = &pool_sizes; - try Utils.checkVk(c.vkCreateDescriptorPool(vk, &pool_info, null, &self.descriptor_pool)); - var bindings: [11]c.VkDescriptorSetLayoutBinding = undefined; - for (&bindings, 0..) |*binding, i| binding.* = .{ .binding = @intCast(i), .descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 1, .stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT, .pImmutableSamplers = null }; - var layout_info = std.mem.zeroes(c.VkDescriptorSetLayoutCreateInfo); - layout_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; - layout_info.bindingCount = bindings.len; - layout_info.pBindings = &bindings; - try Utils.checkVk(c.vkCreateDescriptorSetLayout(vk, &layout_info, null, &self.descriptor_layout)); - var layouts = [_]c.VkDescriptorSetLayout{self.descriptor_layout} ** FRAME_COUNT; - var alloc_info = std.mem.zeroes(c.VkDescriptorSetAllocateInfo); - alloc_info.sType = c.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; - alloc_info.descriptorPool = self.descriptor_pool; - alloc_info.descriptorSetCount = FRAME_COUNT; - alloc_info.pSetLayouts = &layouts; - try Utils.checkVk(c.vkAllocateDescriptorSets(vk, &alloc_info, &self.descriptor_sets)); - self.updateDescriptors(); - var range = std.mem.zeroes(c.VkPushConstantRange); - range.stageFlags = c.VK_SHADER_STAGE_COMPUTE_BIT; - range.size = @sizeOf(culling.LODCullDispatch); - var pipeline_layout_info = std.mem.zeroes(c.VkPipelineLayoutCreateInfo); - pipeline_layout_info.sType = c.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; - pipeline_layout_info.setLayoutCount = 1; - pipeline_layout_info.pSetLayouts = &self.descriptor_layout; - pipeline_layout_info.pushConstantRangeCount = 1; - pipeline_layout_info.pPushConstantRanges = ⦥ - try Utils.checkVk(c.vkCreatePipelineLayout(vk, &pipeline_layout_info, null, &self.pipeline_layout)); - const module = try loadShader(vk, self.allocator); - defer c.vkDestroyShaderModule(vk, module, null); - var stage = std.mem.zeroes(c.VkPipelineShaderStageCreateInfo); - stage.sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; - stage.stage = c.VK_SHADER_STAGE_COMPUTE_BIT; - stage.module = module; - stage.pName = "main"; - var info = std.mem.zeroes(c.VkComputePipelineCreateInfo); - info.sType = c.VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; - info.stage = stage; - info.layout = self.pipeline_layout; - try Utils.checkVk(c.vkCreateComputePipelines(vk, null, 1, &info, null, &self.pipeline)); - } - - fn updateDescriptors(self: *LODCullingSystem) void { - var writes: [11 * FRAME_COUNT]c.VkWriteDescriptorSet = undefined; - var infos: [11 * FRAME_COUNT]c.VkDescriptorBufferInfo = undefined; - var n: usize = 0; - for (0..FRAME_COUNT) |fi| { - const buffers = [_]c.VkBuffer{ - self.candidates[fi].buffer, - self.lookup(self.terrain_instances[fi]).?.buffer, - self.lookup(self.water_instances[fi]).?.buffer, - self.lookup(self.terrain_indirect[fi]).?.buffer, - self.lookup(self.water_indirect[fi]).?.buffer, - self.lookup(self.compact_terrain_instances[fi]).?.buffer, - self.lookup(self.compact_water_instances[fi]).?.buffer, - self.lookup(self.compact_terrain_indirect[fi]).?.buffer, - self.lookup(self.compact_water_indirect[fi]).?.buffer, - self.lookup(self.counters[fi]).?.buffer, - self.lookup(self.visible_ids[fi]).?.buffer, - }; - for (buffers, 0..) |buffer, binding| { - infos[n] = .{ .buffer = buffer, .offset = 0, .range = c.VK_WHOLE_SIZE }; - writes[n] = std.mem.zeroes(c.VkWriteDescriptorSet); - writes[n].sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[n].dstSet = self.descriptor_sets[fi]; - writes[n].dstBinding = @intCast(binding); - writes[n].descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - writes[n].descriptorCount = 1; - writes[n].pBufferInfo = &infos[n]; - n += 1; - } - } - c.vkUpdateDescriptorSets(self.ctx.vulkan_device.vk_device, @intCast(n), &writes, 0, null); - } - - fn destroyPipeline(self: *LODCullingSystem) void { - const vk = self.ctx.vulkan_device.vk_device; - if (self.pipeline != null) c.vkDestroyPipeline(vk, self.pipeline, null); - if (self.pipeline_layout != null) c.vkDestroyPipelineLayout(vk, self.pipeline_layout, null); - if (self.descriptor_layout != null) c.vkDestroyDescriptorSetLayout(vk, self.descriptor_layout, null); - if (self.descriptor_pool != null) c.vkDestroyDescriptorPool(vk, self.descriptor_pool, null); - } -}; - -const VTABLE = culling.ILODCullingSystem.VTable{ - .deinit = struct { - fn call(ptr: *anyopaque) void { - (@as(*LODCullingSystem, @ptrCast(@alignCast(ptr)))).deinit(); - } - }.call, - .dispatch = struct { - fn call(ptr: *anyopaque, frame: usize, candidates: []const culling.LODCullCandidate, config: culling.LODCullDispatch) bool { - return (@as(*LODCullingSystem, @ptrCast(@alignCast(ptr)))).dispatch(frame, candidates, config); - } - }.call, - .instanceBuffer = struct { - fn call(ptr: *anyopaque, frame: usize, fluid: bool, compact: bool) rhi.BufferHandle { - const self: *LODCullingSystem = @ptrCast(@alignCast(ptr)); - if (compact) return if (fluid) self.compact_water_instances[frame] else self.compact_terrain_instances[frame]; - return if (fluid) self.water_instances[frame] else self.terrain_instances[frame]; - } - }.call, - .indirectBuffer = struct { - fn call(ptr: *anyopaque, frame: usize, fluid: bool, compact: bool) rhi.BufferHandle { - const self: *LODCullingSystem = @ptrCast(@alignCast(ptr)); - if (compact) return if (fluid) self.compact_water_indirect[frame] else self.compact_terrain_indirect[frame]; - return if (fluid) self.water_indirect[frame] else self.terrain_indirect[frame]; - } - }.call, - .countBuffer = struct { - fn call(ptr: *anyopaque, frame: usize) rhi.BufferHandle { - const self: *LODCullingSystem = @ptrCast(@alignCast(ptr)); - return self.counters[frame]; - } - }.call, - .diagnostics = struct { - fn call(ptr: *anyopaque) culling.LODCullDiagnostics { - const self: *LODCullingSystem = @ptrCast(@alignCast(ptr)); - return self.diagnostics_state; - } - }.call, -}; - -pub fn create(allocator: std.mem.Allocator, ctx: *VulkanContext, capacity: usize) !rhi.ILODCullingSystem { - const system = try LODCullingSystem.init(allocator, ctx, capacity); - return .{ .ptr = system, .vtable = &VTABLE }; -} - -fn destroyNative(device: c.VkDevice, buffer: *Utils.VulkanBuffer) void { - if (buffer.mapped_ptr != null) c.vkUnmapMemory(device, buffer.memory); - if (buffer.buffer != null) c.vkDestroyBuffer(device, buffer.buffer, null); - if (buffer.memory != null) c.vkFreeMemory(device, buffer.memory, null); - buffer.* = .{}; -} - -fn loadShader(device: c.VkDevice, allocator: std.mem.Allocator) !c.VkShaderModule { - const bytes = try fs.cwd().readFileAlloc(SHADER_PATH, allocator, 16 * 1024 * 1024); - defer allocator.free(bytes); - if (bytes.len % 4 != 0) return error.InvalidShader; - var info = std.mem.zeroes(c.VkShaderModuleCreateInfo); - info.sType = c.VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; - info.codeSize = bytes.len; - info.pCode = @ptrCast(@alignCast(bytes.ptr)); - var module: c.VkShaderModule = null; - try Utils.checkVk(c.vkCreateShaderModule(device, &info, null, &module)); - return module; -} - -fn envFlag(name: [*:0]const u8) bool { - const value = std.c.getenv(name) orelse return false; - const bytes = std.mem.span(value); - return !(std.mem.eql(u8, bytes, "0") or std.ascii.eqlIgnoreCase(bytes, "false")); -} - -fn cpuExpectedCounts(input: []const culling.LODCullCandidate, config: culling.LODCullDispatch) [MAX_LOD_LEVELS * 4]u32 { - var counts = [_]u32{0} ** (MAX_LOD_LEVELS * 4); - for (input) |candidate| { - if (!cpuVisible(candidate, config)) continue; - const lod = @min(candidate.lod_and_padding[0], MAX_LOD_LEVELS - 1); - const base: usize = if (candidate.lod_and_padding[1] != 0) MAX_LOD_LEVELS * 2 else 0; - if (candidate.terrain_command.count != 0 and counts[base + lod] < config.max_commands_per_lod) counts[base + lod] += 1; - const water_index = base + MAX_LOD_LEVELS + lod; - if (candidate.water_command.count != 0 and counts[water_index] < config.max_commands_per_lod) counts[water_index] += 1; - } - return counts; -} - -fn fillExpectedIds(output: []u32, input: []const culling.LODCullCandidate, config: culling.LODCullDispatch, capacity: usize) void { - @memset(output, std.math.maxInt(u32)); - var counts = [_]u32{0} ** (MAX_LOD_LEVELS * 4); - for (input, 0..) |candidate, candidate_index| { - if (!cpuVisible(candidate, config)) continue; - const lod = @min(candidate.lod_and_padding[0], MAX_LOD_LEVELS - 1); - const base: usize = if (candidate.lod_and_padding[1] != 0) MAX_LOD_LEVELS * 2 else 0; - const streams = [_]struct { index: usize, count: u32 }{ - .{ .index = base + lod, .count = candidate.terrain_command.count }, - .{ .index = base + MAX_LOD_LEVELS + lod, .count = candidate.water_command.count }, - }; - for (streams) |stream| { - if (stream.count == 0 or counts[stream.index] >= config.max_commands_per_lod) continue; - output[stream.index * capacity + counts[stream.index]] = @intCast(candidate_index); - counts[stream.index] += 1; - } - } -} - -fn cpuVisible(candidate: culling.LODCullCandidate, config: culling.LODCullDispatch) bool { - for (config.planes) |plane| { - const x = if (plane[0] >= 0) candidate.max_point[0] else candidate.min_point[0]; - const y = if (plane[1] >= 0) candidate.max_point[1] else candidate.min_point[1]; - const z = if (plane[2] >= 0) candidate.max_point[2] else candidate.min_point[2]; - if (plane[0] * x + plane[1] * y + plane[2] * z + plane[3] < 0) return false; - } - if (config.max_distance_blocks <= 0) return true; - const dx: f32 = if (candidate.min_point[0] > 0) candidate.min_point[0] else if (candidate.max_point[0] < 0) candidate.max_point[0] else 0; - const dz: f32 = if (candidate.min_point[2] > 0) candidate.min_point[2] else if (candidate.max_point[2] < 0) candidate.max_point[2] else 0; - return dx * dx + dz * dz <= config.max_distance_blocks * config.max_distance_blocks; -} - -test "LOD culling CPU partitioning separates compact indexed streams" { - var candidate = std.mem.zeroes(culling.LODCullCandidate); - candidate.min_point = .{ -1, -1, -1, 0 }; - candidate.max_point = .{ 1, 1, 1, 0 }; - candidate.terrain_command.count = 3; - candidate.water_command.count = 6; - candidate.lod_and_padding[0] = 2; - const planes = [_][4]f32{.{ 0, 0, 0, 1 }} ** 6; - const counts = cpuExpectedCounts(&.{candidate}, .{ .planes = planes, .candidate_count = 1, .max_distance_blocks = 10, .max_commands_per_lod = 4 }); - try std.testing.expectEqual(@as(u32, 1), counts[2]); - try std.testing.expectEqual(@as(u32, 1), counts[MAX_LOD_LEVELS + 2]); - candidate.lod_and_padding[1] = 1; - const compact_counts = cpuExpectedCounts(&.{candidate}, .{ .planes = planes, .candidate_count = 1, .max_distance_blocks = 10, .max_commands_per_lod = 4 }); - try std.testing.expectEqual(@as(u32, 1), compact_counts[MAX_LOD_LEVELS * 2 + 2]); - try std.testing.expectEqual(@as(u32, 1), compact_counts[MAX_LOD_LEVELS * 3 + 2]); -} - -test "cleared fixed-capacity indirect entries are draw no-ops" { - // GPU culling submits a fixed-capacity MDI stream because RADV's indirect - // count path corrupts otherwise validated output. vkCmdFillBuffer writes - // this exact all-zero representation before each dispatch; Vulkan defines - // zero vertex/index counts as no-op draws. - const direct = std.mem.zeroes(rhi.DrawIndirectCommand); - try std.testing.expectEqual(@as(u32, 0), direct.vertexCount); - try std.testing.expectEqual(@as(u32, 0), direct.instanceCount); - const indexed = std.mem.zeroes(rhi.DrawIndexedIndirectCommand); - try std.testing.expectEqual(@as(u32, 0), indexed.indexCount); - try std.testing.expectEqual(@as(u32, 0), indexed.instanceCount); -} diff --git a/modules/engine-graphics/src/vulkan/pipeline_manager.zig b/modules/engine-graphics/src/vulkan/pipeline_manager.zig index eac472f4..883bdc8d 100644 --- a/modules/engine-graphics/src/vulkan/pipeline_manager.zig +++ b/modules/engine-graphics/src/vulkan/pipeline_manager.zig @@ -41,8 +41,6 @@ pub const PipelineManager = struct { rml_ui_pipeline: c.VkPipeline = null, rml_ui_tex_pipeline: c.VkPipeline = null, water_pipeline: c.VkPipeline = null, - compact_lod_terrain_pipeline: c.VkPipeline = null, - compact_lod_water_pipeline: c.VkPipeline = null, // Swapchain UI pipelines ui_swapchain_pipeline: c.VkPipeline = null, @@ -222,8 +220,6 @@ pub const PipelineManager = struct { if (self.rml_ui_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); if (self.rml_ui_tex_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); if (self.water_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); - if (self.compact_lod_terrain_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); - if (self.compact_lod_water_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); if (self.ui_swapchain_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); if (self.ui_swapchain_tex_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); if (self.rml_ui_swapchain_pipeline) |p| c.vkDestroyPipeline(vk_device, p, null); @@ -244,8 +240,6 @@ pub const PipelineManager = struct { self.rml_ui_pipeline = null; self.rml_ui_tex_pipeline = null; self.water_pipeline = null; - self.compact_lod_terrain_pipeline = null; - self.compact_lod_water_pipeline = null; self.ui_swapchain_pipeline = null; self.ui_swapchain_tex_pipeline = null; self.rml_ui_swapchain_pipeline = null; @@ -343,8 +337,6 @@ pub const PipelineManager = struct { // Terrain Pipeline try self.createTerrainPipeline(allocator, vk_device, hdr_render_pass, &viewport_state, &dynamic_state, &input_assembly, &rasterizer, &terrain_multisampling, &depth_stencil, &terrain_color_blending, sample_count, g_render_pass); - try pipeline_specialized.createCompactLODPipeline(self, allocator, vk_device, hdr_render_pass, &viewport_state, &dynamic_state, &input_assembly, &rasterizer, &terrain_multisampling, &depth_stencil, &terrain_color_blending, shader_registry.COMPACT_LOD_TERRAIN_VERT, shader_registry.COMPACT_LOD_TERRAIN_FRAG, false, &self.compact_lod_terrain_pipeline); - try pipeline_specialized.createCompactLODPipeline(self, allocator, vk_device, hdr_render_pass, &viewport_state, &dynamic_state, &input_assembly, &rasterizer, &terrain_multisampling, &depth_stencil, &terrain_color_blending, shader_registry.COMPACT_LOD_WATER_VERT, shader_registry.COMPACT_LOD_WATER_FRAG, true, &self.compact_lod_water_pipeline); // Sky Pipeline try self.createSkyPipeline(allocator, vk_device, hdr_render_pass, &viewport_state, &dynamic_state, &input_assembly, &rasterizer, &multisampling, &depth_stencil, &terrain_color_blending); diff --git a/modules/engine-graphics/src/vulkan/pipeline_specialized.zig b/modules/engine-graphics/src/vulkan/pipeline_specialized.zig index 5b68b989..2bef9d91 100644 --- a/modules/engine-graphics/src/vulkan/pipeline_specialized.zig +++ b/modules/engine-graphics/src/vulkan/pipeline_specialized.zig @@ -143,78 +143,6 @@ pub fn createTerrainPipeline( } } -/// A vertex-pulling pipeline intentionally has no Vk vertex bindings. Indexed -/// grid topology reaches the compact samples through descriptor binding 16. -pub fn createCompactLODPipeline( - self: anytype, - allocator: std.mem.Allocator, - vk_device: c.VkDevice, - render_pass: c.VkRenderPass, - viewport_state: *const c.VkPipelineViewportStateCreateInfo, - dynamic_state: *const c.VkPipelineDynamicStateCreateInfo, - input_assembly: *const c.VkPipelineInputAssemblyStateCreateInfo, - rasterizer: *const c.VkPipelineRasterizationStateCreateInfo, - multisampling: *const c.VkPipelineMultisampleStateCreateInfo, - depth_stencil: *const c.VkPipelineDepthStencilStateCreateInfo, - color_blending: *const c.VkPipelineColorBlendStateCreateInfo, - vert_path: []const u8, - frag_path: []const u8, - water: bool, - out_pipeline: *c.VkPipeline, -) !void { - const shaders = try loadShaderPair(allocator, vk_device, vert_path, frag_path); - defer c.vkDestroyShaderModule(vk_device, shaders.vert, null); - defer c.vkDestroyShaderModule(vk_device, shaders.frag, null); - var stages = [_]c.VkPipelineShaderStageCreateInfo{ - .{ .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = c.VK_SHADER_STAGE_VERTEX_BIT, .module = shaders.vert, .pName = "main" }, - .{ .sType = c.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, .stage = c.VK_SHADER_STAGE_FRAGMENT_BIT, .module = shaders.frag, .pName = "main" }, - }; - var vertex_input = std.mem.zeroes(c.VkPipelineVertexInputStateCreateInfo); - // Vertex-pulling intentionally has no bindings or attributes, but Vulkan - // still requires this create-info's structure type to be initialized. - // Leaving it zero makes the compact-only pipeline undefined on drivers - // that validate nested pipeline state (observed as an all-black compact - // capture on RADV). - vertex_input.sType = c.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; - - var water_depth_stencil = depth_stencil.*; - var compact_rasterizer = rasterizer.*; - // Edge skirts contain both orientations around the tile perimeter. They - // must remain visible regardless of winding after Vulkan Y inversion. - compact_rasterizer.cullMode = c.VK_CULL_MODE_NONE; - compact_rasterizer.polygonMode = c.VK_POLYGON_MODE_FILL; - var water_blend_attachment = color_blending.pAttachments[0]; - var water_color_blending = color_blending.*; - if (water) { - water_depth_stencil.depthWriteEnable = c.VK_FALSE; - water_blend_attachment.blendEnable = c.VK_TRUE; - water_blend_attachment.srcColorBlendFactor = c.VK_BLEND_FACTOR_SRC_ALPHA; - water_blend_attachment.dstColorBlendFactor = c.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; - water_blend_attachment.colorBlendOp = c.VK_BLEND_OP_ADD; - water_blend_attachment.srcAlphaBlendFactor = c.VK_BLEND_FACTOR_ONE; - water_blend_attachment.dstAlphaBlendFactor = c.VK_BLEND_FACTOR_ZERO; - water_blend_attachment.alphaBlendOp = c.VK_BLEND_OP_ADD; - water_color_blending.pAttachments = &water_blend_attachment; - } - - var info = std.mem.zeroes(c.VkGraphicsPipelineCreateInfo); - info.sType = c.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; - info.stageCount = stages.len; - info.pStages = &stages; - info.pVertexInputState = &vertex_input; - info.pInputAssemblyState = input_assembly; - info.pViewportState = viewport_state; - info.pRasterizationState = &compact_rasterizer; - info.pMultisampleState = multisampling; - info.pDepthStencilState = if (water) &water_depth_stencil else depth_stencil; - info.pColorBlendState = if (water) &water_color_blending else color_blending; - info.pDynamicState = dynamic_state; - info.layout = self.pipeline_layout; - info.renderPass = render_pass; - info.subpass = 0; - try Utils.checkVk(c.vkCreateGraphicsPipelines(vk_device, null, 1, &info, null, out_pipeline)); -} - pub fn createSwapchainUIPipelines( self: anytype, allocator: std.mem.Allocator, diff --git a/modules/engine-graphics/src/vulkan/rhi_context_factory.zig b/modules/engine-graphics/src/vulkan/rhi_context_factory.zig index a39e8f65..f21489ff 100644 --- a/modules/engine-graphics/src/vulkan/rhi_context_factory.zig +++ b/modules/engine-graphics/src/vulkan/rhi_context_factory.zig @@ -90,14 +90,7 @@ pub fn createRHI( ctx.draw.bound_water_reflection_texture = 0; ctx.draw.bound_scene_depth_texture = 0; ctx.draw.bound_lpv_texture = 0; - ctx.draw.current_mask_radius = 0; - ctx.draw.lod_mode = false; - ctx.draw.lod_descriptor_stream = .terrain_standard_direct; - ctx.draw.lod_descriptor_stream_valid = false; ctx.draw.pending_instance_buffer = 0; - ctx.draw.pending_lod_instance_buffer = 0; - ctx.draw.pending_lod_compact_sample_buffer = 0; - ctx.draw.pending_lod_compact_instance_buffer = 0; ctx.options.wireframe_enabled = false; ctx.options.textures_enabled = true; @@ -171,13 +164,9 @@ pub fn createRHI( ctx.ui.rml_vbos[i] = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; ctx.ui.rml_ibos[i] = .{ .buffer = null, .memory = null, .size = 0, .is_host_visible = false }; ctx.descriptors.descriptor_sets[i] = null; - ctx.descriptors.lod_descriptor_sets[i] = null; - for (&ctx.descriptors.lod_descriptor_snapshots[i]) |*snapshot| snapshot.* = null; ctx.ui.ui_tex_descriptor_sets[i] = null; ctx.ui.ui_tex_descriptor_next[i] = 0; ctx.draw.bound_instance_buffer[i] = 0; - ctx.draw.lod_snapshot_bindings[i] = .{}; - ctx.draw.lod_snapshot_seals[i] = .{}; for (0..ctx.ui.ui_tex_descriptor_pool[i].len) |j| { ctx.ui.ui_tex_descriptor_pool[i][j] = null; } @@ -207,7 +196,6 @@ pub fn createRHI( ctx.legacy.dummy_shadow_view = null; ctx.draw.current_model = Mat4.identity; ctx.draw.current_color = .{ 1.0, 1.0, 1.0, 1.0 }; - ctx.draw.current_mask_radius = 0; return rhi.RHI{ .ptr = ctx, diff --git a/modules/engine-graphics/src/vulkan/rhi_context_types.zig b/modules/engine-graphics/src/vulkan/rhi_context_types.zig index 1a768c0d..3b48bbb1 100644 --- a/modules/engine-graphics/src/vulkan/rhi_context_types.zig +++ b/modules/engine-graphics/src/vulkan/rhi_context_types.zig @@ -166,26 +166,12 @@ const DrawState = struct { descriptors_dirty: [MAX_FRAMES_IN_FLIGHT]bool, terrain_pipeline_bound: bool = false, descriptors_updated: bool = false, - lod_mode: bool = false, - lod_descriptor_stream: rhi.LODDescriptorStream = .terrain_standard_direct, - /// False when the selected immutable snapshot could not acquire the current - /// material revision. LOD setters and draws remain no-ops until another - /// stream is selected successfully. - lod_descriptor_stream_valid: bool = false, bound_instance_buffer: [MAX_FRAMES_IN_FLIGHT]rhi.BufferHandle = .{ 0, 0 }, - lod_snapshot_bindings: [MAX_FRAMES_IN_FLIGHT]@import("descriptor_manager.zig").LODDescriptorSnapshotBindings = .{ .{}, .{} }, - /// A snapshot is immutable from its first bind until the corresponding - /// frame slot fence has completed and prepareFrameState reopens it. - lod_snapshot_seals: [MAX_FRAMES_IN_FLIGHT]@import("descriptor_manager.zig").LODDescriptorSnapshotSeal = .{ .{}, .{} }, texture_state_revision: u64 = 0, pending_instance_buffer: rhi.BufferHandle = 0, - pending_lod_instance_buffer: rhi.BufferHandle = 0, - pending_lod_compact_sample_buffer: rhi.BufferHandle = 0, - pending_lod_compact_instance_buffer: rhi.BufferHandle = 0, current_view_proj: Mat4 = Mat4.identity, current_model: Mat4 = Mat4.identity, current_color: [4]f32 = .{ 1.0, 1.0, 1.0, 1.0 }, - current_mask_radius: f32 = 0.0, }; const RuntimeState = struct { diff --git a/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig b/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig index ebf89a34..5ef58ecc 100644 --- a/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig +++ b/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig @@ -1,30 +1,20 @@ const std = @import("std"); const c = @import("c").c; const rhi = @import("engine-rhi").rhi; -const rhi_types = @import("engine-rhi").rhi_types; const log = @import("engine-core").log; const Mat4 = @import("engine-math").Mat4; const pass_orchestration = @import("rhi_pass_orchestration.zig"); -fn lodDescriptorSet(ctx: anytype) c.VkDescriptorSet { - const frame = ctx.frames.current_frame; - return ctx.descriptors.lodDescriptorSet(frame, ctx.draw.lod_descriptor_stream); -} - const ModelUniforms = extern struct { model: Mat4, color: [4]f32, - mask_radius: f32, }; -/// Push constants for an indirect draw that must fetch instance data from the -/// bound instance buffer. Alpha is the shader's indirect sentinel so signed -/// LOD handoff masks remain valid direct-draw values. +/// Push constants for an indirect draw that fetches per-instance model data. pub fn indirectModelUniforms() ModelUniforms { return .{ .model = Mat4.identity, .color = .{ 1.0, 1.0, 1.0, -1.0 }, - .mask_radius = 0.0, }; } @@ -80,90 +70,8 @@ pub fn drawIndexed(ctx: anytype, vbo_handle: rhi.BufferHandle, ebo_handle: rhi.B } } -/// Indexed, storage-buffer vertex pulling for compact far LOD tiles. There is -/// deliberately no vertex buffer binding: the static index grid provides the -/// vertex id and binding 16 supplies the packed 16-byte samples. -pub fn drawCompactLOD(ctx: anytype, index_handle: rhi.BufferHandle, index_count: u32, params: rhi.CompactLODDraw) bool { - if (!ctx.frames.frame_in_progress or index_count == 0 or params.width < 2 or params.layer > 1) return false; - if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); - if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) return false; - const index = ctx.resources.buffers.get(index_handle) orelse return false; - const index_bytes = std.math.mul(u64, @as(u64, index_count), @sizeOf(u32)) catch return false; - if ((index.usage & c.VK_BUFFER_USAGE_INDEX_BUFFER_BIT) == 0 or index.size < index_bytes) return false; - const stream_index = @intFromEnum(ctx.draw.lod_descriptor_stream); - const sample_handle = ctx.draw.lod_snapshot_bindings[ctx.frames.current_frame].compact_samples[stream_index]; - const samples = ctx.resources.buffers.get(sample_handle) orelse return false; - if ((samples.usage & c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) == 0 or samples.size == 0 or samples.size % @sizeOf(rhi.CompactLODSampleWords) != 0) return false; - - // Every shader sample is a 16-byte uvec4. Validate the padded (one-sample - // apron on each side) address range before recording the draw. - const stride = std.math.add(u64, @as(u64, params.width), 2) catch return false; - const sample_count = std.math.mul(u64, stride, stride) catch return false; - const sample_end = std.math.add(u64, @as(u64, params.sample_offset), sample_count) catch return false; - const sample_bytes = std.math.mul(u64, sample_end, @sizeOf(rhi.CompactLODSampleWords)) catch return false; - if (sample_bytes > samples.size) return false; - const cb = ctx.frames.command_buffers[ctx.frames.current_frame]; - const pipeline = if (params.layer == 1) ctx.pipeline_manager.compact_lod_water_pipeline else ctx.pipeline_manager.compact_lod_terrain_pipeline; - if (pipeline == null) return false; - c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - ctx.draw.terrain_pipeline_bound = false; - const descriptor_set = lodDescriptorSet(ctx); - c.vkCmdBindDescriptorSets(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); - c.vkCmdPushConstants(cb, ctx.pipeline_manager.pipeline_layout, c.VK_SHADER_STAGE_VERTEX_BIT | c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(rhi.CompactLODDraw), ¶ms); - c.vkCmdBindIndexBuffer(cb, index.buffer, 0, c.VK_INDEX_TYPE_UINT32); - c.vkCmdDrawIndexed(cb, index_count, 1, 0, 0, 0); - ctx.runtime.draw_call_count += 1; - return true; -} - -/// Compatibility-named fixed-capacity indexed submission for compact far LOD. -/// `count_handle` and `count_offset` are deliberately unused: RADV's indexed -/// indirect-count path corrupts this vertex-pulling stream, while the culler -/// guarantees every unused command is zero-filled. `max_draw_count` is the -/// submitted capacity, and zero-count entries are Vulkan no-ops. -pub fn drawCompactLODIndirectCount(ctx: anytype, index_handle: rhi.BufferHandle, command_handle: rhi.BufferHandle, offset: usize, _count_handle: rhi.BufferHandle, _count_offset: usize, max_draw_count: u32) bool { - _ = _count_handle; - _ = _count_offset; - if (!ctx.frames.frame_in_progress or !ctx.vulkan_device.multi_draw_indirect or max_draw_count == 0 or !ctx.draw.lod_descriptor_stream_valid) return false; - if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); - if (!ctx.runtime.main_pass_active and !ctx.water_system.pass_active) return false; - const index = ctx.resources.buffers.get(index_handle) orelse return false; - const commands = ctx.resources.buffers.get(command_handle) orelse return false; - const fi = ctx.frames.current_frame; - const stream_index = @intFromEnum(ctx.draw.lod_descriptor_stream); - const snapshots = ctx.draw.lod_snapshot_bindings[fi]; - const samples = ctx.resources.buffers.get(snapshots.compact_samples[stream_index]) orelse return false; - const instances = ctx.resources.buffers.get(snapshots.compact_instances[stream_index]) orelse return false; - if ((index.usage & c.VK_BUFFER_USAGE_INDEX_BUFFER_BIT) == 0 or (commands.usage & c.VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT) == 0) return false; - if ((samples.usage & c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) == 0 or (instances.usage & c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT) == 0) return false; - if (index.size == 0 or index.size % @sizeOf(u32) != 0) return false; - const command_bytes = std.math.mul(usize, max_draw_count, @sizeOf(rhi_types.DrawIndexedIndirectCommand)) catch return false; - const command_end = std.math.add(usize, offset, command_bytes) catch return false; - if (command_end > commands.size) return false; - const cb = ctx.frames.command_buffers[fi]; - const pipeline = if (ctx.water_system.pass_active) ctx.pipeline_manager.compact_lod_water_pipeline else ctx.pipeline_manager.compact_lod_terrain_pipeline; - if (pipeline == null) return false; - c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); - ctx.draw.terrain_pipeline_bound = false; - const descriptor_set = lodDescriptorSet(ctx); - c.vkCmdBindDescriptorSets(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); - const sentinel = rhi.CompactLODDraw{ .model = Mat4.identity, .mask_radius = 0, .lod_fade = 0, .sample_offset = 0, .width = 0, .cell_size = 0, .layer = 2, .skirt_depth = 0 }; - c.vkCmdPushConstants(cb, ctx.pipeline_manager.pipeline_layout, c.VK_SHADER_STAGE_VERTEX_BIT | c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(rhi.CompactLODDraw), &sentinel); - c.vkCmdBindIndexBuffer(cb, index.buffer, 0, c.VK_INDEX_TYPE_UINT32); - // RADV consumes correct compute output when copied for validation, yet its - // indexed indirect-count execution can corrupt this vertex-pulling stream. - // The culler clears every unused command before dispatch, so submitting the - // fixed-capacity stream is equivalent: zero indexCount entries are no-ops. - // This remains entirely GPU driven; the count buffer is intentionally not - // mapped or read back on the CPU. - c.vkCmdDrawIndexedIndirect(cb, commands.buffer, @intCast(offset), max_draw_count, @sizeOf(rhi_types.DrawIndexedIndirectCommand)); - ctx.runtime.draw_call_count += 1; - return true; -} - pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, draw_count: u32, stride: u32) void { if (!ctx.frames.frame_in_progress) return; - if (ctx.draw.lod_mode and !ctx.draw.lod_descriptor_stream_valid) return; if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); @@ -218,10 +126,7 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. } } - const descriptor_set = if (ctx.draw.lod_mode) - lodDescriptorSet(ctx) - else - ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + const descriptor_set = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; c.vkCmdBindDescriptorSets(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); if (use_shadow) { @@ -282,7 +187,7 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. pub fn drawIndirectCount(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, _count_buffer: rhi.BufferHandle, _count_offset: usize, max_draw_count: u32, stride: u32) bool { _ = _count_buffer; _ = _count_offset; - if (!ctx.frames.frame_in_progress or !ctx.vulkan_device.multi_draw_indirect or max_draw_count == 0 or (ctx.draw.lod_mode and !ctx.draw.lod_descriptor_stream_valid)) return false; + if (!ctx.frames.frame_in_progress or !ctx.vulkan_device.multi_draw_indirect or max_draw_count == 0) return false; if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) return false; const vbo = ctx.resources.buffers.get(handle) orelse return false; @@ -307,15 +212,12 @@ pub fn drawIndirectCount(ctx: anytype, handle: rhi.BufferHandle, command_buffer: c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active and pipeline == ctx.pipeline_manager.terrain_pipeline; } - const descriptor_set = if (ctx.draw.lod_mode) lodDescriptorSet(ctx) else ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + const descriptor_set = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; c.vkCmdBindDescriptorSets(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); const uniforms = indirectModelUniforms(); c.vkCmdPushConstants(cb, ctx.pipeline_manager.pipeline_layout, c.VK_SHADER_STAGE_VERTEX_BIT | c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(ModelUniforms), &uniforms); const offsets = [_]c.VkDeviceSize{0}; c.vkCmdBindVertexBuffers(cb, 0, 1, &vbo.buffer, &offsets); - // See drawCompactLODIndirectCount. The compute pass zeroes the remainder - // of each fixed-capacity stream, making this regular MDI submission exactly - // match the compacted count without a CPU readback or a RADV count-path. c.vkCmdDrawIndirect(cb, commands.buffer, @intCast(offset), max_draw_count, stride); ctx.runtime.draw_call_count += 1; return true; @@ -323,7 +225,6 @@ pub fn drawIndirectCount(ctx: anytype, handle: rhi.BufferHandle, command_buffer: pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance_index: u32) void { if (!ctx.frames.frame_in_progress) return; - if (ctx.draw.lod_mode and !ctx.draw.lod_descriptor_stream_valid) return; if (!ctx.runtime.main_pass_active and !ctx.shadow_system.pass_active and !ctx.runtime.g_pass_active and !ctx.water_system.pass_active) pass_orchestration.beginMainPassInternal(ctx); @@ -362,10 +263,7 @@ pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance } } - const descriptor_set = if (ctx.draw.lod_mode) - lodDescriptorSet(ctx) - else - ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + const descriptor_set = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; c.vkCmdBindDescriptorSets(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); if (use_shadow) { @@ -380,7 +278,6 @@ pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance const uniforms = ModelUniforms{ .model = Mat4.identity, .color = .{ 1.0, 1.0, 1.0, 1.0 }, - .mask_radius = 0, }; c.vkCmdPushConstants(command_buffer, ctx.pipeline_manager.pipeline_layout, c.VK_SHADER_STAGE_VERTEX_BIT | c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(ModelUniforms), &uniforms); } @@ -396,7 +293,6 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. log.log.warn("drawOffset: no frame in progress", .{}); return; } - if (ctx.draw.lod_mode and !ctx.draw.lod_descriptor_stream_valid) return; if (ctx.post_process.pass_active) { const command_buffer = ctx.frames.command_buffers[ctx.frames.current_frame]; @@ -473,10 +369,7 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. ctx.draw.terrain_pipeline_bound = !ctx.water_system.pass_active and selected_pipeline == ctx.pipeline_manager.terrain_pipeline; } - const descriptor_set = if (ctx.draw.lod_mode) - lodDescriptorSet(ctx) - else - ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; + const descriptor_set = ctx.descriptors.descriptor_sets[ctx.frames.current_frame]; c.vkCmdBindDescriptorSets(command_buffer, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); } @@ -492,7 +385,6 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. const uniforms = ModelUniforms{ .model = ctx.draw.current_model, .color = ctx.draw.current_color, - .mask_radius = ctx.draw.current_mask_radius, }; c.vkCmdPushConstants(command_buffer, ctx.pipeline_manager.pipeline_layout, c.VK_SHADER_STAGE_VERTEX_BIT | c.VK_SHADER_STAGE_FRAGMENT_BIT, 0, @sizeOf(ModelUniforms), &uniforms); } diff --git a/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig b/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig index b2812ec7..e430d42f 100644 --- a/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig +++ b/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig @@ -241,10 +241,6 @@ pub fn prepareFrameState(ctx: anytype) void { ctx.shadow_system.pipeline_bound = false; ctx.draw.descriptors_updated = false; ctx.draw.bound_texture = 0; - // The frame slot fence was waited before prepareFrameState. Reopen only - // this slot's LOD snapshots; other in-flight slots remain immutable. - ctx.draw.lod_snapshot_seals[ctx.frames.current_frame] = .{}; - ctx.draw.lod_descriptor_stream_valid = false; const command_buffer = ctx.frames.getCurrentCommandBuffer(); @@ -423,55 +419,3 @@ pub fn refreshTextureDescriptors(ctx: anytype) void { ctx.draw.descriptors_updated = true; } - -/// Acquires a stream-local LOD texture/UBO descriptor snapshot. It copies the -/// current ordinary descriptor set exactly once, before the snapshot's first -/// bind. Later selections must observe the same material state until this -/// frame slot is reused after its fence completes. -pub fn prepareLODDescriptorSnapshot(ctx: anytype, stream: rhi.LODDescriptorStream) bool { - const frame = ctx.frames.current_frame; - const index = @intFromEnum(stream); - const source = ctx.descriptors.descriptor_sets[frame]; - const destination = ctx.descriptors.lod_descriptor_snapshots[frame][index]; - if (source == null or destination == null) { - log.log.err("LOD descriptor snapshot unavailable for frame={} stream={s}", .{ frame, @tagName(stream) }); - return false; - } - const should_copy = ctx.draw.lod_snapshot_seals[frame].acquire(stream, ctx.draw.texture_state_revision) catch |err| { - log.log.err("LOD descriptor snapshot rejected for frame={} stream={s} revision={}: {}", .{ frame, @tagName(stream), ctx.draw.texture_state_revision, err }); - return false; - }; - if (!should_copy) return true; - - // Per-frame UBO descriptors are installed when every snapshot is - // allocated; their mapped contents change, not the descriptors. Copy only - // material-controlled textures here. Compact/instance SSBO bindings are - // stream-owned and are intentionally not copied from the main set. - const copied_bindings = [_]u32{ - bindings.ALBEDO_TEXTURE, - bindings.NORMAL_TEXTURE, - bindings.ROUGHNESS_TEXTURE, - bindings.DISPLACEMENT_TEXTURE, - bindings.ENV_TEXTURE, - bindings.SSAO_TEXTURE, - bindings.LPV_TEXTURE, - bindings.LPV_TEXTURE_G, - bindings.LPV_TEXTURE_B, - bindings.WATER_REFLECTION_TEXTURE, - bindings.SCENE_DEPTH_TEXTURE, - }; - var copies: [copied_bindings.len + 2]c.VkCopyDescriptorSet = undefined; - var count: usize = 0; - for (copied_bindings) |binding| { - copies[count] = .{ .sType = c.VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET, .srcSet = source, .srcBinding = binding, .dstSet = destination, .dstBinding = binding, .descriptorCount = 1 }; - count += 1; - } - if (ctx.shadow_system.shadow_image_view != null and ctx.shadow_system.shadow_sampler != null) { - for ([_]u32{ bindings.SHADOW_COMPARE_TEXTURE, bindings.SHADOW_REGULAR_TEXTURE }) |binding| { - copies[count] = .{ .sType = c.VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET, .srcSet = source, .srcBinding = binding, .dstSet = destination, .dstBinding = binding, .descriptorCount = 1 }; - count += 1; - } - } - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 0, null, @intCast(count), &copies); - return true; -} diff --git a/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig b/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig index 11afe53d..802b0375 100644 --- a/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig +++ b/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig @@ -65,14 +65,7 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* ctx.draw.bound_roughness_texture = 0; ctx.draw.bound_displacement_texture = 0; ctx.draw.bound_env_texture = 0; - ctx.draw.current_mask_radius = 0; - ctx.draw.lod_mode = false; - ctx.draw.lod_descriptor_stream = .terrain_standard_direct; - ctx.draw.lod_descriptor_stream_valid = false; ctx.draw.pending_instance_buffer = 0; - ctx.draw.pending_lod_instance_buffer = 0; - ctx.draw.pending_lod_compact_sample_buffer = 0; - ctx.draw.pending_lod_compact_instance_buffer = 0; ctx.options.wireframe_enabled = false; ctx.options.textures_enabled = true; diff --git a/modules/engine-graphics/src/vulkan/rhi_pass_orchestration.zig b/modules/engine-graphics/src/vulkan/rhi_pass_orchestration.zig index de05bffc..c7d90417 100644 --- a/modules/engine-graphics/src/vulkan/rhi_pass_orchestration.zig +++ b/modules/engine-graphics/src/vulkan/rhi_pass_orchestration.zig @@ -305,7 +305,6 @@ pub fn beginMainPassInternal(ctx: anytype) void { c.vkCmdBeginRenderPass(command_buffer, &render_pass_info, c.VK_SUBPASS_CONTENTS_INLINE); ctx.runtime.main_pass_active = true; - ctx.draw.lod_mode = false; } const main_extent = ctx.dynamic_resolution.getRenderExtent(); diff --git a/modules/engine-graphics/src/vulkan/rhi_render_state.zig b/modules/engine-graphics/src/vulkan/rhi_render_state.zig index b37d7c1b..6a5e8ecf 100644 --- a/modules/engine-graphics/src/vulkan/rhi_render_state.zig +++ b/modules/engine-graphics/src/vulkan/rhi_render_state.zig @@ -4,7 +4,6 @@ const rhi = @import("engine-rhi").rhi; const Mat4 = @import("engine-math").Mat4; const Vec3 = @import("engine-math").Vec3; const bindings = @import("descriptor_bindings.zig"); -const frame_orchestration = @import("rhi_frame_orchestration.zig"); fn getenv(name: [:0]const u8) ?[]const u8 { const value = std.c.getenv(name) orelse return null; @@ -66,46 +65,14 @@ pub fn updateGlobalUniforms(ctx: anytype, uniforms: rhi.GlobalUniforms, frame_pa ctx.velocity.view_proj_prev = uniforms.view_proj; } -pub fn setModelMatrix(ctx: anytype, model: Mat4, color: Vec3, mask_radius: f32) void { +pub fn setModelMatrix(ctx: anytype, model: Mat4, color: Vec3) void { ctx.draw.current_model = model; ctx.draw.current_color = .{ color.x, color.y, color.z, 1.0 }; - ctx.draw.current_mask_radius = mask_radius; } pub fn setInstanceBuffer(ctx: anytype, handle: rhi.BufferHandle) void { if (!ctx.frames.frame_in_progress) return; ctx.draw.pending_instance_buffer = handle; - ctx.draw.lod_mode = false; - applyPendingDescriptorUpdates(ctx, ctx.frames.current_frame); -} - -pub fn setLODInstanceBuffer(ctx: anytype, handle: rhi.BufferHandle) void { - if (!ctx.frames.frame_in_progress or !ctx.draw.lod_descriptor_stream_valid) return; - ctx.draw.pending_lod_instance_buffer = handle; - ctx.draw.lod_mode = true; - applyPendingDescriptorUpdates(ctx, ctx.frames.current_frame); -} - -/// Select the immutable LOD descriptor snapshot before updating its bindings. -/// Callers own stream choice; render state never infers it from pass state. -pub fn setLODDescriptorStream(ctx: anytype, stream: rhi.LODDescriptorStream) void { - if (!ctx.frames.frame_in_progress) return; - ctx.draw.lod_descriptor_stream = stream; - ctx.draw.lod_mode = true; - ctx.draw.lod_descriptor_stream_valid = frame_orchestration.prepareLODDescriptorSnapshot(ctx, stream); -} - -pub fn setLODCompactSampleBuffer(ctx: anytype, handle: rhi.BufferHandle) void { - if (!ctx.frames.frame_in_progress or !ctx.draw.lod_descriptor_stream_valid) return; - ctx.draw.pending_lod_compact_sample_buffer = handle; - ctx.draw.lod_mode = true; - applyPendingDescriptorUpdates(ctx, ctx.frames.current_frame); -} - -pub fn setLODCompactInstanceBuffer(ctx: anytype, handle: rhi.BufferHandle) void { - if (!ctx.frames.frame_in_progress or !ctx.draw.lod_descriptor_stream_valid) return; - ctx.draw.pending_lod_compact_instance_buffer = handle; - ctx.draw.lod_mode = true; applyPendingDescriptorUpdates(ctx, ctx.frames.current_frame); } @@ -140,60 +107,4 @@ pub fn applyPendingDescriptorUpdates(ctx: anytype, frame_index: usize) void { ctx.draw.bound_instance_buffer[frame_index] = ctx.draw.pending_instance_buffer; } } - - if (!ctx.draw.lod_descriptor_stream_valid) return; - const stream_index = @intFromEnum(ctx.draw.lod_descriptor_stream); - const snapshot_bindings = &ctx.draw.lod_snapshot_bindings[frame_index]; - const bound_lod_instance = &snapshot_bindings.instance[stream_index]; - if (ctx.draw.pending_lod_instance_buffer != 0 and bound_lod_instance.* != ctx.draw.pending_lod_instance_buffer) { - const buf_opt = ctx.resources.buffers.get(ctx.draw.pending_lod_instance_buffer); - - if (buf_opt) |buf| { - var buffer_info = c.VkDescriptorBufferInfo{ - .buffer = buf.buffer, - .offset = 0, - .range = buf.size, - }; - - var write = std.mem.zeroes(c.VkWriteDescriptorSet); - write.sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = ctx.descriptors.lodDescriptorSet(frame_index, ctx.draw.lod_descriptor_stream); - write.dstBinding = bindings.INSTANCE_SSBO; - write.descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - write.descriptorCount = 1; - write.pBufferInfo = &buffer_info; - - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &write, 0, null); - bound_lod_instance.* = ctx.draw.pending_lod_instance_buffer; - } - } - - const bound_compact_sample = &snapshot_bindings.compact_samples[stream_index]; - if (ctx.draw.pending_lod_compact_sample_buffer != 0 and bound_compact_sample.* != ctx.draw.pending_lod_compact_sample_buffer) { - const buf = ctx.resources.buffers.get(ctx.draw.pending_lod_compact_sample_buffer) orelse return; - var info = c.VkDescriptorBufferInfo{ .buffer = buf.buffer, .offset = 0, .range = buf.size }; - var write = std.mem.zeroes(c.VkWriteDescriptorSet); - write.sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = ctx.descriptors.lodDescriptorSet(frame_index, ctx.draw.lod_descriptor_stream); - write.dstBinding = bindings.COMPACT_LOD_SAMPLES; - write.descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - write.descriptorCount = 1; - write.pBufferInfo = &info; - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &write, 0, null); - bound_compact_sample.* = ctx.draw.pending_lod_compact_sample_buffer; - } - const bound_compact_instance = &snapshot_bindings.compact_instances[stream_index]; - if (ctx.draw.pending_lod_compact_instance_buffer != 0 and bound_compact_instance.* != ctx.draw.pending_lod_compact_instance_buffer) { - const buf = ctx.resources.buffers.get(ctx.draw.pending_lod_compact_instance_buffer) orelse return; - var info = c.VkDescriptorBufferInfo{ .buffer = buf.buffer, .offset = 0, .range = buf.size }; - var write = std.mem.zeroes(c.VkWriteDescriptorSet); - write.sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = ctx.descriptors.lodDescriptorSet(frame_index, ctx.draw.lod_descriptor_stream); - write.dstBinding = bindings.COMPACT_LOD_INSTANCES; - write.descriptorType = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; - write.descriptorCount = 1; - write.pBufferInfo = &info; - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &write, 0, null); - bound_compact_instance.* = ctx.draw.pending_lod_compact_instance_buffer; - } } diff --git a/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig b/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig index aad3bc4d..b2ab3393 100644 --- a/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig +++ b/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig @@ -403,13 +403,6 @@ pub fn createSSAOResources(ctx: anytype) !void { main_ssao_write.descriptorCount = 1; main_ssao_write.pImageInfo = &main_ssao_info; c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &main_ssao_write, 0, null); - - main_ssao_write.dstSet = ctx.descriptors.lod_descriptor_sets[i]; - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &main_ssao_write, 0, null); - for (ctx.descriptors.lod_descriptor_snapshots[i]) |snapshot| { - main_ssao_write.dstSet = snapshot; - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, 1, &main_ssao_write, 0, null); - } } const ssao_images = [_]c.VkImage{ ctx.ssao_system.image, ctx.ssao_system.blur_image }; diff --git a/modules/engine-graphics/src/vulkan/rhi_state_control.zig b/modules/engine-graphics/src/vulkan/rhi_state_control.zig index 484ca230..91dbf573 100644 --- a/modules/engine-graphics/src/vulkan/rhi_state_control.zig +++ b/modules/engine-graphics/src/vulkan/rhi_state_control.zig @@ -61,26 +61,6 @@ pub fn supportsIndirectCount(ctx: anytype) bool { return ctx.vulkan_device.draw_indirect_count; } -/// This is intentionally a single backend capability rather than a collection -/// of Vulkan feature bits. It certifies the descriptor-snapshot contract used -/// by compact terrain/water indirect streams. -pub fn supportsCompactLODGpuCulling(ctx: anytype) bool { - return ctx.vulkan_device.draw_indirect_first_instance and ctx.vulkan_device.multi_draw_indirect; -} - -test "compact LOD GPU culling requires first-instance but not indirect-count support" { - const Context = struct { - vulkan_device: struct { - draw_indirect_first_instance: bool, - draw_indirect_count: bool, - multi_draw_indirect: bool, - }, - }; - try std.testing.expect(supportsCompactLODGpuCulling(Context{ .vulkan_device = .{ .draw_indirect_first_instance = true, .draw_indirect_count = false, .multi_draw_indirect = true } })); - try std.testing.expect(!supportsCompactLODGpuCulling(Context{ .vulkan_device = .{ .draw_indirect_first_instance = false, .draw_indirect_count = true, .multi_draw_indirect = true } })); - try std.testing.expect(!supportsCompactLODGpuCulling(Context{ .vulkan_device = .{ .draw_indirect_first_instance = true, .draw_indirect_count = true, .multi_draw_indirect = false } })); -} - pub fn recover(ctx: anytype) !void { if (!ctx.runtime.gpu_fault_detected) return; diff --git a/modules/engine-graphics/src/vulkan/rhi_timing.zig b/modules/engine-graphics/src/vulkan/rhi_timing.zig index faca07e1..172538a0 100644 --- a/modules/engine-graphics/src/vulkan/rhi_timing.zig +++ b/modules/engine-graphics/src/vulkan/rhi_timing.zig @@ -12,16 +12,11 @@ const GpuPass = enum { lpv_compute, sky, opaque_pass, - lod_terrain, - lod_water, - lod_compact_terrain, - lod_compact_water, - lod_culling_compute, bloom, fxaa, post_process, - pub const COUNT = 17; + pub const COUNT = 12; }; pub const PASS_COUNT = GpuPass.COUNT; @@ -37,11 +32,6 @@ fn mapPassName(name: []const u8) ?GpuPass { if (std.mem.eql(u8, name, "LPVPass")) return .lpv_compute; if (std.mem.eql(u8, name, "SkyPass")) return .sky; if (std.mem.eql(u8, name, "OpaquePass")) return .opaque_pass; - if (std.mem.eql(u8, name, "LODTerrainPass")) return .lod_terrain; - if (std.mem.eql(u8, name, "LODWaterPass")) return .lod_water; - if (std.mem.eql(u8, name, "LODCompactTerrainPass")) return .lod_compact_terrain; - if (std.mem.eql(u8, name, "LODCompactWaterPass")) return .lod_compact_water; - if (std.mem.eql(u8, name, "LODGpuCullingComputeBarrier")) return .lod_culling_compute; if (std.mem.eql(u8, name, "BloomPass")) return .bloom; if (std.mem.eql(u8, name, "FXAAPass")) return .fxaa; if (std.mem.eql(u8, name, "PostProcessPass")) return .post_process; @@ -111,11 +101,6 @@ pub fn processTimingResults(ctx: anytype) void { const lpv = readPassMs(ctx, frame, .lpv_compute, period) orelse return; const sky = readPassMs(ctx, frame, .sky, period) orelse return; const opaque_ms = readPassMs(ctx, frame, .opaque_pass, period) orelse return; - const lod_terrain = readPassMs(ctx, frame, .lod_terrain, period) orelse return; - const lod_water = readPassMs(ctx, frame, .lod_water, period) orelse return; - const lod_compact_terrain = readPassMs(ctx, frame, .lod_compact_terrain, period) orelse return; - const lod_compact_water = readPassMs(ctx, frame, .lod_compact_water, period) orelse return; - const lod_culling_compute = readPassMs(ctx, frame, .lod_culling_compute, period) orelse return; const bloom = readPassMs(ctx, frame, .bloom, period) orelse return; const fxaa = readPassMs(ctx, frame, .fxaa, period) orelse return; const post_process = readPassMs(ctx, frame, .post_process, period) orelse return; @@ -129,11 +114,6 @@ pub fn processTimingResults(ctx: anytype) void { ctx.timing.timing_results.lpv_pass_ms = lpv; ctx.timing.timing_results.sky_pass_ms = sky; ctx.timing.timing_results.opaque_pass_ms = opaque_ms; - ctx.timing.timing_results.lod_terrain_pass_ms = lod_terrain; - ctx.timing.timing_results.lod_water_pass_ms = lod_water; - ctx.timing.timing_results.lod_compact_terrain_pass_ms = lod_compact_terrain; - ctx.timing.timing_results.lod_compact_water_pass_ms = lod_compact_water; - ctx.timing.timing_results.lod_culling_compute_ms = lod_culling_compute; ctx.timing.timing_results.bloom_pass_ms = bloom; ctx.timing.timing_results.fxaa_pass_ms = fxaa; ctx.timing.timing_results.post_process_pass_ms = post_process; @@ -150,16 +130,7 @@ pub fn processTimingResults(ctx: anytype) void { ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.ssao_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.lpv_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.main_pass_ms; - ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.lod_culling_compute_ms; - // LOD terrain/water timings are nested in scene passes and must not be - // double-counted. The culling compute pass above is independent. ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.bloom_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.fxaa_pass_ms; ctx.timing.timing_results.total_gpu_ms += ctx.timing.timing_results.post_process_pass_ms; } - -test "GPU timing maps LOD pass names" { - try std.testing.expectEqual(GpuPass.lod_terrain, mapPassName("LODTerrainPass").?); - try std.testing.expectEqual(GpuPass.lod_water, mapPassName("LODWaterPass").?); - try std.testing.expectEqual(GpuPass.lod_culling_compute, mapPassName("LODGpuCullingComputeBarrier").?); -} diff --git a/modules/engine-graphics/src/vulkan/shader_registry.zig b/modules/engine-graphics/src/vulkan/shader_registry.zig index 66a8fba8..ec64c495 100644 --- a/modules/engine-graphics/src/vulkan/shader_registry.zig +++ b/modules/engine-graphics/src/vulkan/shader_registry.zig @@ -43,8 +43,3 @@ pub const CULLING_COMP = "assets/shaders/vulkan/culling.comp.spv"; pub const WATER_VERT = "assets/shaders/vulkan/water.vert.spv"; pub const WATER_FRAG = "assets/shaders/vulkan/water.frag.spv"; - -pub const COMPACT_LOD_TERRAIN_VERT = "assets/shaders/vulkan/lod_compact_terrain.vert.spv"; -pub const COMPACT_LOD_TERRAIN_FRAG = "assets/shaders/vulkan/lod_compact_terrain.frag.spv"; -pub const COMPACT_LOD_WATER_VERT = "assets/shaders/vulkan/lod_compact_water.vert.spv"; -pub const COMPACT_LOD_WATER_FRAG = "assets/shaders/vulkan/lod_compact_water.frag.spv"; diff --git a/modules/engine-graphics/src/vulkan/vulkan_frame_tests.zig b/modules/engine-graphics/src/vulkan/vulkan_frame_tests.zig index 60efa383..0a3f1a6b 100644 --- a/modules/engine-graphics/src/vulkan/vulkan_frame_tests.zig +++ b/modules/engine-graphics/src/vulkan/vulkan_frame_tests.zig @@ -60,7 +60,6 @@ const MockDrawState = struct { current_lpv_texture_g: u32 = 0, current_lpv_texture_b: u32 = 0, descriptors_dirty: [rhi.MAX_FRAMES_IN_FLIGHT]bool = .{ false, false }, - lod_mode: bool = false, dummy_texture: u32 = 1, dummy_texture_3d: u32 = 2, }; diff --git a/modules/engine-input/src/interfaces.zig b/modules/engine-input/src/interfaces.zig index 9e32a7ec..dd085be3 100644 --- a/modules/engine-input/src/interfaces.zig +++ b/modules/engine-input/src/interfaces.zig @@ -56,7 +56,6 @@ pub const GameAction = enum(u8) { ui_back, toggle_shadow_debug_vis, toggle_timing_overlay, - toggle_lod_render, toggle_gpass_render, toggle_ssao, toggle_fog, diff --git a/modules/engine-rhi/src/culling.zig b/modules/engine-rhi/src/culling.zig index 46a2daed..c595ab4d 100644 --- a/modules/engine-rhi/src/culling.zig +++ b/modules/engine-rhi/src/culling.zig @@ -1,5 +1,4 @@ const Mat4 = @import("engine-math").Mat4; -const rhi_types = @import("rhi_types.zig"); pub const ChunkCullData = extern struct { min_point: [4]f32, @@ -56,109 +55,3 @@ pub const ICullingSystem = struct { self.vtable.dispatch(self.ptr, config); } }; - -/// A command source has two vec4 lanes in the candidate SSBO. Standard draws -/// use `count`, `instance_count`, `first`, and `first_instance`; compact draws -/// additionally use `vertex_offset` and are emitted as indexed commands. -pub const LODCullCommand = extern struct { - count: u32, - instance_count: u32, - first: u32, - vertex_offset: i32 = 0, - first_instance: u32 = 0, - _padding: [3]u32 = .{ 0, 0, 0 }, -}; - -/// One CPU-approved LOD region. The layout is a fixed std430 ABI shared with -/// lod_culling.comp. `lod_and_flags[1]` selects indexed compact output. -pub const LODCullCandidate = extern struct { - min_point: [4]f32, - max_point: [4]f32, - model: Mat4, - instance_params: [4]f32, - /// x=sample offset, y=grid width; only meaningful for compact candidates. - compact_words: [4]u32, - /// x=cell size, y=skirt depth; mask/fade remain in instance_params. - compact_metrics: [4]f32, - terrain_command: LODCullCommand, - water_command: LODCullCommand, - lod_and_padding: [4]u32, -}; - -pub const LODCullDispatch = extern struct { - planes: [6][4]f32, - candidate_count: u32, - max_distance_blocks: f32, - max_commands_per_lod: u32, - _padding: u32 = 0, -}; - -pub const LODCullDiagnostics = extern struct { - overflow_count: u32 = 0, - validation_mismatch_count: u32 = 0, - /// Monotonic opt-in validation work generation. A generation is assigned - /// when its device readback has been queued, not when it is assumed done. - validation_generation: u64 = 0, - /// Last validation generation observed after its delayed frame-slot fence. - validation_completed_generation: u64 = 0, - /// Number of delayed validation readbacks actually completed. - validation_completed_count: u64 = 0, -}; - -pub const ILODCullingSystem = struct { - ptr: *anyopaque, - vtable: *const VTable, - - pub const VTable = struct { - deinit: *const fn (ptr: *anyopaque) void, - dispatch: *const fn (ptr: *anyopaque, frame_index: usize, candidates: []const LODCullCandidate, config: LODCullDispatch) bool, - instanceBuffer: *const fn (ptr: *anyopaque, frame_index: usize, fluid: bool, compact: bool) rhi_types.BufferHandle, - indirectBuffer: *const fn (ptr: *anyopaque, frame_index: usize, fluid: bool, compact: bool) rhi_types.BufferHandle, - countBuffer: *const fn (ptr: *anyopaque, frame_index: usize) rhi_types.BufferHandle, - diagnostics: *const fn (ptr: *anyopaque) LODCullDiagnostics, - }; - - pub fn deinit(self: ILODCullingSystem) void { - self.vtable.deinit(self.ptr); - } - - /// Records same-frame compute culling before graphics render passes begin. - /// Returns false when inputs exceed the fixed GPU capacity. - pub fn dispatch(self: ILODCullingSystem, frame_index: usize, candidates: []const LODCullCandidate, config: LODCullDispatch) bool { - return self.vtable.dispatch(self.ptr, frame_index, candidates, config); - } - - pub fn instanceBuffer(self: ILODCullingSystem, frame_index: usize, fluid: bool, compact: bool) rhi_types.BufferHandle { - return self.vtable.instanceBuffer(self.ptr, frame_index, fluid, compact); - } - - pub fn indirectBuffer(self: ILODCullingSystem, frame_index: usize, fluid: bool, compact: bool) rhi_types.BufferHandle { - return self.vtable.indirectBuffer(self.ptr, frame_index, fluid, compact); - } - pub fn countBuffer(self: ILODCullingSystem, frame_index: usize) rhi_types.BufferHandle { - return self.vtable.countBuffer(self.ptr, frame_index); - } - pub fn diagnostics(self: ILODCullingSystem) LODCullDiagnostics { - return self.vtable.diagnostics(self.ptr); - } -}; - -test "LOD culling candidate ABI is std430 aligned" { - try @import("std").testing.expectEqual(@as(usize, 224), @sizeOf(LODCullCandidate)); - try @import("std").testing.expectEqual(@as(usize, 32), @offsetOf(LODCullCandidate, "model")); - try @import("std").testing.expectEqual(@as(usize, 96), @offsetOf(LODCullCandidate, "instance_params")); - try @import("std").testing.expectEqual(@as(usize, 144), @offsetOf(LODCullCandidate, "terrain_command")); - try @import("std").testing.expectEqual(@as(usize, 208), @offsetOf(LODCullCandidate, "lod_and_padding")); - try @import("std").testing.expectEqual(@as(usize, 32), @sizeOf(LODCullCommand)); -} - -test "LOD culling diagnostics expose delayed validation completion" { - const diagnostics = LODCullDiagnostics{ - .validation_generation = 9, - .validation_completed_generation = 8, - .validation_completed_count = 8, - }; - try @import("std").testing.expectEqual(@as(u64, 9), diagnostics.validation_generation); - try @import("std").testing.expectEqual(@as(u64, 8), diagnostics.validation_completed_generation); - try @import("std").testing.expectEqual(@as(u64, 8), diagnostics.validation_completed_count); -} diff --git a/modules/engine-rhi/src/render_settings.zig b/modules/engine-rhi/src/render_settings.zig index 62052671..ad889d04 100644 --- a/modules/engine-rhi/src/render_settings.zig +++ b/modules/engine-rhi/src/render_settings.zig @@ -1,159 +1,6 @@ const rhi_pkg = @import("root.zig"); const RHI = rhi_pkg.RHI; const IRenderSettings = @import("engine-core").interfaces.IRenderSettings; -const LODLevel = @import("engine-core").LODLevel; - -pub const RenderDistancePreset = enum(u32) { - low = 0, - medium = 1, - high = 2, - ultra = 3, - extreme = 4, - - pub const count = 5; - - pub fn label(self: RenderDistancePreset) []const u8 { - return switch (self) { - .low => "LOW", - .medium => "MEDIUM", - .high => "HIGH", - .ultra => "ULTRA", - .extreme => "EXTREME", - }; - } -}; - -pub const LODMeshPath = enum(u8) { - /// Stable heightfield mesh path used by current defaults. - heightfield, - /// Rich column/span mesh path for vertical detail where source data provides spans. - column_spans, - /// Quadric error metric decimation path for experimentation while it stabilizes. - qem, -}; - -pub const RenderDistancePresetConfig = struct { - lod_radii: [LODLevel.count]i32, - horizon_radius: i32, - lod_store_size_cap_mb: u32, - horizontal_detail: [LODLevel.count]u32, - sample_density: [LODLevel.count]f32, - vertical_span_budget: u8, - mesh_path: LODMeshPath, - fog_start_percent: [LODLevel.count]f32, - active_lod_count: u32, - qem_targets: [LODLevel.count]u32, - memory_budget_mb: u32, - max_uploads_per_frame: u32, - skip_cutout_lod2: bool, - skip_lighting_lod3: bool, - show_warning: bool, -}; - -pub const RENDER_DISTANCE_PRESETS = [_]RenderDistancePresetConfig{ - .{ - .lod_radii = .{ 6, 64, 156, 256, 256 }, - .horizon_radius = 256, - .lod_store_size_cap_mb = 512, - .horizontal_detail = .{ 33, 33, 33, 65, 65 }, - .sample_density = .{ 1.0, 1.0, 1.0, 0.5, 1.0 }, - .vertical_span_budget = 2, - .mesh_path = .column_spans, - .fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3, 0.22 }, - .active_lod_count = LODLevel.count, - .qem_targets = .{ 0, 1200, 300, 48, 24 }, - .memory_budget_mb = 128, - .max_uploads_per_frame = 4, - .skip_cutout_lod2 = false, - .skip_lighting_lod3 = false, - .show_warning = false, - }, - .{ - .lod_radii = .{ 10, 64, 156, 375, 512 }, - .horizon_radius = 512, - .lod_store_size_cap_mb = 1024, - // The 512-chunk fallback is intentionally coarser than LOD3. A 33x33 - // horizon grid cuts cold-start sampling by roughly 4x while retaining - // 16-block cells at distances where finer levels replace it. - .horizontal_detail = .{ 33, 49, 49, 65, 33 }, - .sample_density = .{ 1.0, 1.0, 1.0, 0.5, 0.5 }, - .vertical_span_budget = 2, - .mesh_path = .column_spans, - .fog_start_percent = .{ 0.5, 0.5, 0.4, 0.4, 0.24 }, - .active_lod_count = LODLevel.count, - .qem_targets = .{ 0, 2000, 800, 200, 64 }, - .memory_budget_mb = 256, - .max_uploads_per_frame = 8, - .skip_cutout_lod2 = false, - .skip_lighting_lod3 = false, - .show_warning = false, - }, - .{ - .lod_radii = .{ 12, 64, 156, 375, 512 }, - .horizon_radius = 512, - .lod_store_size_cap_mb = 1536, - .horizontal_detail = .{ 33, 65, 65, 97, 97 }, - .sample_density = .{ 1.0, 1.0, 1.0, 0.5, 0.5 }, - .vertical_span_budget = 3, - .mesh_path = .column_spans, - .fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3, 0.22 }, - .active_lod_count = LODLevel.count, - .qem_targets = .{ 0, 2000, 800, 200, 64 }, - .memory_budget_mb = 384, - .max_uploads_per_frame = 8, - .skip_cutout_lod2 = true, - .skip_lighting_lod3 = false, - .show_warning = false, - }, - .{ - .lod_radii = .{ 14, 64, 156, 375, 512 }, - .horizon_radius = 512, - .lod_store_size_cap_mb = 3072, - .horizontal_detail = .{ 33, 65, 65, 129, 129 }, - .sample_density = .{ 1.0, 1.0, 1.0, 0.5, 0.5 }, - .vertical_span_budget = 4, - .mesh_path = .column_spans, - .fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3, 0.2 }, - .active_lod_count = LODLevel.count, - .qem_targets = .{ 0, 2000, 800, 200, 64 }, - .memory_budget_mb = 512, - .max_uploads_per_frame = 12, - .skip_cutout_lod2 = true, - .skip_lighting_lod3 = true, - .show_warning = false, - }, - .{ - .lod_radii = .{ 16, 64, 156, 375, 512 }, - .horizon_radius = 512, - .lod_store_size_cap_mb = 4096, - .horizontal_detail = .{ 33, 65, 65, 129, 129 }, - .sample_density = .{ 1.0, 1.0, 1.0, 0.5, 0.5 }, - .vertical_span_budget = 4, - .mesh_path = .column_spans, - .fog_start_percent = .{ 0.5, 0.5, 0.4, 0.3, 0.18 }, - .active_lod_count = LODLevel.count, - .qem_targets = .{ 0, 2000, 800, 200, 64 }, - .memory_budget_mb = 1024, - .max_uploads_per_frame = 16, - .skip_cutout_lod2 = true, - .skip_lighting_lod3 = true, - .show_warning = true, - }, -}; - -pub fn getPresetConfig(preset: RenderDistancePreset) RenderDistancePresetConfig { - return RENDER_DISTANCE_PRESETS[@intFromEnum(preset)]; -} - -test "512 chunk presets use a budget-feasible coarse fallback grid" { - const std = @import("std"); - for (RENDER_DISTANCE_PRESETS[1..]) |preset| { - try std.testing.expectEqual(@as(i32, 512), preset.horizon_radius); - try std.testing.expectEqual(@as(f32, 0.5), preset.sample_density[@intFromEnum(LODLevel.lod4)]); - } - try std.testing.expectEqual(@as(f32, 1.0), RENDER_DISTANCE_PRESETS[0].sample_density[@intFromEnum(LODLevel.lod4)]); -} - pub const RenderSettingsAdapter = struct { rhi: *RHI, diff --git a/modules/engine-rhi/src/rhi.zig b/modules/engine-rhi/src/rhi.zig index 12102dd6..299e126a 100644 --- a/modules/engine-rhi/src/rhi.zig +++ b/modules/engine-rhi/src/rhi.zig @@ -86,9 +86,6 @@ pub const DrawMode = rhi_types.DrawMode; pub const ShaderStageFlags = rhi_types.ShaderStageFlags; pub const DrawIndirectCommand = rhi_types.DrawIndirectCommand; pub const InstanceData = rhi_types.InstanceData; -pub const CompactLODDraw = rhi_types.CompactLODDraw; -pub const LODDescriptorStream = rhi_types.LODDescriptorStream; -pub const CompactLODSampleWords = rhi_types.CompactLODSampleWords; pub const SkyParams = rhi_types.SkyParams; pub const SkyPushConstants = rhi_types.SkyPushConstants; pub const FrameRenderParams = rhi_types.FrameRenderParams; @@ -102,7 +99,6 @@ pub const UiVertex = rhi_types.UiVertex; pub const UiScissor = rhi_types.UiScissor; pub const GpuTimingResults = rhi_types.GpuTimingResults; pub const ICullingSystem = culling.ICullingSystem; -pub const ILODCullingSystem = culling.ILODCullingSystem; pub const RenderResolution = struct { width: u32, @@ -433,20 +429,6 @@ pub const RenderContext = struct { pub fn drawIndexed(self: RenderContext, vbo: BufferHandle, ebo: BufferHandle, count: u32) void { self.encoder.drawIndexed(vbo, ebo, count); } - /// Draws a compact far-LOD grid through a storage-buffer vertex-pulling - /// pipeline. The index buffer is reusable and no expanded Vertex array is - /// required for the tile. - pub fn drawCompactLOD(self: RenderContext, index_buffer: BufferHandle, index_count: u32, params: CompactLODDraw) bool { - return self.encoder.drawCompactLOD(index_buffer, index_count, params); - } - /// Draws GPU-compacted indexed compact-LOD commands. Backends may consume - /// `count_buffer`, or submit `max_draw_count` from a guaranteed zero-filled - /// command stream when an indirect-count driver path is unsafe. - pub fn drawCompactLODIndirectCount(self: RenderContext, index_buffer: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32) bool { - // The Vulkan backend currently treats count_buffer/count_offset as - // compatibility parameters and submits a zero-filled fixed capacity. - return self.encoder.drawCompactLODIndirectCount(index_buffer, command_buffer, offset, count_buffer, count_offset, max_draw_count); - } /// Issues indirect draw commands from a GPU buffer. /// The indirect buffer must contain backend-compatible command records and remain valid through submission. Must be called from the render thread that owns the backend context. pub fn drawIndirect(self: RenderContext, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, draw_count: u32, stride: u32) void { @@ -456,8 +438,6 @@ pub const RenderContext = struct { /// `count_buffer`, or use a zero-filled fixed-capacity submission bounded by /// `max_draw_count`. Returns false when neither safe path is supported. pub fn drawIndirectCount(self: RenderContext, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32, stride: u32) bool { - // See drawCompactLODIndirectCount: the backend may intentionally use - // fixed-capacity MDI while preserving this cross-backend ABI. return self.encoder.drawIndirectCount(handle, command_buffer, offset, count_buffer, count_offset, max_draw_count, stride); } /// Issues an instanced draw using currently bound per-instance state. @@ -475,30 +455,14 @@ pub const RenderContext = struct { /// Sets model matrix on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. - pub fn setModelMatrix(self: RenderContext, model: Mat4, color: Vec3, mask_radius: f32) void { - self.state.setModelMatrix(model, color, mask_radius); + pub fn setModelMatrix(self: RenderContext, model: Mat4, color: Vec3) void { + self.state.setModelMatrix(model, color); } /// Sets instance buffer on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. pub fn setInstanceBuffer(self: RenderContext, handle: BufferHandle) void { self.state.setInstanceBuffer(handle); } - /// Binds the LOD instance buffer used by distant-terrain draw calls. - /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. - pub fn setLODInstanceBuffer(self: RenderContext, handle: BufferHandle) void { - self.state.setLODInstanceBuffer(handle); - } - /// Selects the immutable LOD descriptor stream before binding its buffers. - pub fn setLODDescriptorStream(self: RenderContext, stream: LODDescriptorStream) void { - self.state.setLODDescriptorStream(stream); - } - /// Binds the shared compact LOD sample pool for subsequent vertex-pulling draws. - pub fn setLODCompactSampleBuffer(self: RenderContext, handle: BufferHandle) void { - self.state.setLODCompactSampleBuffer(handle); - } - pub fn setLODCompactInstanceBuffer(self: RenderContext, handle: BufferHandle) void { - self.state.setLODCompactInstanceBuffer(handle); - } /// Sets terrain pipeline bound on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. pub fn setTerrainPipelineBound(self: RenderContext, bound: bool) void { @@ -845,8 +809,6 @@ pub const IGraphicsCommandEncoder = struct { draw: *const fn (ptr: *anyopaque, handle: BufferHandle, count: u32, mode: DrawMode) void, drawOffset: *const fn (ptr: *anyopaque, handle: BufferHandle, count: u32, mode: DrawMode, offset: usize) void, drawIndexed: *const fn (ptr: *anyopaque, vbo: BufferHandle, ebo: BufferHandle, count: u32) void, - drawCompactLOD: *const fn (ptr: *anyopaque, index_buffer: BufferHandle, index_count: u32, params: CompactLODDraw) bool, - drawCompactLODIndirectCount: *const fn (ptr: *anyopaque, index_buffer: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32) bool, drawIndirect: *const fn (ptr: *anyopaque, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, draw_count: u32, stride: u32) void, drawIndirectCount: *const fn (ptr: *anyopaque, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32, stride: u32) bool, drawInstance: *const fn (ptr: *anyopaque, handle: BufferHandle, count: u32, instance_index: u32) void, @@ -883,12 +845,6 @@ pub const IGraphicsCommandEncoder = struct { pub fn drawIndexed(self: IGraphicsCommandEncoder, vbo: BufferHandle, ebo: BufferHandle, count: u32) void { self.vtable.drawIndexed(self.ptr, vbo, ebo, count); } - pub fn drawCompactLOD(self: IGraphicsCommandEncoder, index_buffer: BufferHandle, index_count: u32, params: CompactLODDraw) bool { - return self.vtable.drawCompactLOD(self.ptr, index_buffer, index_count, params); - } - pub fn drawCompactLODIndirectCount(self: IGraphicsCommandEncoder, index_buffer: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32) bool { - return self.vtable.drawCompactLODIndirectCount(self.ptr, index_buffer, command_buffer, offset, count_buffer, count_offset, max_draw_count); - } /// Issues indirect draw commands from a GPU buffer. /// The indirect buffer must contain backend-compatible command records and remain valid through submission. Must be called from the render thread that owns the backend context. pub fn drawIndirect(self: IGraphicsCommandEncoder, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, draw_count: u32, stride: u32) void { @@ -914,12 +870,8 @@ pub const IRenderStateContext = struct { vtable: *const VTable, pub const VTable = struct { - setModelMatrix: *const fn (ptr: *anyopaque, model: Mat4, color: Vec3, mask_radius: f32) void, + setModelMatrix: *const fn (ptr: *anyopaque, model: Mat4, color: Vec3) void, setInstanceBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, - setLODInstanceBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, - setLODDescriptorStream: *const fn (ptr: *anyopaque, stream: LODDescriptorStream) void, - setLODCompactSampleBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, - setLODCompactInstanceBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, setTerrainPipelineBound: *const fn (ptr: *anyopaque, bound: bool) void, setSelectionMode: *const fn (ptr: *anyopaque, enabled: bool) void, updateGlobalUniforms: *const fn (ptr: *anyopaque, uniforms: GlobalUniforms, frame_params: FrameRenderParams) anyerror!void, @@ -928,31 +880,14 @@ pub const IRenderStateContext = struct { /// Sets model matrix on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. - pub fn setModelMatrix(self: IRenderStateContext, model: Mat4, color: Vec3, mask_radius: f32) void { - self.vtable.setModelMatrix(self.ptr, model, color, mask_radius); + pub fn setModelMatrix(self: IRenderStateContext, model: Mat4, color: Vec3) void { + self.vtable.setModelMatrix(self.ptr, model, color); } /// Sets instance buffer on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. pub fn setInstanceBuffer(self: IRenderStateContext, handle: BufferHandle) void { self.vtable.setInstanceBuffer(self.ptr, handle); } - /// Binds the LOD instance buffer used by distant-terrain draw calls. - /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. - pub fn setLODInstanceBuffer(self: IRenderStateContext, handle: BufferHandle) void { - self.vtable.setLODInstanceBuffer(self.ptr, handle); - } - /// Selects the immutable descriptor set used by subsequent LOD bindings - /// and draws. Must be called before the stream's buffers are set. - pub fn setLODDescriptorStream(self: IRenderStateContext, stream: LODDescriptorStream) void { - self.vtable.setLODDescriptorStream(self.ptr, stream); - } - /// Selects the immutable shared compact sample pool for LOD vertex pulling. - pub fn setLODCompactSampleBuffer(self: IRenderStateContext, handle: BufferHandle) void { - self.vtable.setLODCompactSampleBuffer(self.ptr, handle); - } - pub fn setLODCompactInstanceBuffer(self: IRenderStateContext, handle: BufferHandle) void { - self.vtable.setLODCompactInstanceBuffer(self.ptr, handle); - } /// Sets terrain pipeline bound on the active graphics backend. /// The setting affects later frames or later commands according to backend state lifetime. Must be called from the render thread that owns the backend context. pub fn setTerrainPipelineBound(self: IRenderStateContext, bound: bool) void { @@ -1169,6 +1104,11 @@ pub const IRenderContext = struct { pub fn requestSwapchainRecreate(self: IRenderContext) void { self.vtable.requestSwapchainRecreate(self.ptr); } + /// Sets the color used when the backend clears the main render target. + /// The value takes effect according to backend frame-state lifetime rules. + pub fn setClearColor(self: IRenderContext, color: Vec3) void { + self.vtable.setClearColor(self.ptr, color); + } /// Returns encoder from the active graphics backend. /// The returned value is backend-owned or diagnostic unless the specific type documents otherwise. pub fn getEncoder(self: IRenderContext) IGraphicsCommandEncoder { @@ -1372,9 +1312,6 @@ pub const IDeviceQuery = struct { getFrameIndex: *const fn (ptr: *anyopaque) usize, supportsIndirectFirstInstance: *const fn (ptr: *anyopaque) bool, supportsIndirectCount: *const fn (ptr: *anyopaque) bool, - /// The backend provides immutable descriptor snapshots for compact - /// terrain/water GPU-culling streams. - supportsCompactLODGpuCulling: *const fn (ptr: *anyopaque) bool, getMaxAnisotropy: *const fn (ptr: *anyopaque) u8, getMaxMSAASamples: *const fn (ptr: *anyopaque) u8, getFaultCount: *const fn (ptr: *anyopaque) u32, @@ -1399,11 +1336,6 @@ pub const IDeviceQuery = struct { pub fn supportsIndirectCount(self: IDeviceQuery) bool { return self.vtable.supportsIndirectCount(self.ptr); } - /// Reports whether compact terrain and water can share a frame without - /// mutating descriptors referenced by already-recorded commands. - pub fn supportsCompactLODGpuCulling(self: IDeviceQuery) bool { - return self.vtable.supportsCompactLODGpuCulling(self.ptr); - } /// Returns fault count from the active graphics backend. /// The returned value is backend-owned or diagnostic unless the specific type documents otherwise. pub fn getFaultCount(self: IDeviceQuery) u32 { @@ -1639,7 +1571,6 @@ pub const ICullingSystemFactory = struct { pub const VTable = struct { createCullingSystem: *const fn (ctx: *anyopaque, allocator: Allocator, max_chunks: usize) anyerror!?ICullingSystem, - createLODCullingSystem: *const fn (ctx: *anyopaque, allocator: Allocator, max_regions: usize) anyerror!?ILODCullingSystem, }; /// Creates a GPU culling system bound to the active backend resources. @@ -1647,9 +1578,6 @@ pub const ICullingSystemFactory = struct { pub fn createCullingSystem(self: ICullingSystemFactory, allocator: Allocator, max_chunks: usize) anyerror!?ICullingSystem { return self.vtable.createCullingSystem(self.ptr, allocator, max_chunks); } - pub fn createLODCullingSystem(self: ICullingSystemFactory, allocator: Allocator, max_regions: usize) anyerror!?ILODCullingSystem { - return self.vtable.createLODCullingSystem(self.ptr, allocator, max_regions); - } }; pub const IScreenshotContext = struct { @@ -1914,10 +1842,6 @@ pub const RHI = struct { pub fn createCullingSystem(self: RHI, allocator: Allocator, max_chunks: usize) anyerror!?ICullingSystem { return self.cullingFactory().createCullingSystem(allocator, max_chunks); } - /// Creates the dedicated LOD compute/MDI compaction system. - pub fn createLODCullingSystem(self: RHI, allocator: Allocator, max_regions: usize) anyerror!?ILODCullingSystem { - return self.cullingFactory().createLODCullingSystem(allocator, max_regions); - } // Lifecycle /// Constructs an `RHI` composite from a backend pointer and vtable. diff --git a/modules/engine-rhi/src/rhi_contract_tests.zig b/modules/engine-rhi/src/rhi_contract_tests.zig index 9d7139e8..45a7d55d 100644 --- a/modules/engine-rhi/src/rhi_contract_tests.zig +++ b/modules/engine-rhi/src/rhi_contract_tests.zig @@ -339,9 +339,6 @@ test "RHI contract declares RenderContext.setModelMatrix" { test "RHI contract declares RenderContext.setInstanceBuffer" { try std.testing.expect(@hasDecl(rhi.RenderContext, "setInstanceBuffer")); } -test "RHI contract declares RenderContext.setLODInstanceBuffer" { - try std.testing.expect(@hasDecl(rhi.RenderContext, "setLODInstanceBuffer")); -} test "RHI contract declares RenderContext.setTerrainPipelineBound" { try std.testing.expect(@hasDecl(rhi.RenderContext, "setTerrainPipelineBound")); } @@ -497,13 +494,6 @@ test "RHI contract declares IRenderStateContext.setModelMatrix" { test "RHI contract declares IRenderStateContext.setInstanceBuffer" { try std.testing.expect(@hasDecl(rhi.IRenderStateContext, "setInstanceBuffer")); } -test "RHI contract declares IRenderStateContext.setLODInstanceBuffer" { - try std.testing.expect(@hasDecl(rhi.IRenderStateContext, "setLODInstanceBuffer")); -} - -test "RHI contract declares IRenderStateContext.setLODDescriptorStream" { - try std.testing.expect(@hasDecl(rhi.IRenderStateContext, "setLODDescriptorStream")); -} test "RHI contract declares IRenderStateContext.setTerrainPipelineBound" { try std.testing.expect(@hasDecl(rhi.IRenderStateContext, "setTerrainPipelineBound")); } @@ -664,9 +654,6 @@ test "RHI contract declares IDeviceQuery.supportsIndirectFirstInstance" { try std.testing.expect(@hasDecl(rhi.IDeviceQuery, "supportsIndirectFirstInstance")); } -test "RHI contract declares compact GPU-culling safety capability" { - try std.testing.expect(@hasDecl(rhi.IDeviceQuery, "supportsCompactLODGpuCulling")); -} test "RHI contract declares IDeviceQuery.getFaultCount" { try std.testing.expect(@hasDecl(rhi.IDeviceQuery, "getFaultCount")); } diff --git a/modules/engine-rhi/src/rhi_types.zig b/modules/engine-rhi/src/rhi_types.zig index 8c815a60..4654c944 100644 --- a/modules/engine-rhi/src/rhi_types.zig +++ b/modules/engine-rhi/src/rhi_types.zig @@ -116,7 +116,7 @@ pub const Vertex = extern struct { packed_meta: u32, blocklight: u32, - pub const LOD_TILE_ID: u16 = 0xFFFF; + pub const CLOUD_TILE_ID: u16 = 0xFFFF; pub fn init( pos: [3]f32, @@ -157,22 +157,6 @@ pub const Vertex = extern struct { .blocklight = encodeBlocklight(blocklight, true), }; } - - pub fn initLOD( - pos: [3]f32, - color: [3]f32, - normal: [3]f32, - uv: [2]f32, - ) Vertex { - return .{ - .pos = pos, - .color = encodeColor(color), - .normal = encodeNormal(normal), - .uv = .{ @floatCast(uv[0]), @floatCast(uv[1]) }, - .packed_meta = encodeMeta(LOD_TILE_ID, 1.0, 1.0), - .blocklight = encodeBlocklight(.{ 0.0, 0.0, 0.0 }, false), - }; - } }; /// Encode RGB float color to RGBA8. Alpha is always 255. @@ -210,7 +194,7 @@ pub fn encodeNormal(n: [3]f32) u32 { } /// Pack tile_id (u16), skylight (u8), and AO (u8) into a single u32. -/// Skylight and AO precision: 1/255 (~0.4% steps). Tile IDs 0-65534 valid; 0xFFFF is LOD sentinel. +/// Skylight and AO precision: 1/255 (~0.4% steps). pub fn encodeMeta(tile_id: u16, skylight: f32, ao: f32) u32 { const sl: u8 = @intFromFloat(@round(@max(0.0, @min(1.0, skylight)) * 255.0)); const ao_u8: u8 = @intFromFloat(@round(@max(0.0, @min(1.0, ao)) * 255.0)); @@ -243,173 +227,6 @@ pub const DrawMode = enum { points, }; -/// Parameters for the far-LOD vertex-pulling path. Samples live in a storage -/// buffer; the reusable index grid supplies `gl_VertexIndex`. -pub const CompactLODDraw = extern struct { - model: Mat4, - mask_radius: f32, - lod_fade: f32, - sample_offset: u32, - width: u32, - cell_size: f32, - layer: u32, - skirt_depth: f32, - /// Bits 0..3: authoritative same-level compact aprons (N/E/S/W). The - /// shader emits a skirt for every complementary edge. - edge_masks: u32 = 0, -}; - -/// Selects one immutable descriptor snapshot before any LOD stream bindings -/// are updated or draw commands are recorded. Direct and GPU-indirect streams -/// are intentionally distinct even when they currently share a buffer type. -pub const LODDescriptorStream = enum(u3) { - terrain_standard_direct, - water_standard_direct, - terrain_standard_gpu, - water_standard_gpu, - terrain_compact_direct, - water_compact_direct, - terrain_compact_gpu, - water_compact_gpu, -}; - -/// Per-draw compact-tile data consumed from a std430 SSBO by the indirect -/// vertex-pulling path. `gl_InstanceIndex` includes `firstInstance`, so the -/// compute pass can compact this record and use its output slot directly. -pub const CompactLODInstance = extern struct { - model: Mat4, - /// mask radius, LOD fade, cell size, skirt depth - params: [4]f32, - /// sample offset, tile width, layer, authoritative-apron mask - words: [4]u32, -}; - -pub const DrawIndexedIndirectCommand = extern struct { - indexCount: u32, - instanceCount: u32, - firstIndex: u32, - vertexOffset: i32, - firstInstance: u32, -}; - -comptime { - // extern alignment rounds the scalar payload fields to a 16-byte - // push-constant block, matching Vulkan's GLSL layout size. - std.debug.assert(@sizeOf(CompactLODDraw) == 96); - std.debug.assert(@offsetOf(CompactLODDraw, "mask_radius") == 64); - std.debug.assert(@offsetOf(CompactLODDraw, "lod_fade") == 68); - std.debug.assert(@offsetOf(CompactLODDraw, "sample_offset") == 72); - std.debug.assert(@offsetOf(CompactLODDraw, "width") == 76); - std.debug.assert(@offsetOf(CompactLODDraw, "cell_size") == 80); - std.debug.assert(@offsetOf(CompactLODDraw, "layer") == 84); - std.debug.assert(@offsetOf(CompactLODDraw, "skirt_depth") == 88); - std.debug.assert(@offsetOf(CompactLODDraw, "edge_masks") == 92); -} - -test "compact LOD push constants match GLSL scalar offsets" { - try std.testing.expectEqual(@as(usize, 96), @sizeOf(CompactLODDraw)); - try std.testing.expectEqual(@as(usize, 0), @offsetOf(CompactLODDraw, "model")); - try std.testing.expectEqual(@as(usize, 64), @offsetOf(CompactLODDraw, "mask_radius")); - try std.testing.expectEqual(@as(usize, 68), @offsetOf(CompactLODDraw, "lod_fade")); - try std.testing.expectEqual(@as(usize, 72), @offsetOf(CompactLODDraw, "sample_offset")); - try std.testing.expectEqual(@as(usize, 76), @offsetOf(CompactLODDraw, "width")); - try std.testing.expectEqual(@as(usize, 80), @offsetOf(CompactLODDraw, "cell_size")); - try std.testing.expectEqual(@as(usize, 84), @offsetOf(CompactLODDraw, "layer")); - try std.testing.expectEqual(@as(usize, 88), @offsetOf(CompactLODDraw, "skirt_depth")); - try std.testing.expectEqual(@as(usize, 92), @offsetOf(CompactLODDraw, "edge_masks")); -} - -test "compact LOD indirect instance and indexed command ABI" { - try std.testing.expectEqual(@as(usize, 96), @sizeOf(CompactLODInstance)); - try std.testing.expectEqual(@as(usize, 64), @offsetOf(CompactLODInstance, "params")); - try std.testing.expectEqual(@as(usize, 80), @offsetOf(CompactLODInstance, "words")); - try std.testing.expectEqual(@as(usize, 20), @sizeOf(DrawIndexedIndirectCommand)); - try std.testing.expectEqual(@as(usize, 16), @offsetOf(DrawIndexedIndirectCommand, "firstInstance")); -} - -/// Little-endian 128-bit compact LOD sample, represented as Vulkan SSBO words. -/// The field locations are shared by the world upload format and the compact -/// vertex shaders. This is deliberately a word view: Vulkan storage buffers -/// expose the 16-byte sample as `uvec4`. -pub const CompactLODSampleWords = extern struct { - words: [4]u32, - - pub const Decoded = struct { - terrain_height: i16, - water_height: i16, - water_depth: u8, - water_coverage: u8, - surface_material: u7, - subsurface_material: u7, - foundation_material: u7, - color: u32, - sky_light: u4, - block_light: u4, - ambient_occlusion: u6, - }; - - pub fn decode(self: CompactLODSampleWords) Decoded { - return .{ - .terrain_height = signed16(self.words[0], 0), - .water_height = signed16(self.words[0], 16), - .water_depth = @truncate(self.words[1]), - .water_coverage = @truncate(self.words[1] >> 8), - .surface_material = @truncate(self.words[1] >> 16), - .subsurface_material = @truncate(self.words[1] >> 23), - .foundation_material = @truncate((self.words[1] >> 30) | (self.words[2] << 2)), - .color = (self.words[2] >> 5) & 0x00ff_ffff, - .sky_light = @truncate((self.words[2] >> 29) | (self.words[3] << 3)), - .block_light = @truncate(self.words[3] >> 1), - .ambient_occlusion = @truncate(self.words[3] >> 5), - }; - } - - fn signed16(word: u32, shift: u5) i16 { - const raw: u16 = @truncate(word >> shift); - return @bitCast(raw); - } -}; - -comptime { - std.debug.assert(@sizeOf(CompactLODSampleWords) == 16); - std.debug.assert(@alignOf(CompactLODSampleWords) == @alignOf(u32)); - std.debug.assert(@offsetOf(CompactLODSampleWords, "words") == 0); -} - -test "compact LOD 128-bit sample golden vector decodes across word boundaries" { - var bits: u128 = 0; - bits |= @as(u128, @as(u16, @bitCast(@as(i16, -100)))); - bits |= @as(u128, @as(u16, @bitCast(@as(i16, 58)))) << 16; - bits |= @as(u128, 9) << 32; - bits |= @as(u128, 255) << 40; - bits |= @as(u128, 0x15) << 48; - bits |= @as(u128, 0x2a) << 55; - bits |= @as(u128, 0x45) << 62; - bits |= @as(u128, 0x00ab_cdef) << 69; - bits |= @as(u128, 15) << 93; - bits |= @as(u128, 11) << 97; - bits |= @as(u128, 42) << 101; - - const sample = CompactLODSampleWords{ .words = .{ - @truncate(bits), - @truncate(bits >> 32), - @truncate(bits >> 64), - @truncate(bits >> 96), - } }; - const decoded = sample.decode(); - try std.testing.expectEqual(@as(i16, -100), decoded.terrain_height); - try std.testing.expectEqual(@as(i16, 58), decoded.water_height); - try std.testing.expectEqual(@as(u8, 9), decoded.water_depth); - try std.testing.expectEqual(@as(u8, 255), decoded.water_coverage); - try std.testing.expectEqual(@as(u7, 0x15), decoded.surface_material); - try std.testing.expectEqual(@as(u7, 0x2a), decoded.subsurface_material); - try std.testing.expectEqual(@as(u7, 0x45), decoded.foundation_material); - try std.testing.expectEqual(@as(u32, 0x00ab_cdef), decoded.color); - try std.testing.expectEqual(@as(u4, 15), decoded.sky_light); - try std.testing.expectEqual(@as(u4, 11), decoded.block_light); - try std.testing.expectEqual(@as(u6, 42), decoded.ambient_occlusion); -} - pub const ShaderStageFlags = packed struct(u32) { vertex: bool = false, fragment: bool = false, @@ -426,9 +243,6 @@ pub const DrawIndirectCommand = extern struct { pub const InstanceData = extern struct { model: Mat4, - mask_radius: f32, - lod_fade: f32, - padding: [2]f32, }; pub const SkyParams = struct { @@ -557,19 +371,6 @@ pub const GpuTimingResults = struct { lpv_pass_ms: f32, sky_pass_ms: f32, opaque_pass_ms: f32, - /// GPU time spent drawing distant-terrain LOD geometry. This is a subset - /// of the containing scene pass and is therefore excluded from total_gpu_ms. - lod_terrain_pass_ms: f32, - /// GPU time spent drawing distant-water LOD geometry. This is a subset - /// of the containing scene pass and is therefore excluded from total_gpu_ms. - lod_water_pass_ms: f32, - /// Compact LOD terrain draw scope; retained separately from aggregate LOD terrain. - lod_compact_terrain_pass_ms: f32, - /// Compact LOD water draw scope; retained separately from aggregate LOD water. - lod_compact_water_pass_ms: f32, - /// GPU time spent culling LOD candidates, compacting indirect commands, - /// and executing the compute-to-indirect barrier. - lod_culling_compute_ms: f32, main_pass_ms: f32, // Overall main pass time (sum of sky and opaque) bloom_pass_ms: f32, fxaa_pass_ms: f32, diff --git a/modules/engine-rhi/src/root.zig b/modules/engine-rhi/src/root.zig index c14392dc..9701a335 100644 --- a/modules/engine-rhi/src/root.zig +++ b/modules/engine-rhi/src/root.zig @@ -39,12 +39,7 @@ pub const Vertex = rhi_types.Vertex; pub const DrawMode = rhi_types.DrawMode; pub const ShaderStageFlags = rhi_types.ShaderStageFlags; pub const DrawIndirectCommand = rhi_types.DrawIndirectCommand; -pub const DrawIndexedIndirectCommand = rhi_types.DrawIndexedIndirectCommand; pub const InstanceData = rhi_types.InstanceData; -pub const CompactLODDraw = rhi_types.CompactLODDraw; -pub const LODDescriptorStream = rhi_types.LODDescriptorStream; -pub const CompactLODInstance = rhi_types.CompactLODInstance; -pub const CompactLODSampleWords = rhi_types.CompactLODSampleWords; pub const SkyParams = rhi_types.SkyParams; pub const SkyPushConstants = rhi_types.SkyPushConstants; pub const FrameRenderParams = rhi_types.FrameRenderParams; @@ -90,20 +85,10 @@ pub const ILPVWorld = world_contracts.ILPVWorld; pub const GpuLight = world_contracts.GpuLight; pub const Texture = texture.Texture; pub const Config = texture.Config; -pub const RenderDistancePreset = render_settings.RenderDistancePreset; -pub const RenderDistancePresetConfig = render_settings.RenderDistancePresetConfig; -pub const LODMeshPath = render_settings.LODMeshPath; -pub const RENDER_DISTANCE_PRESETS = render_settings.RENDER_DISTANCE_PRESETS; pub const RenderSettingsAdapter = render_settings.RenderSettingsAdapter; -pub const getPresetConfig = render_settings.getPresetConfig; pub const ChunkCullData = culling.ChunkCullData; pub const DispatchConfig = culling.DispatchConfig; pub const ICullingSystem = culling.ICullingSystem; -pub const LODCullCandidate = culling.LODCullCandidate; -pub const LODCullCommand = culling.LODCullCommand; -pub const LODCullDispatch = culling.LODCullDispatch; -pub const LODCullDiagnostics = culling.LODCullDiagnostics; -pub const ILODCullingSystem = culling.ILODCullingSystem; pub const RenderDevice = render_device.RenderDevice; pub const Stats = render_device.Stats; pub const encodeColor = rhi_types.encodeColor; diff --git a/modules/engine-rhi/src/world_contracts.zig b/modules/engine-rhi/src/world_contracts.zig index ec779b77..75c03ab5 100644 --- a/modules/engine-rhi/src/world_contracts.zig +++ b/modules/engine-rhi/src/world_contracts.zig @@ -25,26 +25,21 @@ pub const IWorldRenderView = struct { vtable: *const VTable, pub const VTable = struct { - prepareLODCulling: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, - render: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, - renderOpaque: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, - renderFluid: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, + render: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, + renderOpaque: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, + renderFluid: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, }; - pub fn prepareLODCulling(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { - self.vtable.prepareLODCulling(self.ptr, view_proj, camera_pos); + pub fn render(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.render(self.ptr, view_proj, camera_pos); } - pub fn render(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.render(self.ptr, view_proj, camera_pos, render_lod); + pub fn renderOpaque(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.renderOpaque(self.ptr, view_proj, camera_pos); } - pub fn renderOpaque(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.renderOpaque(self.ptr, view_proj, camera_pos, render_lod); - } - - pub fn renderFluid(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.renderFluid(self.ptr, view_proj, camera_pos, render_lod); + pub fn renderFluid(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.renderFluid(self.ptr, view_proj, camera_pos); } }; diff --git a/modules/engine-ui/src/debug_frustum.zig b/modules/engine-ui/src/debug_frustum.zig index 10a1aa1b..c6009b08 100644 --- a/modules/engine-ui/src/debug_frustum.zig +++ b/modules/engine-ui/src/debug_frustum.zig @@ -88,7 +88,7 @@ pub const DebugFrustum = struct { const rel_y = -cam_pos.y; const rel_z = -cam_pos.z; const model = Mat4.translate(Vec3.init(rel_x, rel_y, rel_z)); - rc.setModelMatrix(model, Vec3.init(DefaultColor.r, DefaultColor.g, DefaultColor.b), 0); + rc.setModelMatrix(model, Vec3.init(DefaultColor.r, DefaultColor.g, DefaultColor.b)); rc.draw(buffer_handle, @as(u32, @intCast(vertex_count)), .lines); } }; diff --git a/modules/engine-ui/src/debug_menu.zig b/modules/engine-ui/src/debug_menu.zig index f017b43a..561441f2 100644 --- a/modules/engine-ui/src/debug_menu.zig +++ b/modules/engine-ui/src/debug_menu.zig @@ -26,7 +26,6 @@ pub const DebugFeature = enum(u8) { block_light_debug, outdoor_factor_debug, timing_overlay, - lod_render, gpass_render, ssao, fog, @@ -66,7 +65,6 @@ pub const FEATURE_INFOS = [DebugFeature.count]FeatureInfo{ .{ .label = "BLOCK LIGHT", .hotkey = "MENU" }, .{ .label = "OUTDOOR FACTOR", .hotkey = "MENU" }, .{ .label = "TIMING OVERLAY", .hotkey = "F4" }, - .{ .label = "LOD RENDER", .hotkey = "F6" }, .{ .label = "G-PASS RENDER", .hotkey = "F7" }, .{ .label = "SSAO", .hotkey = "F8" }, .{ .label = "FOG", .hotkey = "F10" }, diff --git a/modules/engine-ui/src/root.zig b/modules/engine-ui/src/root.zig index 2ea73419..6a943b88 100644 --- a/modules/engine-ui/src/root.zig +++ b/modules/engine-ui/src/root.zig @@ -26,10 +26,6 @@ pub const DebugUI = debug_ui.DebugUI; pub const FEATURE_INFOS = debug_menu.FEATURE_INFOS; pub const FontAtlas = font_atlas.FontAtlas; pub const InputEvent = ui_system.InputEvent; -pub const LODProfilingDisplay = timing_overlay.LODProfilingDisplay; -pub const LODVisibilityLevelDisplay = timing_overlay.LODVisibilityLevelDisplay; -pub const LOD_VISIBILITY_LEVEL_COUNT = timing_overlay.LOD_VISIBILITY_LEVEL_COUNT; -pub const LODStatsDisplay = timing_overlay.LODStatsDisplay; pub const PerformanceData = timing_overlay.PerformanceData; pub const Rect = ui_system.Rect; pub const RmlUi = rmlui.RmlUi; diff --git a/modules/engine-ui/src/timing_overlay.zig b/modules/engine-ui/src/timing_overlay.zig index b016adbe..b955c387 100644 --- a/modules/engine-ui/src/timing_overlay.zig +++ b/modules/engine-ui/src/timing_overlay.zig @@ -4,11 +4,8 @@ const Color = @import("ui_system.zig").Color; const Rect = @import("ui_system.zig").Rect; const rhi = @import("engine-rhi").rhi; const RenderDeviceStats = @import("engine-rhi").render_device.Stats; -const LODLevel = @import("engine-core").lod_types.LODLevel; const font = @import("font.zig"); -pub const LOD_VISIBILITY_LEVEL_COUNT = LODLevel.count; - pub const TimingOverlay = struct { enabled: bool = false, @@ -25,12 +22,9 @@ pub const TimingOverlay = struct { comptime std.debug.assert(rhi.SHADOW_CASCADE_COUNT >= 3); - var num_lines: f32 = 2 + 1 + 15 + 1 + 6 + 1 + 4 + 1; - if (data.world) |ws| { + var num_lines: f32 = 2 + 1 + 12 + 1 + 6 + 1 + 4 + 1; + if (data.world != null) { num_lines += 1 + 6; - if (ws.lod != null) { - num_lines += 1 + 8; - } } const padding: f32 = 25; @@ -63,9 +57,6 @@ pub const TimingOverlay = struct { drawGpuLine(ui, "LPV:", data.gpu.lpv_pass_ms, label_x, value_x, &y, scale, muted); drawGpuLine(ui, "SKY:", data.gpu.sky_pass_ms, label_x, value_x, &y, scale, muted); drawGpuLine(ui, "OPAQUE:", data.gpu.opaque_pass_ms, label_x, value_x, &y, scale, muted); - drawGpuLine(ui, "LOD TERRAIN:", data.gpu.lod_terrain_pass_ms, label_x, value_x, &y, scale, muted); - drawGpuLine(ui, "LOD CULLING:", data.gpu.lod_culling_compute_ms, label_x, value_x, &y, scale, muted); - drawGpuLine(ui, "LOD WATER:", data.gpu.lod_water_pass_ms, label_x, value_x, &y, scale, muted); drawGpuLine(ui, "MAIN:", data.gpu.main_pass_ms, label_x, value_x, &y, scale, muted); drawGpuLine(ui, "BLOOM:", data.gpu.bloom_pass_ms, label_x, value_x, &y, scale, muted); drawGpuLine(ui, "FXAA:", data.gpu.fxaa_pass_ms, label_x, value_x, &y, scale, muted); @@ -80,18 +71,6 @@ pub const TimingOverlay = struct { drawStatLine(ui, "CULLED:", ws.chunks_culled, label_x, value_x, &y, scale, Color.rgba(0.92, 0.97, 1.0, 1.0)); drawStatLine(ui, "VERTS:", ws.vertices_rendered, label_x, value_x, &y, scale, Color.rgba(0.92, 0.97, 1.0, 1.0)); drawQueueLine(ui, "QUEUES:", ws.gen_queue, ws.mesh_queue, ws.upload_queue, label_x, value_x, &y, scale, Color.rgba(0.92, 0.97, 1.0, 1.0)); - - if (ws.lod) |ls| { - drawSectionHeader(ui, "LOD DISTRIBUTION", label_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD0:", ls.loaded[0], label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD1:", ls.loaded[1], label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD2:", ls.loaded[2], label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD3:", ls.loaded[3], label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD4:", ls.loaded[4], label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawStatLine(ui, "LOD MEM:", ls.memory_used_mb, label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawBytePairLine(ui, "FAR UP E/C:", ls.profiling.far_expanded_upload_bytes, ls.profiling.compact_upload_bytes, label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - drawMsPairLine(ui, "FAR CPU E/C:", ls.profiling.worker_far_expanded_mesh_construction_ms, ls.profiling.worker_compact_encode_ms, label_x, value_x, &y, scale, Color.rgba(0.5, 0.7, 1.0, 1.0)); - } } drawSectionHeader(ui, "GPU MEMORY", label_x, &y, scale, Color.rgba(0.95, 0.62, 0.24, 1.0)); @@ -141,24 +120,6 @@ pub const TimingOverlay = struct { y.* += 15; } - fn drawBytePairLine(ui: *UISystem, label: []const u8, expanded: u64, compact: u64, label_x: f32, right_x: f32, y: *f32, scale: f32, color: Color) void { - font.drawText(ui, label, label_x, y.*, scale, color); - var buf: [48]u8 = undefined; - const val_str = std.fmt.bufPrint(&buf, "{d}/{d}", .{ expanded, compact }) catch "0/0"; - const val_w = font.measureTextWidth(val_str, scale); - font.drawText(ui, val_str, right_x - val_w, y.*, scale, color); - y.* += 15; - } - - fn drawMsPairLine(ui: *UISystem, label: []const u8, expanded: f64, compact: f64, label_x: f32, right_x: f32, y: *f32, scale: f32, color: Color) void { - font.drawText(ui, label, label_x, y.*, scale, color); - var buf: [48]u8 = undefined; - const val_str = std.fmt.bufPrint(&buf, "{d:.2}/{d:.2}", .{ expanded, compact }) catch "0/0"; - const val_w = font.measureTextWidth(val_str, scale); - font.drawText(ui, val_str, right_x - val_w, y.*, scale, color); - y.* += 15; - } - fn drawMemoryLine(ui: *UISystem, label: []const u8, count: u32, bytes: usize, label_x: f32, right_x: f32, y: *f32, scale: f32, color: Color) void { font.drawText(ui, label, label_x, y.*, scale, color); const mb = @as(f32, @floatFromInt(bytes)) / (1024.0 * 1024.0); @@ -191,108 +152,4 @@ pub const WorldStats = struct { gen_queue: usize, mesh_queue: usize, upload_queue: usize, - /// Current LOD regions with renderable meshes. This gauge is common to CPU - /// and GPU culling benchmark sources, unlike GPU candidate telemetry. - lod_renderable_regions: u64 = 0, - lod: ?LODStatsDisplay, -}; - -pub const LODStatsDisplay = struct { - loaded: [LODLevel.count]u32, - memory_used_mb: u32, - /// Current known LOD allocations. These are accounting gauges, not - /// GPU-driver memory measurements. - memory_used_bytes: u64 = 0, - pool_gpu_capacity_bytes: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - pool_gpu_slack_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - compact_pool_capacity_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - compact_pool_free_bytes: u64 = 0, - compact_pool_retired_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - source_data_cpu_bytes: u64 = 0, - pending_cpu_upload_bytes: u64 = 0, - deferred_deletion_gpu_bytes: u64 = 0, - deferred_deletion_cpu_bytes: u64 = 0, - profiling: LODProfilingDisplay = .{}, -}; - -/// Dependency-neutral projection of the world-lod cumulative profiling snapshot. -/// Keeping this at the UI boundary lets telemetry consumers avoid a world-lod import. -pub const LODProfilingDisplay = struct { - enabled: bool = false, - update_ms: f64 = 0, - scheduling_ms: f64 = 0, - cache_ms: f64 = 0, - generation_dispatch_ms: f64 = 0, - state_transition_ms: f64 = 0, - upload_prep_ms: f64 = 0, - upload_submission_ms: f64 = 0, - visibility_ms: f64 = 0, - coverage_ms: f64 = 0, - eviction_ms: f64 = 0, - worker_generation_ms: f64 = 0, - worker_mesh_construction_ms: f64 = 0, - worker_far_expanded_mesh_construction_ms: f64 = 0, - worker_compact_encode_ms: f64 = 0, - manager_lock_wait_ms: f64 = 0, - manager_lock_hold_ms: f64 = 0, - upload_bytes: u64 = 0, - far_expanded_upload_bytes: u64 = 0, - compact_upload_bytes: u64 = 0, - pending_cpu_upload_bytes: u64 = 0, - staging_pressure_count: u64 = 0, - visible_count: u64 = 0, - rejected_count: u64 = 0, - coverage_count: u64 = 0, - visibility_levels: [LOD_VISIBILITY_LEVEL_COUNT]LODVisibilityLevelDisplay = [_]LODVisibilityLevelDisplay{.{}} ** LOD_VISIBILITY_LEVEL_COUNT, - deferred_deletion_bytes: u64 = 0, - deferred_deletion_cpu_bytes: u64 = 0, - pool_gpu_capacity_bytes: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - pool_gpu_slack_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - compact_pool_capacity_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - compact_pool_free_bytes: u64 = 0, - compact_pool_retired_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - known_memory_bytes: u64 = 0, - wait_idle_count: u64 = 0, - wait_idle_ms: f64 = 0, - gpu_culling_overflows: u32 = 0, - gpu_culling_validation_mismatches: u32 = 0, - gpu_culling_requested: bool = false, - gpu_culling_threshold: u32 = 0, - gpu_culling_candidate_count: u32 = 0, - gpu_culling_candidate_count_max: u32 = 0, - gpu_culling_draw_submissions: u64 = 0, - gpu_culling_validation_generation: u64 = 0, - gpu_culling_validation_completed_generation: u64 = 0, - gpu_culling_validation_completed_count: u64 = 0, - compact_selected: u64 = 0, - compact_build_rejected: u64 = 0, - compact_upload_failures: u64 = 0, - compact_draw_unavailable: u64 = 0, - compact_draw_failures: u64 = 0, - compact_submissions: u64 = 0, - compact_recoveries: u64 = 0, - compact_disabled: u64 = 0, -}; - -/// UI-neutral per-level visibility projection counters from world-lod. -pub const LODVisibilityLevelDisplay = struct { - candidates: u64 = 0, - accepted: u64 = 0, - rejected_no_draw: u64 = 0, - rejected_not_ready: u64 = 0, - rejected_missing_region: u64 = 0, - rejected_not_renderable: u64 = 0, - rejected_finer_coverage: u64 = 0, - rejected_range: u64 = 0, - rejected_frustum: u64 = 0, - rejected_chunk_coverage: u64 = 0, - coverage_checks: u64 = 0, }; diff --git a/modules/game-core/src/benchmark.zig b/modules/game-core/src/benchmark.zig index d0b4b9fa..13eef993 100644 --- a/modules/game-core/src/benchmark.zig +++ b/modules/game-core/src/benchmark.zig @@ -5,20 +5,9 @@ const Player = @import("player.zig").Player; const Vec3 = @import("engine-math").Vec3; const GpuTimingResults = @import("engine-rhi").GpuTimingResults; const WorldStats = @import("engine-ui").WorldStats; -const LODStatsDisplay = @import("engine-ui").LODStatsDisplay; -const LODProfilingDisplay = @import("engine-ui").LODProfilingDisplay; -const LODVisibilityLevelDisplay = @import("engine-ui").LODVisibilityLevelDisplay; -const LOD_VISIBILITY_LEVEL_COUNT = @import("engine-ui").LOD_VISIBILITY_LEVEL_COUNT; - -pub const Waypoint = struct { - pos: Vec3, - look: Vec3, - duration: f32, -}; pub const BENCHMARK_WORLD_SEED: u64 = 12345; -pub const BENCHMARK_SCHEMA_VERSION: u32 = 3; -pub const LOD_BUDGET_SCHEMA_VERSION: u32 = 1; +pub const BENCHMARK_SCHEMA_VERSION: u32 = 4; pub const Scenario = enum { stationary, @@ -44,12 +33,14 @@ pub const Scenario = enum { } }; -const Pose = struct { +const Waypoint = struct { pos: Vec3, look: Vec3, + duration: f32, }; -pub const BENCH_PATH = [_]Waypoint{ +const STATIONARY_PATH = [_]Waypoint{.{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(1, -0.1, 0), .duration = 60.0 }}; +const TRAVERSAL_PATH = [_]Waypoint{ .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(1, 0, 0), .duration = 5.0 }, .{ .pos = Vec3.init(200, 150, 200), .look = Vec3.init(0, -0.3, 1), .duration = 10.0 }, .{ .pos = Vec3.init(-500, 80, 300), .look = Vec3.init(1, 0, -1), .duration = 10.0 }, @@ -57,18 +48,12 @@ pub const BENCH_PATH = [_]Waypoint{ .{ .pos = Vec3.init(300, 90, -800), .look = Vec3.init(-1, 0.25, 0.2), .duration = 10.0 }, .{ .pos = Vec3.init(900, 160, 700), .look = Vec3.init(0.3, -0.9, -0.1), .duration = 15.0 }, }; - -const STATIONARY_PATH = [_]Waypoint{ - .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(1, -0.1, 0), .duration = 60.0 }, -}; - const RAPID_TURN_PATH = [_]Waypoint{ .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(1, 0, 0), .duration = 1.0 }, .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(0.2, -0.2, 1), .duration = 1.0 }, .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(-1, 0.1, 0), .duration = 1.0 }, .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(-0.2, 0.2, -1), .duration = 1.0 }, }; - const TELEPORT_EVICTION_PATH = [_]Waypoint{ .{ .pos = Vec3.init(8, 100, 8), .look = Vec3.init(1, -0.1, 0), .duration = 4.0 }, .{ .pos = Vec3.init(1_536, 140, 512), .look = Vec3.init(-1, -0.2, 0), .duration = 4.0 }, @@ -77,88 +62,6 @@ const TELEPORT_EVICTION_PATH = [_]Waypoint{ .{ .pos = Vec3.init(1_024, 110, -1_024), .look = Vec3.init(0.1, -0.1, 1), .duration = 4.0 }, }; -pub const FrameSample = struct { - cpu_ms: f32, - fps: f32, - gpu_shadow_ms: f32, - gpu_opaque_ms: f32, - gpu_lod_terrain_ms: f32, - gpu_lod_water_ms: f32, - gpu_lod_compact_terrain_ms: f32, - gpu_lod_compact_water_ms: f32, - gpu_lod_culling_ms: f32, - gpu_total_ms: f32, - draw_calls: u32, - vertices: u64, - chunks_rendered: u32, - gpu_memory_mb: f32, - lod: LODFrameSample = .{}, -}; - -/// One benchmark-frame delta derived from world-lod telemetry. Timing and event -/// fields are cumulative counters; every byte-accounting field is a gauge. -pub const LODFrameSample = struct { - enabled: bool = false, - cpu_ms: f64 = 0, - scheduling_ms: f64 = 0, - cache_ms: f64 = 0, - generation_dispatch_ms: f64 = 0, - state_transition_ms: f64 = 0, - upload_prep_ms: f64 = 0, - upload_submission_ms: f64 = 0, - visibility_ms: f64 = 0, - coverage_ms: f64 = 0, - eviction_ms: f64 = 0, - worker_generation_ms: f64 = 0, - worker_mesh_construction_ms: f64 = 0, - worker_far_expanded_mesh_construction_ms: f64 = 0, - worker_compact_encode_ms: f64 = 0, - manager_lock_wait_ms: f64 = 0, - manager_lock_hold_ms: f64 = 0, - upload_bytes: u64 = 0, - far_expanded_upload_bytes: u64 = 0, - compact_upload_bytes: u64 = 0, - pending_cpu_upload_bytes: u64 = 0, - staging_pressure_count: u64 = 0, - visible_count: u64 = 0, - rejected_count: u64 = 0, - coverage_count: u64 = 0, - visibility_levels: [LOD_VISIBILITY_LEVEL_COUNT]LODVisibilityLevelDisplay = [_]LODVisibilityLevelDisplay{.{}} ** LOD_VISIBILITY_LEVEL_COUNT, - deferred_deletion_bytes: u64 = 0, - deferred_deletion_cpu_bytes: u64 = 0, - pool_gpu_capacity_bytes: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - pool_gpu_slack_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - compact_pool_capacity_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - compact_pool_free_bytes: u64 = 0, - compact_pool_retired_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - source_data_cpu_bytes: u64 = 0, - known_memory_bytes: u64 = 0, - wait_idle_count: u64 = 0, - wait_idle_ms: f64 = 0, - gpu_culling_overflows: u32 = 0, - gpu_culling_validation_mismatches: u32 = 0, - gpu_culling_requested: bool = false, - gpu_culling_threshold: u32 = 0, - gpu_culling_candidate_count: u32 = 0, - gpu_culling_candidate_count_max: u32 = 0, - gpu_culling_draw_submissions: u64 = 0, - gpu_culling_validation_generation: u64 = 0, - gpu_culling_validation_completed_generation: u64 = 0, - gpu_culling_validation_completed_count: u64 = 0, -}; - -pub const SloThresholds = struct { - fps_p1_min: f64, - max_frame_ms: f64, - draw_calls_max: f64, - vertices_max: f64, - gpu_memory_mb_max: f64, -}; - pub const Summary = struct { min: f64, avg: f64, @@ -173,184 +76,40 @@ pub const Summary = struct { pub const GpuSummary = struct { shadow_avg: f64, opaque_avg: f64, - lod_terrain_avg: f64, - lod_water_avg: f64, - lod_culling_avg: f64, total_avg: f64, - total: Summary = zeroSummary(), - lod_compact_terrain: Summary = zeroSummary(), - lod_compact_water: Summary = zeroSummary(), - lod_culling: Summary = zeroSummary(), -}; - -pub const TimingTotalAverage = struct { - total_ms: f64, - avg_ms: f64, -}; - -pub const LODCpuCategories = struct { - scheduling: TimingTotalAverage, - cache: TimingTotalAverage, - generation_dispatch: TimingTotalAverage, - state_transition: TimingTotalAverage, - upload_prep: TimingTotalAverage, - upload_submission: TimingTotalAverage, - visibility: TimingTotalAverage, - coverage: TimingTotalAverage, - eviction: TimingTotalAverage, - manager_lock_wait: TimingTotalAverage, - manager_lock_hold: TimingTotalAverage, -}; - -pub const LODWorkerSummary = struct { - generation_total_ms: f64, - mesh_construction_total_ms: f64, - far_expanded_mesh_construction_total_ms: f64, - compact_encode_total_ms: f64, -}; - -pub const ByteGaugeSummary = struct { - avg_bytes: f64, - max_bytes: u64, - last_bytes: u64, - p50_bytes: u64 = 0, - p95_bytes: u64 = 0, - p99_bytes: u64 = 0, -}; - -const ByteGaugeAccumulator = struct { - sum: f64 = 0, - max: u64 = 0, - last: u64 = 0, - - fn add(self: *ByteGaugeAccumulator, value: u64) void { - self.sum += @floatFromInt(value); - self.max = @max(self.max, value); - self.last = value; - } - - fn summarize(self: ByteGaugeAccumulator, frame_count: f64) ByteGaugeSummary { - return .{ - .avg_bytes = self.sum / frame_count, - .max_bytes = self.max, - .last_bytes = self.last, - }; - } - - /// Exact percentile calculation for a sampled gauge series. Keep percentile - /// calculation here so all byte telemetry shares one definition. - fn summarizeSamples(allocator: std.mem.Allocator, values: []const u64) !ByteGaugeSummary { - if (values.len == 0) return .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }; - const sorted = try allocator.dupe(u64, values); - defer allocator.free(sorted); - std.sort.block(u64, sorted, {}, lessThanU64); - var sum: f64 = 0; - for (sorted) |value| sum += @floatFromInt(value); - return .{ - .avg_bytes = sum / @as(f64, @floatFromInt(sorted.len)), - .max_bytes = sorted[sorted.len - 1], - .last_bytes = values[values.len - 1], - .p50_bytes = percentileU64(sorted, 0.50), - .p95_bytes = percentileU64(sorted, 0.95), - .p99_bytes = percentileU64(sorted, 0.99), - }; - } -}; - -pub const LODMemorySummary = struct { - upload_total_bytes: u64, - upload_avg_bytes: f64, - far_expanded_upload_total_bytes: u64, - compact_upload_total_bytes: u64, - pending_cpu_upload_bytes: ByteGaugeSummary, - /// Compatibility alias for `deferred_deletion_gpu_bytes`. - deferred_deletion_bytes: ByteGaugeSummary, - deferred_deletion_gpu_bytes: ByteGaugeSummary, - deferred_deletion_cpu_bytes: ByteGaugeSummary, - pool_gpu_capacity_bytes: ByteGaugeSummary, - pool_gpu_allocated_bytes: ByteGaugeSummary, - pool_gpu_slack_bytes: ByteGaugeSummary, - pool_cpu_shadow_bytes: ByteGaugeSummary, - compact_pool_capacity_bytes: ByteGaugeSummary, - compact_pool_allocated_bytes: ByteGaugeSummary, - compact_pool_free_bytes: ByteGaugeSummary, - compact_pool_retired_bytes: ByteGaugeSummary, - direct_mesh_gpu_bytes: ByteGaugeSummary, - source_data_cpu_bytes: ByteGaugeSummary, - known_memory_bytes: ByteGaugeSummary, - logical_ram_bytes: ByteGaugeSummary = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - logical_vram_bytes: ByteGaugeSummary = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, -}; - -pub const LODVisibilitySummary = struct { - visible_total: u64, - rejected_total: u64, - coverage_total: u64, - levels: [LOD_VISIBILITY_LEVEL_COUNT]LODVisibilityLevelDisplay, -}; - -pub const LODPressureSummary = struct { - staging_pressure_total: u64, - wait_idle_count_total: u64, - wait_idle_ms_total: f64, - wait_idle_ms_avg: f64, - gpu_culling_overflows_max: u32 = 0, - gpu_culling_validation_mismatches_max: u32 = 0, -}; - -/// Evidence that GPU culling was both requested and actively exercised. Counts -/// are sampled from completed render frames; validation fields are monotonic -/// delayed-readback diagnostics from the RHI. -pub const LODGpuCullingSummary = struct { - requested: bool = false, - threshold: u32 = 0, - candidate_count_total: u64 = 0, - candidate_count_max: u32 = 0, - draw_submission_count_total: u64 = 0, - validation_generation: u64 = 0, - validation_completed_generation: u64 = 0, - validation_completed_count: u64 = 0, -}; - -pub const LODBenchmarkSummary = struct { - profiling_enabled: bool, - profiling_frame_count: u32 = 0, - cpu_frame_ms: Summary, - gpu_frame_ms: Summary = zeroSummary(), - cpu_categories: LODCpuCategories, - workers: LODWorkerSummary, - memory_bytes: LODMemorySummary, - visibility: LODVisibilitySummary, - pressure: LODPressureSummary, - gpu_culling: LODGpuCullingSummary = .{}, - /// Latest cumulative count of confirmed compact direct submissions and - /// successfully emitted compact GPU indirect streams. - compact_submissions: u64 = 0, + total: Summary, }; pub const WorstFrame = struct { frame_index: u32, frame_ms: f64, gpu_total_ms: f64, - gpu_lod_terrain_ms: f64, - gpu_lod_water_ms: f64, - gpu_lod_culling_ms: f64, dominant_gpu_pass: []const u8, dominant_gpu_pass_ms: f64, - lod_cpu_ms: f64, - dominant_lod_cpu_category: []const u8, - dominant_lod_cpu_category_ms: f64, - lod_worker_generation_ms: f64, - lod_worker_mesh_construction_ms: f64, - lod_upload_bytes: u64, - lod_pending_cpu_upload_bytes: u64, - lod_deferred_deletion_bytes: u64, - lod_visible_count: u64, - lod_rejected_count: u64, - lod_coverage_count: u64, - lod_wait_idle_count: u64, - lod_wait_idle_ms: f64, - lod_staging_pressure_count: u64, +}; + +pub const BuildMetadata = struct { + mode: []const u8, + world: []const u8, + headless: bool = true, + resolution: [2]u32 = .{ 1920, 1080 }, +}; + +pub const BenchmarkProvenance = struct { + gpu_adapter: []const u8 = "unknown", + gpu_driver: []const u8 = "unknown", + runner: []const u8 = "unknown", + zig_toolchain: []const u8 = "unknown", +}; + +pub const BenchmarkCompletionEvidence = struct { + scenario_completed: bool = false, + sampled_duration_s: f64 = 0, + requested_duration_s: f64 = 0, + sampled_frame_count: u32 = 0, + warmup_ready: bool = false, + warmup_timed_out: bool = false, + evidence_mode: bool = false, }; pub const BenchmarkResults = struct { @@ -360,9 +119,8 @@ pub const BenchmarkResults = struct { scenario: []const u8, world_seed: u64, build: BuildMetadata, - provenance: BenchmarkProvenance = .{}, + provenance: BenchmarkProvenance, render_distance: i32, - horizon_distance: i32, gpu_memory_mb_avg: f64, gpu_memory_mb_max: f64, frames: u32, @@ -376,89 +134,27 @@ pub const BenchmarkResults = struct { vertices_avg: f64, chunks_rendered_avg: f64, worst_frame: WorstFrame, - lod: LODBenchmarkSummary, - /// Cumulative counters and allocation gauges observed when the asynchronous - /// startup/warmup gate became ready. These are deliberately not sampled - /// deltas: steady-state measurements below remain separate. - startup_evidence: BenchmarkStartupEvidence = .{}, - completion: BenchmarkCompletionEvidence = .{}, + completion: BenchmarkCompletionEvidence, }; -pub const BenchmarkProvenance = struct { - gpu_adapter: []const u8 = "unknown", - gpu_driver: []const u8 = "unknown", - runner: []const u8 = "unknown", - zig_toolchain: []const u8 = "unknown", -}; - -pub const BenchmarkStartupEvidence = struct { - readiness_observed: bool = false, - readiness_elapsed_s: f64 = 0, - upload_total_bytes: u64 = 0, - /// Successful LOD3/LOD4 uploads, split by the representation that owns - /// the uploaded GPU storage. Near pooled uploads are excluded. - far_expanded_upload_bytes: u64 = 0, - compact_upload_bytes: u64 = 0, - worker_generation_total_ms: f64 = 0, - worker_mesh_construction_total_ms: f64 = 0, - /// Successful LOD3/LOD4 worker construction, split by representation. - worker_far_expanded_mesh_construction_ms: f64 = 0, - worker_compact_encode_ms: f64 = 0, - compact_submissions: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - /// Current renderable LOD-region gauge at the common CPU/GPU readiness - /// point. It is deliberately not a cumulative visibility counter. - lod_renderable_regions: u64 = 0, - /// Current submitted candidate count at readiness. CPU culling sources - /// legitimately leave this at zero. - gpu_culling_candidate_count: u32 = 0, -}; - -pub const BenchmarkCompletionEvidence = struct { - scenario_completed: bool = false, - sampled_duration_s: f64 = 0, - requested_duration_s: f64 = 0, - sampled_frame_count: u32 = 0, - evidence_mode: bool = false, - warmup_ready: bool = false, - compact_ready: bool = false, - warmup_timed_out: bool = false, - /// Explicit capture controls, defaulted for schema-v3 compatibility. - lod_renderable_region_target: u32 = 0, - gpu_candidate_target: u32 = 0, +const FrameSample = struct { + cpu_ms: f32, + fps: f32, + gpu_shadow_ms: f32, + gpu_opaque_ms: f32, + gpu_total_ms: f32, + draw_calls: u32, + vertices: u64, + chunks_rendered: u32, + gpu_memory_mb: f32, }; -pub const LODPercentileBudgets = struct { cpu_p95_ms: f64, cpu_p99_ms: f64, gpu_p95_ms: f64, gpu_p99_ms: f64, logical_ram_p95_bytes: u64, logical_ram_p99_bytes: u64, logical_vram_p95_bytes: u64, logical_vram_p99_bytes: u64 }; -pub fn lodBudgetFor(preset: []const u8, scenario: Scenario) ?LODPercentileBudgets { - const ram_mib: u64 = if (std.ascii.eqlIgnoreCase(preset, "low")) 512 else if (std.ascii.eqlIgnoreCase(preset, "medium")) 768 else if (std.ascii.eqlIgnoreCase(preset, "high")) 1024 else if (std.ascii.eqlIgnoreCase(preset, "ultra")) 1152 else if (std.ascii.eqlIgnoreCase(preset, "extreme")) 1280 else return null; - const limits: [4]f64 = switch (scenario) { - .stationary => .{ 40, 64, 80, 120 }, - .traversal => .{ 75, 120, 125, 190 }, - .rapid_turn => .{ 65, 104, 115, 175 }, - .teleport_eviction => .{ 110, 176, 150, 225 }, - }; - const mib = 1024 * 1024; - return .{ .cpu_p95_ms = limits[0], .cpu_p99_ms = limits[1], .gpu_p95_ms = limits[2], .gpu_p99_ms = limits[3], .logical_ram_p95_bytes = ram_mib * mib * 3 / 4, .logical_ram_p99_bytes = ram_mib * mib, .logical_vram_p95_bytes = ram_mib * mib * 3 / 2, .logical_vram_p99_bytes = ram_mib * mib * 2 }; -} -fn zeroSummary() Summary { - return .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }; -} -pub const BuildMetadata = struct { - mode: []const u8, - world: []const u8, - fixture: []const u8, - headless: bool = true, - resolution: [2]u32 = .{ 1920, 1080 }, - /// The effective LOD horizon. This is distinct from near render distance - /// so large-horizon captures remain comparable without changing chunks. - horizon_distance: i32, - /// Benchmark-only LOD admission override. Zero means the preset value. - benchmark_lod_memory_budget_mb: u32 = 0, - /// Shared CPU/GPU warmup readiness target. Zero preserves normal warmup. - benchmark_require_gpu_candidates: u32 = 0, +pub const SloThresholds = struct { + fps_p1_min: f64, + max_frame_ms: f64, + draw_calls_max: f64, + vertices_max: f64, + gpu_memory_mb_max: f64, }; pub const BenchmarkRunner = struct { @@ -466,85 +162,37 @@ pub const BenchmarkRunner = struct { preset: []const u8, scenario: Scenario, render_distance: i32, - horizon_distance: i32, duration_s: f32, world_seed: u64, build: BuildMetadata, provenance: BenchmarkProvenance, output_path: []const u8, - benchmark_lod_memory_budget_mb: u32 = 0, - require_gpu_candidates: u32 = 0, start_ms: i64, elapsed_s: f32 = 0, sampled_s: f32 = 0, warmup_s: f32 = 1.0, - /// Evidence snapshots compare two independently launched modes. Hold both - /// at the same fixed camera long enough for the requested horizon to load, - /// then require a full second with empty queues before capturing startup - /// totals. Without this floor, compact auto can be compared against an off - /// run captured during a transient early queue lull. - evidence_min_warmup_s: f32 = 10.0, settled_warmup_s: f32 = 0, - evidence_mode: bool = false, - require_compact: bool = false, warmup_ready: bool = false, - compact_ready: bool = false, - compact_submissions: u64 = 0, - warmup_gpu_candidate_count_max: u32 = 0, - startup_evidence: BenchmarkStartupEvidence = .{}, warmup_timed_out: bool = false, + evidence_mode: bool = false, scenario_elapsed_s: f32 = 0, - next_warmup_diagnostic_s: f32 = 5, samples: std.ArrayListUnmanaged(FrameSample) = .empty, - previous_lod_profiling: ?LODProfilingDisplay = null, - pub fn init( - allocator: std.mem.Allocator, - preset: []const u8, - scenario_name: []const u8, - render_distance: i32, - horizon_distance: i32, - duration_s: f32, - world_seed: u64, - build_mode: []const u8, - benchmark_world: []const u8, - benchmark_fixture: []const u8, - output_path: []const u8, - benchmark_lod_memory_budget_mb: u32, - require_gpu_candidates: u32, - ) !BenchmarkRunner { + pub fn init(allocator: std.mem.Allocator, preset: []const u8, scenario_name: []const u8, render_distance: i32, duration_s: f32, world_seed: u64, build_mode: []const u8, benchmark_world: []const u8, output_path: []const u8) !BenchmarkRunner { var runner = BenchmarkRunner{ .allocator = allocator, .preset = preset, .scenario = try Scenario.parse(scenario_name), .render_distance = render_distance, - .horizon_distance = horizon_distance, .duration_s = duration_s, .world_seed = world_seed, - .build = .{ - .mode = build_mode, - .world = benchmark_world, - .fixture = benchmark_fixture, - .horizon_distance = horizon_distance, - .benchmark_lod_memory_budget_mb = benchmark_lod_memory_budget_mb, - .benchmark_require_gpu_candidates = require_gpu_candidates, - }, + .build = .{ .mode = build_mode, .world = benchmark_world }, .provenance = provenanceFromEnvironment(), .output_path = output_path, - .benchmark_lod_memory_budget_mb = benchmark_lod_memory_budget_mb, - .require_gpu_candidates = require_gpu_candidates, .start_ms = nowMs(), - .elapsed_s = 0, - .sampled_s = 0, - .warmup_s = 1.0, .evidence_mode = environmentFlag("ZIGCRAFT_BENCHMARK_EVIDENCE"), - .require_compact = environmentFlag("ZIGCRAFT_BENCHMARK_REQUIRE_COMPACT"), - .samples = .empty, - .previous_lod_profiling = null, }; - - const estimate_frames = @max(@as(usize, 64), @as(usize, @intFromFloat(@ceil(duration_s * 120.0)))); - try runner.samples.ensureTotalCapacity(allocator, estimate_frames); + try runner.samples.ensureTotalCapacity(allocator, @max(@as(usize, 64), @as(usize, @intFromFloat(@ceil(duration_s * 120.0))))); return runner; } @@ -566,66 +214,27 @@ pub const BenchmarkRunner = struct { } pub fn recordFrame(self: *BenchmarkRunner, dt: f32, fps: f32, gpu: GpuTimingResults, world_stats: ?WorldStats, draw_calls: u32, gpu_memory_mb: f32) !void { + if (!std.math.isFinite(dt) or dt <= 0.0) return error.InvalidBenchmarkSample; self.elapsed_s += dt; - const lod_stats = if (world_stats) |ws| blk: { - if (ws.lod) |ls| break :blk ls; - break :blk null; - } else null; - const lod = lodFrameDelta(lod_stats, &self.previous_lod_profiling); - if (lod_stats) |stats| self.compact_submissions = @max(self.compact_submissions, stats.profiling.compact_submissions); - if (self.evidence_mode and !self.warmup_ready) { - const renderable_regions = if (world_stats) |ws| ws.lod_renderable_regions else 0; - const settled = if (world_stats) |ws| ws.chunks_rendered > 0 and ws.gen_queue == 0 and ws.mesh_queue == 0 and ws.upload_queue == 0 else false; - self.settled_warmup_s = if (settled) self.settled_warmup_s + dt else 0; - const compact_ready = !self.require_compact or compactEvidenceReady(lod_stats); - const gpu_requested = if (lod_stats) |stats| stats.profiling.gpu_culling_requested else false; - const current_candidates = if (lod_stats) |stats| stats.profiling.gpu_culling_candidate_count else 0; - const max_candidates = if (lod_stats) |stats| stats.profiling.gpu_culling_candidate_count_max else 0; - if (gpu_requested) self.warmup_gpu_candidate_count_max = @max(self.warmup_gpu_candidate_count_max, max_candidates); - // The resident-region readiness is deliberately shared by paired - // CPU/GPU runs. Candidate counters only exist when GPU culling is - // on, so using them for the CPU source would create an unequal - // warmup gate. - const regions_ready = renderable_regions >= self.require_gpu_candidates; - const candidates_ready = !gpu_requested or max_candidates >= self.require_gpu_candidates; - const gpu_compact_submitted = !gpu_requested or compactEvidenceReady(lod_stats); - if (self.elapsed_s >= self.next_warmup_diagnostic_s) { - const compact = if (lod_stats) |stats| stats.compact_pool_allocated_bytes else 0; - const compact_diag = if (lod_stats) |stats| stats.profiling else LODProfilingDisplay{}; - std.log.warn("BENCHMARK_WARMUP: elapsed={d:.1}s chunks={} lod_regions={}/{} gpu_candidates={}/{} queues={}/{}/{} compact_bytes={} settled={} compact_ready={} compact=selected:{} rejected:{} upload_fail:{} unavailable:{} draw_fail:{} submissions:{} recovery:{} disabled:{}", .{ self.elapsed_s, if (world_stats) |ws| ws.chunks_rendered else 0, renderable_regions, self.require_gpu_candidates, current_candidates, self.require_gpu_candidates, if (world_stats) |ws| ws.gen_queue else 0, if (world_stats) |ws| ws.mesh_queue else 0, if (world_stats) |ws| ws.upload_queue else 0, compact, settled, compact_ready, compact_diag.compact_selected, compact_diag.compact_build_rejected, compact_diag.compact_upload_failures, compact_diag.compact_draw_unavailable, compact_diag.compact_draw_failures, compact_diag.compact_submissions, compact_diag.compact_recoveries, compact_diag.compact_disabled }); - self.next_warmup_diagnostic_s += 5; - } - self.compact_ready = self.compact_ready or compact_ready; - if (self.elapsed_s >= self.evidence_min_warmup_s and self.settled_warmup_s >= 1.0 and compact_ready and regions_ready and candidates_ready and gpu_compact_submitted) { - self.warmup_ready = true; - if (lod_stats) |stats| self.startup_evidence = startupEvidence(stats, self.elapsed_s, renderable_regions); - } - if (self.elapsed_s > 90.0) self.warmup_timed_out = true; + if (!self.warmup_ready) { + const geometry_ready = if (world_stats) |stats| stats.chunks_rendered > 0 and stats.vertices_rendered > 0 else false; + const queues_settled = if (world_stats) |stats| stats.gen_queue == 0 and stats.mesh_queue == 0 and stats.upload_queue == 0 else false; + self.settled_warmup_s = if (geometry_ready and queues_settled) self.settled_warmup_s + dt else 0; + if (self.elapsed_s >= self.warmup_s and self.settled_warmup_s >= 1.0) self.warmup_ready = true; + if (wallElapsedSeconds(self) > 90.0) self.warmup_timed_out = true; return; } - if (self.elapsed_s < self.warmup_s) return; - - const shadow_avg = averageArray(&gpu.shadow_pass_ms); - const chunks_rendered = if (world_stats) |ws| ws.chunks_rendered else 0; - const vertices = if (world_stats) |ws| ws.vertices_rendered else 0; - const frame_fps = if (dt > 0.000001) 1.0 / dt else fps; - + const shadow_ms = averageArray(&gpu.shadow_pass_ms); try self.samples.append(self.allocator, .{ .cpu_ms = dt * 1000.0, - .fps = frame_fps, - .gpu_shadow_ms = shadow_avg, + .fps = if (dt > 0.000001) 1.0 / dt else fps, + .gpu_shadow_ms = shadow_ms, .gpu_opaque_ms = gpu.opaque_pass_ms, - .gpu_lod_terrain_ms = gpu.lod_terrain_pass_ms, - .gpu_lod_water_ms = gpu.lod_water_pass_ms, - .gpu_lod_compact_terrain_ms = gpu.lod_compact_terrain_pass_ms, - .gpu_lod_compact_water_ms = gpu.lod_compact_water_pass_ms, - .gpu_lod_culling_ms = gpu.lod_culling_compute_ms, .gpu_total_ms = gpu.total_gpu_ms, .draw_calls = draw_calls, - .vertices = vertices, - .chunks_rendered = chunks_rendered, + .vertices = if (world_stats) |stats| stats.vertices_rendered else 0, + .chunks_rendered = if (world_stats) |stats| stats.chunks_rendered else 0, .gpu_memory_mb = gpu_memory_mb, - .lod = lod, }); self.sampled_s += dt; self.scenario_elapsed_s += dt; @@ -637,213 +246,51 @@ pub const BenchmarkRunner = struct { pub fn writeResults(self: *const BenchmarkRunner) !void { const results = try self.makeResults(); + try validateResults(results); const json = try results_json(results, self.allocator); defer self.allocator.free(json); - - if (fs.path.dirname(self.output_path)) |dir| { - try fs.cwd().makePath(dir); - } - + if (fs.path.dirname(self.output_path)) |dir| try fs.cwd().makePath(dir); var file = try fs.cwd().createFile(self.output_path, .{ .truncate = true }); defer file.close(); try file.writeAll(json); - - try enforceSlo(results); } pub fn makeResults(self: *const BenchmarkRunner) !BenchmarkResults { - const fps_values = try self.collectField(fpsField); - defer self.allocator.free(fps_values); - const frame_ms_values = try self.collectField(frameMsField); - defer self.allocator.free(frame_ms_values); - const lod_cpu_values = try self.collectLodCpuValues(); - defer self.allocator.free(lod_cpu_values); - const gpu_total_values = try self.collectField(gpuTotalField); - defer self.allocator.free(gpu_total_values); - const lod_gpu_values = try self.collectField(lodGpuField); - defer self.allocator.free(lod_gpu_values); - const compact_terrain_values = try self.collectField(compactTerrainField); - defer self.allocator.free(compact_terrain_values); - const compact_water_values = try self.collectField(compactWaterField); - defer self.allocator.free(compact_water_values); - const culling_values = try self.collectField(cullingField); - defer self.allocator.free(culling_values); - const logical_ram_values = try self.collectLodBytes(logicalRamBytes); - defer self.allocator.free(logical_ram_values); - const logical_vram_values = try self.collectLodBytes(logicalVramBytes); - defer self.allocator.free(logical_vram_values); + const fps = try self.collect(fpsField); + defer self.allocator.free(fps); + const frame_ms = try self.collect(frameMsField); + defer self.allocator.free(frame_ms); + const gpu_total = try self.collect(gpuTotalField); + defer self.allocator.free(gpu_total); var cpu_sum: f64 = 0; var shadow_sum: f64 = 0; var opaque_sum: f64 = 0; - var lod_terrain_sum: f64 = 0; - var lod_water_sum: f64 = 0; - var lod_culling_sum: f64 = 0; - var total_sum: f64 = 0; + var gpu_sum: f64 = 0; var draw_sum: f64 = 0; var vertices_sum: f64 = 0; var chunks_sum: f64 = 0; var memory_sum: f64 = 0; var memory_max: f64 = 0; - var max_frame_ms: f64 = 0; - var lod_profiling_enabled = false; - var lod_profiling_frame_count: u32 = 0; - var lod_scheduling_ms: f64 = 0; - var lod_cache_ms: f64 = 0; - var lod_generation_dispatch_ms: f64 = 0; - var lod_state_transition_ms: f64 = 0; - var lod_upload_prep_ms: f64 = 0; - var lod_upload_submission_ms: f64 = 0; - var lod_visibility_ms: f64 = 0; - var lod_coverage_ms: f64 = 0; - var lod_eviction_ms: f64 = 0; - var lod_worker_generation_ms: f64 = 0; - var lod_worker_mesh_construction_ms: f64 = 0; - var lod_worker_far_expanded_mesh_construction_ms: f64 = 0; - var lod_worker_compact_encode_ms: f64 = 0; - var lod_manager_lock_wait_ms: f64 = 0; - var lod_manager_lock_hold_ms: f64 = 0; - var lod_upload_bytes: u64 = 0; - var lod_far_expanded_upload_bytes: u64 = 0; - var lod_compact_upload_bytes: u64 = 0; - var lod_pending_cpu_upload_bytes = ByteGaugeAccumulator{}; - var lod_deferred_deletion_gpu_bytes = ByteGaugeAccumulator{}; - var lod_deferred_deletion_cpu_bytes = ByteGaugeAccumulator{}; - var lod_pool_gpu_capacity_bytes = ByteGaugeAccumulator{}; - var lod_pool_gpu_allocated_bytes = ByteGaugeAccumulator{}; - var lod_pool_gpu_slack_bytes = ByteGaugeAccumulator{}; - var lod_pool_cpu_shadow_bytes = ByteGaugeAccumulator{}; - var lod_compact_pool_capacity_bytes = ByteGaugeAccumulator{}; - var lod_compact_pool_allocated_bytes = ByteGaugeAccumulator{}; - var lod_compact_pool_free_bytes = ByteGaugeAccumulator{}; - var lod_compact_pool_retired_bytes = ByteGaugeAccumulator{}; - var lod_direct_mesh_gpu_bytes = ByteGaugeAccumulator{}; - var lod_source_data_cpu_bytes = ByteGaugeAccumulator{}; - var lod_known_memory_bytes = ByteGaugeAccumulator{}; - var lod_staging_pressure_count: u64 = 0; - var lod_visible_count: u64 = 0; - var lod_rejected_count: u64 = 0; - var lod_coverage_count: u64 = 0; - var lod_visibility_levels: [LOD_VISIBILITY_LEVEL_COUNT]LODVisibilityLevelDisplay = [_]LODVisibilityLevelDisplay{.{}} ** LOD_VISIBILITY_LEVEL_COUNT; - var lod_wait_idle_count: u64 = 0; - var lod_wait_idle_ms: f64 = 0; - var lod_gpu_culling_overflows_max: u32 = 0; - var lod_gpu_culling_validation_mismatches_max: u32 = 0; - var lod_gpu_culling_requested = false; - var lod_gpu_culling_threshold: u32 = 0; - var lod_gpu_culling_candidate_count_total: u64 = 0; - var lod_gpu_culling_candidate_count_max: u32 = self.warmup_gpu_candidate_count_max; - var lod_gpu_culling_draw_submission_count_total: u64 = 0; - var lod_gpu_culling_validation_generation: u64 = 0; - var lod_gpu_culling_validation_completed_generation: u64 = 0; - var lod_gpu_culling_validation_completed_count: u64 = 0; - var worst_frame = WorstFrame{ - .frame_index = 0, - .frame_ms = 0, - .gpu_total_ms = 0, - .gpu_lod_terrain_ms = 0, - .gpu_lod_water_ms = 0, - .gpu_lod_culling_ms = 0, - .dominant_gpu_pass = "none", - .dominant_gpu_pass_ms = 0, - .lod_cpu_ms = 0, - .dominant_lod_cpu_category = "none", - .dominant_lod_cpu_category_ms = 0, - .lod_worker_generation_ms = 0, - .lod_worker_mesh_construction_ms = 0, - .lod_upload_bytes = 0, - .lod_pending_cpu_upload_bytes = 0, - .lod_deferred_deletion_bytes = 0, - .lod_visible_count = 0, - .lod_rejected_count = 0, - .lod_coverage_count = 0, - .lod_wait_idle_count = 0, - .lod_wait_idle_ms = 0, - .lod_staging_pressure_count = 0, - }; - + var worst = WorstFrame{ .frame_index = 0, .frame_ms = 0, .gpu_total_ms = 0, .dominant_gpu_pass = "none", .dominant_gpu_pass_ms = 0 }; for (self.samples.items, 0..) |sample, index| { cpu_sum += sample.cpu_ms; - if (sample.cpu_ms > max_frame_ms) { - max_frame_ms = sample.cpu_ms; - worst_frame = worstFrameForSample(index, sample); - } shadow_sum += sample.gpu_shadow_ms; opaque_sum += sample.gpu_opaque_ms; - lod_terrain_sum += sample.gpu_lod_terrain_ms; - lod_water_sum += sample.gpu_lod_water_ms; - lod_culling_sum += sample.gpu_lod_culling_ms; - total_sum += sample.gpu_total_ms; + gpu_sum += sample.gpu_total_ms; draw_sum += @floatFromInt(sample.draw_calls); vertices_sum += @floatFromInt(sample.vertices); chunks_sum += @floatFromInt(sample.chunks_rendered); memory_sum += sample.gpu_memory_mb; memory_max = @max(memory_max, sample.gpu_memory_mb); - - const lod = sample.lod; - lod_profiling_enabled = lod_profiling_enabled or lod.enabled; - if (lod.enabled) lod_profiling_frame_count +|= 1; - lod_scheduling_ms += lod.scheduling_ms; - lod_cache_ms += lod.cache_ms; - lod_generation_dispatch_ms += lod.generation_dispatch_ms; - lod_state_transition_ms += lod.state_transition_ms; - lod_upload_prep_ms += lod.upload_prep_ms; - lod_upload_submission_ms += lod.upload_submission_ms; - lod_visibility_ms += lod.visibility_ms; - lod_coverage_ms += lod.coverage_ms; - lod_eviction_ms += lod.eviction_ms; - lod_worker_generation_ms += lod.worker_generation_ms; - lod_worker_mesh_construction_ms += lod.worker_mesh_construction_ms; - lod_worker_far_expanded_mesh_construction_ms += lod.worker_far_expanded_mesh_construction_ms; - lod_worker_compact_encode_ms += lod.worker_compact_encode_ms; - lod_manager_lock_wait_ms += lod.manager_lock_wait_ms; - lod_manager_lock_hold_ms += lod.manager_lock_hold_ms; - lod_upload_bytes +|= lod.upload_bytes; - lod_far_expanded_upload_bytes +|= lod.far_expanded_upload_bytes; - lod_compact_upload_bytes +|= lod.compact_upload_bytes; - lod_pending_cpu_upload_bytes.add(lod.pending_cpu_upload_bytes); - lod_deferred_deletion_gpu_bytes.add(lod.deferred_deletion_bytes); - lod_deferred_deletion_cpu_bytes.add(lod.deferred_deletion_cpu_bytes); - lod_pool_gpu_capacity_bytes.add(lod.pool_gpu_capacity_bytes); - lod_pool_gpu_allocated_bytes.add(lod.pool_gpu_allocated_bytes); - lod_pool_gpu_slack_bytes.add(lod.pool_gpu_slack_bytes); - lod_pool_cpu_shadow_bytes.add(lod.pool_cpu_shadow_bytes); - lod_compact_pool_capacity_bytes.add(lod.compact_pool_capacity_bytes); - lod_compact_pool_allocated_bytes.add(lod.compact_pool_allocated_bytes); - lod_compact_pool_free_bytes.add(lod.compact_pool_free_bytes); - lod_compact_pool_retired_bytes.add(lod.compact_pool_retired_bytes); - lod_direct_mesh_gpu_bytes.add(lod.direct_mesh_gpu_bytes); - lod_source_data_cpu_bytes.add(lod.source_data_cpu_bytes); - lod_known_memory_bytes.add(lod.known_memory_bytes); - lod_staging_pressure_count +|= lod.staging_pressure_count; - lod_visible_count +|= lod.visible_count; - lod_rejected_count +|= lod.rejected_count; - lod_coverage_count +|= lod.coverage_count; - for (&lod_visibility_levels, lod.visibility_levels) |*total, level| addVisibilityLevel(total, level); - lod_wait_idle_count +|= lod.wait_idle_count; - lod_wait_idle_ms += lod.wait_idle_ms; - lod_gpu_culling_overflows_max = @max(lod_gpu_culling_overflows_max, lod.gpu_culling_overflows); - lod_gpu_culling_validation_mismatches_max = @max(lod_gpu_culling_validation_mismatches_max, lod.gpu_culling_validation_mismatches); - lod_gpu_culling_requested = lod_gpu_culling_requested or lod.gpu_culling_requested; - lod_gpu_culling_threshold = @max(lod_gpu_culling_threshold, lod.gpu_culling_threshold); - lod_gpu_culling_candidate_count_total +|= lod.gpu_culling_candidate_count; - lod_gpu_culling_candidate_count_max = @max(lod_gpu_culling_candidate_count_max, lod.gpu_culling_candidate_count_max); - lod_gpu_culling_draw_submission_count_total +|= lod.gpu_culling_draw_submissions; - lod_gpu_culling_validation_generation = @max(lod_gpu_culling_validation_generation, lod.gpu_culling_validation_generation); - lod_gpu_culling_validation_completed_generation = @max(lod_gpu_culling_validation_completed_generation, lod.gpu_culling_validation_completed_generation); - lod_gpu_culling_validation_completed_count = @max(lod_gpu_culling_validation_completed_count, lod.gpu_culling_validation_completed_count); + if (sample.cpu_ms > worst.frame_ms) { + const opaque_is_dominant = sample.gpu_opaque_ms >= sample.gpu_shadow_ms; + worst = .{ .frame_index = @intCast(index), .frame_ms = sample.cpu_ms, .gpu_total_ms = sample.gpu_total_ms, .dominant_gpu_pass = if (opaque_is_dominant) "opaque" else "shadow", .dominant_gpu_pass_ms = if (opaque_is_dominant) sample.gpu_opaque_ms else sample.gpu_shadow_ms }; + } } - const count = @as(f64, @floatFromInt(@max(self.samples.items.len, 1))); - var fps_summary = try summarizeSeries(self.allocator, fps_values); - const frame_ms_summary = try summarizeSeries(self.allocator, frame_ms_values); - const lod_cpu_summary = try summarizeSeries(self.allocator, lod_cpu_values); - const lod_gpu_summary = try summarizeSeries(self.allocator, lod_gpu_values); - const sampled_s = cpu_sum / 1000.0; - if (sampled_s > 0.0) { - fps_summary.avg = @as(f64, @floatFromInt(self.samples.items.len)) / sampled_s; - } - + var fps_summary = try summarizeSeries(self.allocator, fps); + if (cpu_sum > 0) fps_summary.avg = @as(f64, @floatFromInt(self.samples.items.len)) / (cpu_sum / 1000.0); return .{ .preset = self.preset, .scenario = self.scenario.name(), @@ -851,202 +298,48 @@ pub const BenchmarkRunner = struct { .build = self.build, .provenance = self.provenance, .render_distance = self.render_distance, - .horizon_distance = self.horizon_distance, .gpu_memory_mb_avg = memory_sum / count, .gpu_memory_mb_max = memory_max, .frames = @intCast(self.samples.items.len), .duration_s = self.duration_s, .fps = fps_summary, - .frame_ms = frame_ms_summary, - .max_frame_ms = max_frame_ms, + .frame_ms = try summarizeSeries(self.allocator, frame_ms), + .max_frame_ms = worst.frame_ms, .cpu_ms_avg = cpu_sum / count, - .gpu_ms = .{ - .shadow_avg = shadow_sum / count, - .opaque_avg = opaque_sum / count, - .lod_terrain_avg = lod_terrain_sum / count, - .lod_water_avg = lod_water_sum / count, - .lod_culling_avg = lod_culling_sum / count, - .total_avg = total_sum / count, - .total = try summarizeSeries(self.allocator, gpu_total_values), - .lod_compact_terrain = try summarizeSeries(self.allocator, compact_terrain_values), - .lod_compact_water = try summarizeSeries(self.allocator, compact_water_values), - .lod_culling = try summarizeSeries(self.allocator, culling_values), - }, + .gpu_ms = .{ .shadow_avg = shadow_sum / count, .opaque_avg = opaque_sum / count, .total_avg = gpu_sum / count, .total = try summarizeSeries(self.allocator, gpu_total) }, .draw_calls_avg = draw_sum / count, .vertices_avg = vertices_sum / count, .chunks_rendered_avg = chunks_sum / count, - .worst_frame = worst_frame, - .lod = .{ - .profiling_enabled = lod_profiling_enabled, - .profiling_frame_count = lod_profiling_frame_count, - .cpu_frame_ms = lod_cpu_summary, - .gpu_frame_ms = lod_gpu_summary, - .cpu_categories = .{ - .scheduling = timingTotalAverage(lod_scheduling_ms, count), - .cache = timingTotalAverage(lod_cache_ms, count), - .generation_dispatch = timingTotalAverage(lod_generation_dispatch_ms, count), - .state_transition = timingTotalAverage(lod_state_transition_ms, count), - .upload_prep = timingTotalAverage(lod_upload_prep_ms, count), - .upload_submission = timingTotalAverage(lod_upload_submission_ms, count), - .visibility = timingTotalAverage(lod_visibility_ms, count), - .coverage = timingTotalAverage(lod_coverage_ms, count), - .eviction = timingTotalAverage(lod_eviction_ms, count), - .manager_lock_wait = timingTotalAverage(lod_manager_lock_wait_ms, count), - .manager_lock_hold = timingTotalAverage(lod_manager_lock_hold_ms, count), - }, - .workers = .{ - .generation_total_ms = lod_worker_generation_ms, - .mesh_construction_total_ms = lod_worker_mesh_construction_ms, - .far_expanded_mesh_construction_total_ms = lod_worker_far_expanded_mesh_construction_ms, - .compact_encode_total_ms = lod_worker_compact_encode_ms, - }, - .memory_bytes = .{ - .upload_total_bytes = lod_upload_bytes, - .upload_avg_bytes = @as(f64, @floatFromInt(lod_upload_bytes)) / count, - .far_expanded_upload_total_bytes = lod_far_expanded_upload_bytes, - .compact_upload_total_bytes = lod_compact_upload_bytes, - .pending_cpu_upload_bytes = lod_pending_cpu_upload_bytes.summarize(count), - .deferred_deletion_bytes = lod_deferred_deletion_gpu_bytes.summarize(count), - .deferred_deletion_gpu_bytes = lod_deferred_deletion_gpu_bytes.summarize(count), - .deferred_deletion_cpu_bytes = lod_deferred_deletion_cpu_bytes.summarize(count), - .pool_gpu_capacity_bytes = lod_pool_gpu_capacity_bytes.summarize(count), - .pool_gpu_allocated_bytes = lod_pool_gpu_allocated_bytes.summarize(count), - .pool_gpu_slack_bytes = lod_pool_gpu_slack_bytes.summarize(count), - .pool_cpu_shadow_bytes = lod_pool_cpu_shadow_bytes.summarize(count), - .compact_pool_capacity_bytes = lod_compact_pool_capacity_bytes.summarize(count), - .compact_pool_allocated_bytes = lod_compact_pool_allocated_bytes.summarize(count), - .compact_pool_free_bytes = lod_compact_pool_free_bytes.summarize(count), - .compact_pool_retired_bytes = lod_compact_pool_retired_bytes.summarize(count), - .direct_mesh_gpu_bytes = lod_direct_mesh_gpu_bytes.summarize(count), - .source_data_cpu_bytes = lod_source_data_cpu_bytes.summarize(count), - .known_memory_bytes = lod_known_memory_bytes.summarize(count), - .logical_ram_bytes = try ByteGaugeAccumulator.summarizeSamples(self.allocator, logical_ram_values), - .logical_vram_bytes = try ByteGaugeAccumulator.summarizeSamples(self.allocator, logical_vram_values), - }, - .visibility = .{ - .visible_total = lod_visible_count, - .rejected_total = lod_rejected_count, - .coverage_total = lod_coverage_count, - .levels = lod_visibility_levels, - }, - .pressure = .{ - .staging_pressure_total = lod_staging_pressure_count, - .wait_idle_count_total = lod_wait_idle_count, - .wait_idle_ms_total = lod_wait_idle_ms, - .wait_idle_ms_avg = lod_wait_idle_ms / count, - .gpu_culling_overflows_max = lod_gpu_culling_overflows_max, - .gpu_culling_validation_mismatches_max = lod_gpu_culling_validation_mismatches_max, - }, - .gpu_culling = .{ - .requested = lod_gpu_culling_requested, - .threshold = lod_gpu_culling_threshold, - .candidate_count_total = lod_gpu_culling_candidate_count_total, - .candidate_count_max = lod_gpu_culling_candidate_count_max, - .draw_submission_count_total = lod_gpu_culling_draw_submission_count_total, - .validation_generation = lod_gpu_culling_validation_generation, - .validation_completed_generation = lod_gpu_culling_validation_completed_generation, - .validation_completed_count = lod_gpu_culling_validation_completed_count, - }, - .compact_submissions = self.compact_submissions, - }, - .startup_evidence = self.startup_evidence, - .completion = .{ .scenario_completed = self.sampled_s >= self.duration_s, .sampled_duration_s = self.sampled_s, .requested_duration_s = self.duration_s, .sampled_frame_count = @intCast(self.samples.items.len), .evidence_mode = self.evidence_mode, .warmup_ready = !self.evidence_mode or self.warmup_ready, .compact_ready = !self.require_compact or self.compact_ready, .warmup_timed_out = self.warmup_timed_out, .lod_renderable_region_target = self.require_gpu_candidates, .gpu_candidate_target = self.require_gpu_candidates }, + .worst_frame = worst, + .completion = .{ .scenario_completed = self.sampled_s >= self.duration_s and !self.warmup_timed_out, .sampled_duration_s = self.sampled_s, .requested_duration_s = self.duration_s, .sampled_frame_count = @intCast(self.samples.items.len), .warmup_ready = self.warmup_ready, .warmup_timed_out = self.warmup_timed_out, .evidence_mode = self.evidence_mode }, }; } - fn collectField(self: *const BenchmarkRunner, comptime getter: fn (FrameSample) f32) ![]f32 { + fn collect(self: *const BenchmarkRunner, comptime getter: fn (FrameSample) f32) ![]f32 { var values = try self.allocator.alloc(f32, self.samples.items.len); - for (self.samples.items, 0..) |sample, i| { - values[i] = getter(sample); - } + for (self.samples.items, 0..) |sample, index| values[index] = getter(sample); return values; } - - fn collectLodCpuValues(self: *const BenchmarkRunner) ![]f32 { - var values = try self.allocator.alloc(f32, self.samples.items.len); - for (self.samples.items, 0..) |sample, i| { - values[i] = @floatCast(sample.lod.cpu_ms); - } - return values; - } - - fn collectLodBytes(self: *const BenchmarkRunner, comptime getter: fn (LODFrameSample) u64) ![]u64 { - var values = try self.allocator.alloc(u64, self.samples.items.len); - for (self.samples.items, 0..) |sample, i| values[i] = getter(sample.lod); - return values; - } - - fn results_json(results: BenchmarkResults, allocator: std.mem.Allocator) ![]u8 { - return try std.json.Stringify.valueAlloc(allocator, results, .{ .whitespace = .indent_2 }); - } }; -fn nowMs() i64 { - return std.Io.Clock.real.now(std.Options.debug_io).toMilliseconds(); -} - -fn environmentFlag(name: [:0]const u8) bool { - const raw = std.c.getenv(name) orelse return false; - const value = std.mem.span(raw); - return value.len > 0 and - !std.mem.eql(u8, value, "0") and - !std.ascii.eqlIgnoreCase(value, "false") and - !std.ascii.eqlIgnoreCase(value, "off"); -} - -fn environmentLabel(name: [:0]const u8) []const u8 { - const raw = std.c.getenv(name) orelse return "unknown"; - const value = std.mem.span(raw); - return if (value.len == 0) "unknown" else value; -} - -fn provenanceFromEnvironment() BenchmarkProvenance { - return .{ - .gpu_adapter = environmentLabel("ZIGCRAFT_BENCHMARK_GPU_ADAPTER"), - .gpu_driver = environmentLabel("ZIGCRAFT_BENCHMARK_GPU_DRIVER"), - .runner = environmentLabel("ZIGCRAFT_BENCHMARK_RUNNER"), - .zig_toolchain = environmentLabel("ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN"), - }; -} - -fn startupEvidence(stats: LODStatsDisplay, elapsed_s: f32, lod_renderable_regions: u64) BenchmarkStartupEvidence { - return .{ - .readiness_observed = true, - .readiness_elapsed_s = elapsed_s, - .upload_total_bytes = stats.profiling.upload_bytes, - .far_expanded_upload_bytes = stats.profiling.far_expanded_upload_bytes, - .compact_upload_bytes = stats.profiling.compact_upload_bytes, - .worker_generation_total_ms = stats.profiling.worker_generation_ms, - .worker_mesh_construction_total_ms = stats.profiling.worker_mesh_construction_ms, - .worker_far_expanded_mesh_construction_ms = stats.profiling.worker_far_expanded_mesh_construction_ms, - .worker_compact_encode_ms = stats.profiling.worker_compact_encode_ms, - .compact_submissions = stats.profiling.compact_submissions, - .pool_gpu_allocated_bytes = stats.pool_gpu_allocated_bytes, - .direct_mesh_gpu_bytes = stats.direct_mesh_gpu_bytes, - .compact_pool_allocated_bytes = stats.compact_pool_allocated_bytes, - .pool_cpu_shadow_bytes = stats.pool_cpu_shadow_bytes, - .lod_renderable_regions = lod_renderable_regions, - .gpu_culling_candidate_count = stats.profiling.gpu_culling_candidate_count, - }; -} - -fn wallElapsedSeconds(self: *const BenchmarkRunner) f32 { - const elapsed_ms = @max(@as(i64, 0), nowMs() - self.start_ms); - return @as(f32, @floatFromInt(elapsed_ms)) / 1000.0; -} - -fn compactEvidenceReady(lod_stats: ?LODStatsDisplay) bool { - const stats = lod_stats orelse return false; - return stats.compact_pool_allocated_bytes > 0 and stats.profiling.compact_submissions > 0; -} - pub fn thresholdsForPreset(preset: []const u8) SloThresholds { if (std.ascii.eqlIgnoreCase(preset, "low")) return .{ .fps_p1_min = 12, .max_frame_ms = 260, .draw_calls_max = 700, .vertices_max = 3_500_000, .gpu_memory_mb_max = 2200 }; if (std.ascii.eqlIgnoreCase(preset, "medium")) return .{ .fps_p1_min = 8, .max_frame_ms = 260, .draw_calls_max = 2600, .vertices_max = 6_000_000, .gpu_memory_mb_max = 2400 }; if (std.ascii.eqlIgnoreCase(preset, "high")) return .{ .fps_p1_min = 6, .max_frame_ms = 260, .draw_calls_max = 3600, .vertices_max = 8_500_000, .gpu_memory_mb_max = 2800 }; if (std.ascii.eqlIgnoreCase(preset, "ultra")) return .{ .fps_p1_min = 4, .max_frame_ms = 260, .draw_calls_max = 4500, .vertices_max = 12_000_000, .gpu_memory_mb_max = 3400 }; - if (std.ascii.eqlIgnoreCase(preset, "extreme")) return .{ .fps_p1_min = 3, .max_frame_ms = 260, .draw_calls_max = 5500, .vertices_max = 16_000_000, .gpu_memory_mb_max = 4096 }; - return .{ .fps_p1_min = 6, .max_frame_ms = 260, .draw_calls_max = 3600, .vertices_max = 8_500_000, .gpu_memory_mb_max = 2800 }; + return .{ .fps_p1_min = 3, .max_frame_ms = 260, .draw_calls_max = 5500, .vertices_max = 16_000_000, .gpu_memory_mb_max = 4096 }; +} + +pub fn validateResults(results: BenchmarkResults) !void { + const completion = results.completion; + if (!completion.scenario_completed or !completion.warmup_ready or completion.warmup_timed_out or completion.sampled_frame_count == 0 or completion.sampled_duration_s < completion.requested_duration_s) return error.IncompleteBenchmarkScenario; + if (results.frames != completion.sampled_frame_count or !std.math.isFinite(results.duration_s) or results.duration_s <= 0.0) return error.IncompleteBenchmarkScenario; + inline for (.{ results.fps.min, results.fps.avg, results.fps.max, results.fps.p1, results.fps.p5, results.fps.p50, results.fps.p95, results.fps.p99, results.frame_ms.min, results.frame_ms.avg, results.frame_ms.max, results.frame_ms.p1, results.frame_ms.p5, results.frame_ms.p50, results.frame_ms.p95, results.frame_ms.p99, results.max_frame_ms, results.cpu_ms_avg, results.gpu_ms.shadow_avg, results.gpu_ms.opaque_avg, results.gpu_ms.total_avg, results.gpu_ms.total.min, results.gpu_ms.total.avg, results.gpu_ms.total.max, results.draw_calls_avg, results.vertices_avg, results.chunks_rendered_avg, results.gpu_memory_mb_avg, results.gpu_memory_mb_max }) |value| { + if (!std.math.isFinite(value)) return error.NonFiniteBenchmarkResult; + } + if (completion.evidence_mode and !validProvenance(results.provenance)) return error.MissingBenchmarkProvenance; + const thresholds = thresholdsForResults(results); + if (results.fps.p1 < thresholds.fps_p1_min or results.max_frame_ms > thresholds.max_frame_ms or results.draw_calls_avg > thresholds.draw_calls_max or results.vertices_avg > thresholds.vertices_max or results.gpu_memory_mb_max > thresholds.gpu_memory_mb_max) return error.BenchmarkSloBreach; } fn baselineRenderDistanceForPreset(preset: []const u8) i32 { @@ -1054,8 +347,7 @@ fn baselineRenderDistanceForPreset(preset: []const u8) i32 { if (std.ascii.eqlIgnoreCase(preset, "medium")) return 10; if (std.ascii.eqlIgnoreCase(preset, "high")) return 12; if (std.ascii.eqlIgnoreCase(preset, "ultra")) return 14; - if (std.ascii.eqlIgnoreCase(preset, "extreme")) return 16; - return 12; + return 16; } fn thresholdsForResults(results: BenchmarkResults) SloThresholds { @@ -1068,1000 +360,134 @@ fn thresholdsForResults(results: BenchmarkResults) SloThresholds { return thresholds; } -pub fn validateResults(results: BenchmarkResults) !void { - if (!results.completion.scenario_completed or results.completion.sampled_duration_s < results.completion.requested_duration_s or results.completion.sampled_frame_count == 0) return error.IncompleteBenchmarkScenario; - if (!results.completion.warmup_ready or results.completion.warmup_timed_out) return error.BenchmarkWarmupNotReady; - if (!results.completion.compact_ready) return error.CompactWarmupNotReady; - if (results.completion.lod_renderable_region_target > 0) { - if (results.startup_evidence.lod_renderable_regions < results.completion.lod_renderable_region_target) return error.LodReadinessTargetNotMet; - if (results.build.benchmark_require_gpu_candidates != results.completion.lod_renderable_region_target or results.completion.gpu_candidate_target != results.completion.lod_renderable_region_target) return error.BenchmarkReadinessTargetMismatch; - if (results.lod.gpu_culling.requested and results.lod.gpu_culling.candidate_count_max < results.completion.gpu_candidate_target) return error.GpuCandidateTargetNotMet; - } - if (results.completion.evidence_mode) { - if (!results.startup_evidence.readiness_observed) return error.MissingStartupEvidence; - inline for (.{ results.provenance.gpu_adapter, results.provenance.gpu_driver, results.provenance.runner, results.provenance.zig_toolchain }) |label| { - if (label.len == 0 or std.ascii.eqlIgnoreCase(label, "unknown")) return error.MissingBenchmarkProvenance; - } - } - if (!results.lod.profiling_enabled or results.lod.profiling_frame_count != results.completion.sampled_frame_count) return error.MissingLodProfiling; - if (results.lod.pressure.wait_idle_count_total != 0) return error.StreamingWaitIdleObserved; - if (results.lod.pressure.gpu_culling_overflows_max != 0) return error.GpuCullingOverflowObserved; - if (results.lod.pressure.gpu_culling_validation_mismatches_max != 0) return error.GpuCullingMismatchObserved; - const logical = results.lod.memory_bytes; - if (logical.logical_ram_bytes.max_bytes == 0 or logical.logical_vram_bytes.max_bytes == 0) return error.MissingLogicalLodMemoryEvidence; - const scenario = Scenario.parse(results.scenario) catch return error.InvalidBenchmarkScenario; - const budget = lodBudgetFor(results.preset, scenario) orelse return error.MissingLodBudget; - if (results.lod.cpu_frame_ms.p95 > budget.cpu_p95_ms or results.lod.cpu_frame_ms.p99 > budget.cpu_p99_ms) return error.LodCpuBudgetBreach; - if (results.lod.gpu_frame_ms.p95 > budget.gpu_p95_ms or results.lod.gpu_frame_ms.p99 > budget.gpu_p99_ms) return error.LodGpuBudgetBreach; - if (logical.logical_ram_bytes.p95_bytes > budget.logical_ram_p95_bytes or logical.logical_ram_bytes.p99_bytes > budget.logical_ram_p99_bytes) return error.LodLogicalRamBudgetBreach; - if (logical.logical_vram_bytes.p95_bytes > budget.logical_vram_p95_bytes or logical.logical_vram_bytes.p99_bytes > budget.logical_vram_p99_bytes) return error.LodLogicalVramBudgetBreach; - const thresholds = thresholdsForResults(results); - var failed = false; - - if (results.fps.p1 < thresholds.fps_p1_min) { - std.log.err("benchmark SLO breach: {s} p1 FPS {d:.2} < {d:.2}", .{ results.preset, results.fps.p1, thresholds.fps_p1_min }); - failed = true; - } - if (results.max_frame_ms > thresholds.max_frame_ms) { - std.log.err("benchmark SLO breach: {s} max frame {d:.2}ms > {d:.2}ms", .{ results.preset, results.max_frame_ms, thresholds.max_frame_ms }); - failed = true; - } - if (results.draw_calls_avg > thresholds.draw_calls_max) { - std.log.err("benchmark SLO breach: {s} draw calls avg {d:.2} > {d:.2}", .{ results.preset, results.draw_calls_avg, thresholds.draw_calls_max }); - failed = true; - } - if (results.vertices_avg > thresholds.vertices_max) { - std.log.err("benchmark SLO breach: {s} vertices avg {d:.2} > {d:.2}", .{ results.preset, results.vertices_avg, thresholds.vertices_max }); - failed = true; - } - if (results.gpu_memory_mb_max > thresholds.gpu_memory_mb_max) { - std.log.err("benchmark SLO breach: {s} GPU memory max {d:.2}MB > {d:.2}MB", .{ results.preset, results.gpu_memory_mb_max, thresholds.gpu_memory_mb_max }); - failed = true; - } - - if (failed) return error.BenchmarkSloBreach; -} -fn enforceSlo(results: BenchmarkResults) !void { - return validateResults(results); -} - -test "benchmark draw-call SLO scales for render-distance override" { - const base = thresholdsForPreset("medium"); - const results = BenchmarkResults{ - .preset = "medium", - .scenario = "traversal", - .world_seed = BENCHMARK_WORLD_SEED, - .build = .{ .mode = "Debug", .horizon_distance = 512 }, - .render_distance = 22, - .horizon_distance = 512, - .gpu_memory_mb_avg = 0, - .gpu_memory_mb_max = 0, - .frames = 0, - .duration_s = 0, - .fps = .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }, - .frame_ms = .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }, - .max_frame_ms = 0, - .cpu_ms_avg = 0, - .gpu_ms = .{ .shadow_avg = 0, .opaque_avg = 0, .lod_terrain_avg = 0, .lod_water_avg = 0, .lod_culling_avg = 0, .total_avg = 0 }, - .draw_calls_avg = 0, - .vertices_avg = 0, - .chunks_rendered_avg = 0, - .worst_frame = .{ - .frame_index = 0, - .frame_ms = 0, - .gpu_total_ms = 0, - .gpu_lod_terrain_ms = 0, - .gpu_lod_water_ms = 0, - .gpu_lod_culling_ms = 0, - .dominant_gpu_pass = "none", - .dominant_gpu_pass_ms = 0, - .lod_cpu_ms = 0, - .dominant_lod_cpu_category = "none", - .dominant_lod_cpu_category_ms = 0, - .lod_worker_generation_ms = 0, - .lod_worker_mesh_construction_ms = 0, - .lod_upload_bytes = 0, - .lod_pending_cpu_upload_bytes = 0, - .lod_deferred_deletion_bytes = 0, - .lod_visible_count = 0, - .lod_rejected_count = 0, - .lod_coverage_count = 0, - .lod_wait_idle_count = 0, - .lod_wait_idle_ms = 0, - .lod_staging_pressure_count = 0, - }, - .lod = .{ - .profiling_enabled = false, - .cpu_frame_ms = .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }, - .cpu_categories = .{ - .scheduling = .{ .total_ms = 0, .avg_ms = 0 }, - .cache = .{ .total_ms = 0, .avg_ms = 0 }, - .generation_dispatch = .{ .total_ms = 0, .avg_ms = 0 }, - .state_transition = .{ .total_ms = 0, .avg_ms = 0 }, - .upload_prep = .{ .total_ms = 0, .avg_ms = 0 }, - .upload_submission = .{ .total_ms = 0, .avg_ms = 0 }, - .visibility = .{ .total_ms = 0, .avg_ms = 0 }, - .coverage = .{ .total_ms = 0, .avg_ms = 0 }, - .eviction = .{ .total_ms = 0, .avg_ms = 0 }, - .manager_lock_wait = .{ .total_ms = 0, .avg_ms = 0 }, - .manager_lock_hold = .{ .total_ms = 0, .avg_ms = 0 }, - }, - .workers = .{ .generation_total_ms = 0, .mesh_construction_total_ms = 0, .far_expanded_mesh_construction_total_ms = 0, .compact_encode_total_ms = 0 }, - .memory_bytes = .{ - .upload_total_bytes = 0, - .upload_avg_bytes = 0, - .far_expanded_upload_total_bytes = 0, - .compact_upload_total_bytes = 0, - .pending_cpu_upload_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .deferred_deletion_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .deferred_deletion_gpu_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .deferred_deletion_cpu_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .pool_gpu_capacity_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .pool_gpu_allocated_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .pool_gpu_slack_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .pool_cpu_shadow_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .compact_pool_capacity_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .compact_pool_allocated_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .compact_pool_free_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .compact_pool_retired_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .direct_mesh_gpu_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - .known_memory_bytes = .{ .avg_bytes = 0, .max_bytes = 0, .last_bytes = 0 }, - }, - .visibility = .{ .visible_total = 0, .rejected_total = 0, .coverage_total = 0, .levels = [_]LODVisibilityLevelDisplay{.{}} ** LOD_VISIBILITY_LEVEL_COUNT }, - .pressure = .{ .staging_pressure_total = 0, .wait_idle_count_total = 0, .wait_idle_ms_total = 0, .wait_idle_ms_avg = 0 }, - }, - }; - const adjusted = thresholdsForResults(results); - - try std.testing.expect(adjusted.draw_calls_max > base.draw_calls_max); - try std.testing.expectEqual(base.gpu_memory_mb_max, adjusted.gpu_memory_mb_max); - try std.testing.expectEqual(base.fps_p1_min, adjusted.fps_p1_min); -} - -test "benchmark reports LOD GPU timing, frame percentiles, and worst-frame attribution" { - var runner = try BenchmarkRunner.init(std.testing.allocator, "medium", "traversal", 12, 512, 1, BENCHMARK_WORLD_SEED, "Debug", "flat", "", "unused.json", 0, 0); - defer runner.deinit(); - - try runner.samples.append(std.testing.allocator, .{ - .cpu_ms = 10, - .fps = 100, - .gpu_shadow_ms = 1, - .gpu_opaque_ms = 2, - .gpu_lod_terrain_ms = 3, - .gpu_lod_water_ms = 4, - .gpu_lod_compact_terrain_ms = 0, - .gpu_lod_compact_water_ms = 0, - .gpu_lod_culling_ms = 2, - .gpu_total_ms = 8, - .draw_calls = 10, - .vertices = 100, - .chunks_rendered = 1, - .gpu_memory_mb = 100, - }); - try runner.samples.append(std.testing.allocator, .{ - .cpu_ms = 20, - .fps = 50, - .gpu_shadow_ms = 1, - .gpu_opaque_ms = 2, - .gpu_lod_terrain_ms = 9, - .gpu_lod_water_ms = 4, - .gpu_lod_compact_terrain_ms = 0, - .gpu_lod_compact_water_ms = 0, - .gpu_lod_culling_ms = 5, - .gpu_total_ms = 16, - .draw_calls = 20, - .vertices = 200, - .chunks_rendered = 2, - .gpu_memory_mb = 200, - }); - - const results = try runner.makeResults(); - try std.testing.expectApproxEqAbs(@as(f64, 15), results.frame_ms.p50, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 19.5), results.frame_ms.p95, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 8), results.gpu_ms.lod_terrain_avg, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 4), results.gpu_ms.lod_water_avg, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 3.5), results.gpu_ms.lod_culling_avg, 0.001); - try std.testing.expectEqual(@as(u32, 1), results.worst_frame.frame_index); - try std.testing.expectEqualStrings("lod_terrain", results.worst_frame.dominant_gpu_pass); - try std.testing.expectApproxEqAbs(@as(f64, 9), results.worst_frame.dominant_gpu_pass_ms, 0.001); - try std.testing.expectEqualStrings("traversal", results.scenario); - try std.testing.expectEqual(BENCHMARK_WORLD_SEED, results.world_seed); - try std.testing.expectEqualStrings("Debug", results.build.mode); +fn results_json(results: BenchmarkResults, allocator: std.mem.Allocator) ![]u8 { + return try std.json.Stringify.valueAlloc(allocator, results, .{ .whitespace = .indent_2 }); } -test "benchmark projects LOD profiling deltas and accepts cumulative counter resets" { - const Test = struct { - fn world(profiling: LODProfilingDisplay) WorldStats { - return .{ - .chunks_total = 0, - .chunks_rendered = 0, - .chunks_culled = 0, - .vertices_rendered = 0, - .gen_queue = 0, - .mesh_queue = 0, - .upload_queue = 0, - .lod = .{ - .loaded = .{ 0, 0, 0, 0, 0 }, - .memory_used_mb = 0, - .memory_used_bytes = profiling.known_memory_bytes, - .pool_gpu_capacity_bytes = profiling.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = profiling.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = profiling.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = profiling.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = profiling.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = profiling.compact_pool_allocated_bytes, - .compact_pool_free_bytes = profiling.compact_pool_free_bytes, - .compact_pool_retired_bytes = profiling.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = profiling.direct_mesh_gpu_bytes, - .pending_cpu_upload_bytes = profiling.pending_cpu_upload_bytes, - .deferred_deletion_gpu_bytes = profiling.deferred_deletion_bytes, - .deferred_deletion_cpu_bytes = profiling.deferred_deletion_cpu_bytes, - .profiling = profiling, - }, - }; - } - }; - - var runner = try BenchmarkRunner.init(std.testing.allocator, "medium", "traversal", 12, 512, 1, BENCHMARK_WORLD_SEED, "Debug", "flat", "", "unused.json", 0, 0); - defer runner.deinit(); - runner.warmup_s = 0; - const gpu = std.mem.zeroes(GpuTimingResults); - - try runner.recordFrame(0.01, 100, gpu, Test.world(.{ - .enabled = true, - .update_ms = 10, - .scheduling_ms = 5, - .cache_ms = 1, - .generation_dispatch_ms = 2, - .state_transition_ms = 1, - .upload_prep_ms = 1, - .upload_submission_ms = 1, - .visibility_ms = 1, - .coverage_ms = 1, - .eviction_ms = 1, - .worker_generation_ms = 8, - .worker_mesh_construction_ms = 4, - .worker_far_expanded_mesh_construction_ms = 2, - .worker_compact_encode_ms = 1, - .manager_lock_wait_ms = 2, - .manager_lock_hold_ms = 4, - .upload_bytes = 100, - .far_expanded_upload_bytes = 40, - .compact_upload_bytes = 10, - .pending_cpu_upload_bytes = 10, - .staging_pressure_count = 3, - .visible_count = 10, - .rejected_count = 4, - .coverage_count = 6, - .visibility_levels = .{ .{}, .{ .candidates = 5, .accepted = 2, .rejected_frustum = 1, .coverage_checks = 4 }, .{}, .{}, .{} }, - .deferred_deletion_bytes = 30, - .deferred_deletion_cpu_bytes = 3, - .pool_gpu_capacity_bytes = 100, - .pool_gpu_allocated_bytes = 80, - .pool_gpu_slack_bytes = 20, - .pool_cpu_shadow_bytes = 100, - .compact_pool_capacity_bytes = 40, - .compact_pool_allocated_bytes = 30, - .compact_pool_free_bytes = 10, - .compact_pool_retired_bytes = 4, - .direct_mesh_gpu_bytes = 50, - .known_memory_bytes = 253, - .wait_idle_count = 5, - .wait_idle_ms = 3, - .compact_submissions = 1, - }), 0, 0); - try runner.recordFrame(0.02, 50, gpu, Test.world(.{ - .enabled = true, - .update_ms = 12, - .scheduling_ms = 7, - .cache_ms = 2, - .generation_dispatch_ms = 3, - .state_transition_ms = 2, - .upload_prep_ms = 3, - .upload_submission_ms = 4, - .visibility_ms = 3, - .coverage_ms = 2, - .eviction_ms = 2, - .worker_generation_ms = 12, - .worker_mesh_construction_ms = 6, - .worker_far_expanded_mesh_construction_ms = 5, - .worker_compact_encode_ms = 3, - .manager_lock_wait_ms = 5, - .manager_lock_hold_ms = 7, - .upload_bytes = 150, - .far_expanded_upload_bytes = 70, - .compact_upload_bytes = 30, - .pending_cpu_upload_bytes = 20, - .staging_pressure_count = 4, - .visible_count = 13, - .rejected_count = 5, - .coverage_count = 8, - .visibility_levels = .{ .{}, .{ .candidates = 7, .accepted = 3, .rejected_frustum = 2, .coverage_checks = 6 }, .{}, .{}, .{} }, - .deferred_deletion_bytes = 40, - .deferred_deletion_cpu_bytes = 5, - .pool_gpu_capacity_bytes = 120, - .pool_gpu_allocated_bytes = 90, - .pool_gpu_slack_bytes = 30, - .pool_cpu_shadow_bytes = 120, - .compact_pool_capacity_bytes = 50, - .compact_pool_allocated_bytes = 40, - .compact_pool_free_bytes = 10, - .compact_pool_retired_bytes = 5, - .direct_mesh_gpu_bytes = 60, - .known_memory_bytes = 305, - .wait_idle_count = 6, - .wait_idle_ms = 5, - .compact_submissions = 3, - }), 0, 0); - try runner.recordFrame(0.03, 33, gpu, Test.world(.{ - .enabled = true, - .update_ms = 1, - .scheduling_ms = 0.5, - .cache_ms = 4, - .generation_dispatch_ms = 0.25, - .state_transition_ms = 0.5, - .upload_prep_ms = 0.25, - .upload_submission_ms = 0.25, - .visibility_ms = 0.5, - .coverage_ms = 0.25, - .eviction_ms = 0.5, - .worker_generation_ms = 3, - .worker_mesh_construction_ms = 2, - .worker_far_expanded_mesh_construction_ms = 1, - .worker_compact_encode_ms = 0.5, - .manager_lock_wait_ms = 1, - .manager_lock_hold_ms = 1, - .upload_bytes = 20, - .far_expanded_upload_bytes = 20, - .compact_upload_bytes = 5, - .pending_cpu_upload_bytes = 5, - .staging_pressure_count = 2, - .visible_count = 2, - .rejected_count = 1, - .coverage_count = 3, - .visibility_levels = .{ .{}, .{ .candidates = 1, .accepted = 1, .rejected_chunk_coverage = 1, .coverage_checks = 1 }, .{}, .{}, .{} }, - .deferred_deletion_bytes = 8, - .deferred_deletion_cpu_bytes = 1, - .pool_gpu_capacity_bytes = 90, - .pool_gpu_allocated_bytes = 70, - .pool_gpu_slack_bytes = 20, - .pool_cpu_shadow_bytes = 90, - .compact_pool_capacity_bytes = 30, - .compact_pool_allocated_bytes = 20, - .compact_pool_free_bytes = 10, - .compact_pool_retired_bytes = 2, - .direct_mesh_gpu_bytes = 40, - .known_memory_bytes = 229, - .wait_idle_count = 2, - .wait_idle_ms = 0.5, - .compact_submissions = 1, - }), 0, 0); - - const results = try runner.makeResults(); - try std.testing.expect(results.lod.profiling_enabled); - try std.testing.expectApproxEqAbs(@as(f64, 1), results.lod.cpu_frame_ms.p50, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 1.9), results.lod.cpu_frame_ms.p95, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 2.5), results.lod.cpu_categories.scheduling.total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 5), results.lod.cpu_categories.cache.total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 7), results.lod.workers.generation_total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 4), results.lod.workers.mesh_construction_total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 4), results.lod.workers.far_expanded_mesh_construction_total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 2.5), results.lod.workers.compact_encode_total_ms, 0.001); - try std.testing.expectEqual(@as(u64, 70), results.lod.memory_bytes.upload_total_bytes); - try std.testing.expectEqual(@as(u64, 50), results.lod.memory_bytes.far_expanded_upload_total_bytes); - try std.testing.expectEqual(@as(u64, 25), results.lod.memory_bytes.compact_upload_total_bytes); - try std.testing.expectEqual(@as(u64, 20), results.lod.memory_bytes.pending_cpu_upload_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 8), results.lod.memory_bytes.deferred_deletion_bytes.last_bytes); - try std.testing.expectEqual(@as(u64, 40), results.lod.memory_bytes.deferred_deletion_gpu_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 1), results.lod.memory_bytes.deferred_deletion_cpu_bytes.last_bytes); - try std.testing.expectEqual(@as(u64, 120), results.lod.memory_bytes.pool_gpu_capacity_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 90), results.lod.memory_bytes.pool_gpu_capacity_bytes.last_bytes); - try std.testing.expectEqual(@as(u64, 90), results.lod.memory_bytes.pool_gpu_allocated_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 30), results.lod.memory_bytes.pool_gpu_slack_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 120), results.lod.memory_bytes.pool_cpu_shadow_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 50), results.lod.memory_bytes.compact_pool_capacity_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 40), results.lod.memory_bytes.compact_pool_allocated_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 10), results.lod.memory_bytes.compact_pool_free_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 2), results.lod.memory_bytes.compact_pool_retired_bytes.last_bytes); - try std.testing.expectEqual(@as(u64, 60), results.lod.memory_bytes.direct_mesh_gpu_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 305), results.lod.memory_bytes.known_memory_bytes.max_bytes); - try std.testing.expectEqual(@as(u64, 229), results.lod.memory_bytes.known_memory_bytes.last_bytes); - try std.testing.expectEqual(@as(u64, 5), results.lod.visibility.visible_total); - try std.testing.expectEqual(@as(u64, 2), results.lod.visibility.rejected_total); - try std.testing.expectEqual(@as(u64, 5), results.lod.visibility.coverage_total); - try std.testing.expectEqual(@as(u64, 3), results.lod.pressure.wait_idle_count_total); - try std.testing.expectApproxEqAbs(@as(f64, 2.5), results.lod.pressure.wait_idle_ms_total, 0.001); - try std.testing.expectEqual(@as(u64, 3), results.lod.pressure.staging_pressure_total); - try std.testing.expectApproxEqAbs(@as(f64, 4), results.lod.cpu_categories.manager_lock_wait.total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 4), results.lod.cpu_categories.manager_lock_hold.total_ms, 0.001); - try std.testing.expectEqual(@as(u64, 3), results.lod.visibility.levels[1].candidates); - try std.testing.expectEqual(@as(u64, 2), results.lod.visibility.levels[1].accepted); - try std.testing.expectEqual(@as(u64, 1), results.lod.visibility.levels[1].rejected_frustum); - try std.testing.expectEqual(@as(u64, 1), results.lod.visibility.levels[1].rejected_chunk_coverage); - try std.testing.expectEqual(@as(u64, 3), results.lod.visibility.levels[1].coverage_checks); - try std.testing.expectEqual(@as(u32, 2), results.worst_frame.frame_index); - try std.testing.expectEqualStrings("cache", results.worst_frame.dominant_lod_cpu_category); - try std.testing.expectEqual(@as(u64, 20), results.worst_frame.lod_upload_bytes); - try std.testing.expectEqual(@as(u64, 3), results.lod.compact_submissions); - - const json = try BenchmarkRunner.results_json(results, std.testing.allocator); - defer std.testing.allocator.free(json); - inline for (.{ - "manager_lock_wait", - "manager_lock_hold", - "pool_gpu_capacity_bytes", - "pool_gpu_allocated_bytes", - "pool_gpu_slack_bytes", - "pool_cpu_shadow_bytes", - "compact_pool_capacity_bytes", - "compact_pool_allocated_bytes", - "compact_pool_free_bytes", - "compact_pool_retired_bytes", - "direct_mesh_gpu_bytes", - "pending_cpu_upload_bytes", - "deferred_deletion_gpu_bytes", - "deferred_deletion_cpu_bytes", - "known_memory_bytes", - "far_expanded_upload_bytes", - "compact_upload_bytes", - "far_expanded_mesh_construction_total_ms", - "compact_encode_total_ms", - "compact_submissions", - }) |field| { - try std.testing.expect(std.mem.indexOf(u8, json, field) != null); - } -} - -test "benchmark scenarios have stable bounded poses" { - try std.testing.expectEqual(Scenario.traversal, try Scenario.parse("traversal")); - try std.testing.expectEqual(Scenario.teleport_eviction, try Scenario.parse("teleport-eviction")); - try std.testing.expectError(error.InvalidBenchmarkScenario, Scenario.parse("unbounded")); - - const stationary_a = poseAtTime(.stationary, 0); - const stationary_b = poseAtTime(.stationary, 37); - try std.testing.expectApproxEqAbs(stationary_a.pos.x, stationary_b.pos.x, 0.0001); - try std.testing.expectApproxEqAbs(stationary_a.pos.z, stationary_b.pos.z, 0.0001); - - const teleport_a = poseAtTime(.teleport_eviction, 3.9); - const teleport_b = poseAtTime(.teleport_eviction, 4.0); - try std.testing.expectApproxEqAbs(@as(f32, 8), teleport_a.pos.x, 0.0001); - try std.testing.expectApproxEqAbs(@as(f32, 1_536), teleport_b.pos.x, 0.0001); - - const rapid_turn_a = poseAtTime(.rapid_turn, 0); - const rapid_turn_b = poseAtTime(.rapid_turn, 1); - try std.testing.expect(rapid_turn_a.look.z < rapid_turn_b.look.z); -} - -test "compact benchmark evidence requires residency and a successful submission" { - const resident_without_submission = LODStatsDisplay{ - .loaded = .{ 0, 0, 0, 0, 0 }, - .memory_used_mb = 0, - .compact_pool_allocated_bytes = 64, - }; - try std.testing.expect(!compactEvidenceReady(null)); - try std.testing.expect(!compactEvidenceReady(resident_without_submission)); - - var submitted = resident_without_submission; - submitted.profiling.compact_submissions = 1; - try std.testing.expect(compactEvidenceReady(submitted)); -} - -test "benchmark readiness uses common regions and GPU-only candidate evidence" { - const gpu = std.mem.zeroes(GpuTimingResults); - const ready_lod = LODStatsDisplay{ - .loaded = .{ 0, 0, 0, 0, 0 }, - .memory_used_mb = 0, - .compact_pool_allocated_bytes = 64, - .profiling = .{ - .gpu_culling_requested = true, - .gpu_culling_candidate_count = 512, - .compact_submissions = 1, - }, - }; - const ready_world = WorldStats{ - .chunks_total = 1, - .chunks_rendered = 1, - .chunks_culled = 0, - .vertices_rendered = 0, - .gen_queue = 0, - .mesh_queue = 0, - .upload_queue = 0, - .lod_renderable_regions = 512, - .lod = ready_lod, - }; - - var runner = try BenchmarkRunner.init(std.testing.allocator, "high", "traversal", 12, 4096, 1, BENCHMARK_WORLD_SEED, "Debug", "flat", "", "unused.json", 2048, 512); - defer runner.deinit(); - runner.evidence_mode = true; - runner.evidence_min_warmup_s = 0; - try runner.recordFrame(1.0, 60, gpu, ready_world, 0, 0); - try std.testing.expect(runner.warmup_ready); - try std.testing.expectEqual(@as(u32, 512), runner.warmup_gpu_candidate_count_max); - try std.testing.expectEqual(@as(u64, 512), runner.startup_evidence.lod_renderable_regions); - - // The CPU source must meet the same resident-region gate but cannot be - // held to a GPU-only candidate counter. - runner.warmup_ready = false; - runner.elapsed_s = 0; - runner.settled_warmup_s = 0; - runner.startup_evidence = .{}; - var cpu_world = ready_world; - cpu_world.lod.?.profiling.gpu_culling_requested = false; - cpu_world.lod.?.profiling.gpu_culling_candidate_count = 0; - try runner.recordFrame(1.0, 60, gpu, cpu_world, 0, 0); - try std.testing.expect(runner.warmup_ready); -} - -test "startup evidence preserves cumulative readiness counters and resident gauges" { - const evidence = startupEvidence(.{ - .loaded = .{ 0, 0, 0, 0, 0 }, - .memory_used_mb = 0, - .pool_gpu_allocated_bytes = 100, - .direct_mesh_gpu_bytes = 200, - .compact_pool_allocated_bytes = 300, - .pool_cpu_shadow_bytes = 400, - .profiling = .{ - .upload_bytes = 500, - .far_expanded_upload_bytes = 300, - .compact_upload_bytes = 100, - .worker_generation_ms = 6.5, - .worker_mesh_construction_ms = 7.5, - .worker_far_expanded_mesh_construction_ms = 4.5, - .worker_compact_encode_ms = 2.5, - .compact_submissions = 8, - }, - }, 9, 512); - try std.testing.expect(evidence.readiness_observed); - try std.testing.expectEqual(@as(u64, 500), evidence.upload_total_bytes); - try std.testing.expectEqual(@as(u64, 300), evidence.far_expanded_upload_bytes); - try std.testing.expectEqual(@as(u64, 100), evidence.compact_upload_bytes); - try std.testing.expectApproxEqAbs(@as(f64, 7.5), evidence.worker_mesh_construction_total_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 4.5), evidence.worker_far_expanded_mesh_construction_ms, 0.001); - try std.testing.expectApproxEqAbs(@as(f64, 2.5), evidence.worker_compact_encode_ms, 0.001); - try std.testing.expectEqual(@as(u64, 8), evidence.compact_submissions); - try std.testing.expectEqual(@as(u64, 500), evidence.direct_mesh_gpu_bytes + evidence.compact_pool_allocated_bytes); -} - -fn validResultForPolicyTest() !BenchmarkResults { - var runner = try BenchmarkRunner.init(std.testing.allocator, "low", "stationary", 6, 512, 1, BENCHMARK_WORLD_SEED, "ReleaseFast", "flat", "", "unused.json", 0, 0); - defer runner.deinit(); - runner.sampled_s = 1; - try runner.samples.append(std.testing.allocator, .{ - .cpu_ms = 1, - .fps = 100, - .gpu_shadow_ms = 0, - .gpu_opaque_ms = 0, - .gpu_lod_terrain_ms = 1, - .gpu_lod_water_ms = 1, - .gpu_lod_compact_terrain_ms = 0, - .gpu_lod_compact_water_ms = 0, - .gpu_lod_culling_ms = 1, - .gpu_total_ms = 3, - .draw_calls = 0, - .vertices = 0, - .chunks_rendered = 0, - .gpu_memory_mb = 0, - .lod = .{ .enabled = true, .pending_cpu_upload_bytes = 1, .deferred_deletion_cpu_bytes = 1, .pool_cpu_shadow_bytes = 1, .deferred_deletion_bytes = 1, .pool_gpu_capacity_bytes = 1, .compact_pool_capacity_bytes = 1, .direct_mesh_gpu_bytes = 1 }, - }); - return runner.makeResults(); -} - -test "evidence mode rejects unknown benchmark provenance" { - var results = try validResultForPolicyTest(); - results.completion.evidence_mode = true; - results.startup_evidence.readiness_observed = true; - results.provenance = .{ - .gpu_adapter = "Lavapipe", - .gpu_driver = "Mesa", - .runner = "test runner", - .zig_toolchain = "Zig 0.16.0", - }; - try validateResults(results); - results.provenance.gpu_driver = "unknown"; - try std.testing.expectError(error.MissingBenchmarkProvenance, validateResults(results)); +fn summarizeSeries(allocator: std.mem.Allocator, values: []const f32) !Summary { + if (values.len == 0) return .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }; + const sorted = try allocator.dupe(f32, values); + defer allocator.free(sorted); + std.sort.block(f32, sorted, {}, comptime std.sort.asc(f32)); + var sum: f64 = 0; + for (sorted) |value| sum += value; + return .{ .min = sorted[0], .avg = sum / @as(f64, @floatFromInt(sorted.len)), .max = sorted[sorted.len - 1], .p1 = percentile(sorted, 0.01), .p5 = percentile(sorted, 0.05), .p50 = percentile(sorted, 0.50), .p95 = percentile(sorted, 0.95), .p99 = percentile(sorted, 0.99) }; } -test "benchmark policy rejects each LOD evidence and percentile budget breach" { - var results = try validResultForPolicyTest(); - try validateResults(results); - results.lod.memory_bytes.logical_ram_bytes.max_bytes = 0; - try std.testing.expectError(error.MissingLogicalLodMemoryEvidence, validateResults(results)); - results = try validResultForPolicyTest(); - results.lod.cpu_frame_ms.p95 = 41; - try std.testing.expectError(error.LodCpuBudgetBreach, validateResults(results)); - results = try validResultForPolicyTest(); - results.lod.gpu_frame_ms.p99 = 121; - try std.testing.expectError(error.LodGpuBudgetBreach, validateResults(results)); - results = try validResultForPolicyTest(); - results.lod.memory_bytes.logical_ram_bytes.p99_bytes = 513 * 1024 * 1024; - try std.testing.expectError(error.LodLogicalRamBudgetBreach, validateResults(results)); - results = try validResultForPolicyTest(); - results.lod.memory_bytes.logical_vram_bytes.p99_bytes = 1025 * 1024 * 1024; - try std.testing.expectError(error.LodLogicalVramBudgetBreach, validateResults(results)); +fn percentile(sorted: []const f32, fraction: f64) f64 { + if (sorted.len == 0) return 0; + if (sorted.len == 1) return sorted[0]; + const position = std.math.clamp(fraction, 0.0, 1.0) * @as(f64, @floatFromInt(sorted.len - 1)); + const lower: usize = @intFromFloat(@floor(position)); + const upper = @min(lower + 1, sorted.len - 1); + const blend = position - @as(f64, @floatFromInt(lower)); + return @as(f64, sorted[lower]) * (1.0 - blend) + @as(f64, sorted[upper]) * blend; } -test "byte gauge sample summary calculates p50 p95 and p99" { - const values = [_]u64{ 1, 2, 3, 4, 100 }; - const summary = try ByteGaugeAccumulator.summarizeSamples(std.testing.allocator, &values); - try std.testing.expectEqual(@as(u64, 3), summary.p50_bytes); - try std.testing.expectEqual(@as(u64, 100), summary.p95_bytes); - try std.testing.expectEqual(@as(u64, 100), summary.p99_bytes); +fn averageArray(values: []const f32) f32 { + var sum: f32 = 0; + for (values) |value| sum += value; + return sum / @as(f32, @floatFromInt(values.len)); } fn fpsField(sample: FrameSample) f32 { return sample.fps; } - fn frameMsField(sample: FrameSample) f32 { return sample.cpu_ms; } fn gpuTotalField(sample: FrameSample) f32 { return sample.gpu_total_ms; } -fn lodGpuField(sample: FrameSample) f32 { - return sample.gpu_lod_terrain_ms + sample.gpu_lod_water_ms + sample.gpu_lod_culling_ms; -} -fn logicalRamBytes(sample: LODFrameSample) u64 { - return sample.source_data_cpu_bytes +| sample.pending_cpu_upload_bytes +| sample.deferred_deletion_cpu_bytes +| sample.pool_cpu_shadow_bytes; -} -fn logicalVramBytes(sample: LODFrameSample) u64 { - return sample.deferred_deletion_bytes +| sample.pool_gpu_capacity_bytes +| sample.compact_pool_capacity_bytes +| sample.direct_mesh_gpu_bytes; -} -fn compactTerrainField(sample: FrameSample) f32 { - return sample.gpu_lod_compact_terrain_ms; -} -fn compactWaterField(sample: FrameSample) f32 { - return sample.gpu_lod_compact_water_ms; -} -fn cullingField(sample: FrameSample) f32 { - return sample.gpu_lod_culling_ms; -} -fn timingTotalAverage(total_ms: f64, frame_count: f64) TimingTotalAverage { - return .{ .total_ms = total_ms, .avg_ms = total_ms / frame_count }; -} +const Pose = struct { pos: Vec3, look: Vec3 }; -/// Calculates a per-frame telemetry sample. A reduced cumulative value means -/// the LOD collector reset, so the current value is used instead of underflowing. -fn lodFrameDelta(current: ?LODStatsDisplay, previous: *?LODProfilingDisplay) LODFrameSample { - const display = current orelse { - previous.* = null; - return .{}; +fn poseAtTime(scenario: Scenario, elapsed_s: f32) Pose { + const path = switch (scenario) { + .stationary => &STATIONARY_PATH, + .traversal => &TRAVERSAL_PATH, + .rapid_turn => &RAPID_TURN_PATH, + .teleport_eviction => &TELEPORT_EVICTION_PATH, }; - const snapshot = display.profiling; - const last = previous.*; - previous.* = snapshot; - - const gauges = struct { - pending_cpu_upload_bytes: u64, - deferred_deletion_bytes: u64, - deferred_deletion_cpu_bytes: u64, - pool_gpu_capacity_bytes: u64, - pool_gpu_allocated_bytes: u64, - pool_gpu_slack_bytes: u64, - pool_cpu_shadow_bytes: u64, - compact_pool_capacity_bytes: u64, - compact_pool_allocated_bytes: u64, - compact_pool_free_bytes: u64, - compact_pool_retired_bytes: u64, - direct_mesh_gpu_bytes: u64, - source_data_cpu_bytes: u64, - known_memory_bytes: u64, - }{ - .pending_cpu_upload_bytes = display.pending_cpu_upload_bytes, - .deferred_deletion_bytes = display.deferred_deletion_gpu_bytes, - .deferred_deletion_cpu_bytes = display.deferred_deletion_cpu_bytes, - .pool_gpu_capacity_bytes = display.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = display.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = display.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = display.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = display.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = display.compact_pool_allocated_bytes, - .compact_pool_free_bytes = display.compact_pool_free_bytes, - .compact_pool_retired_bytes = display.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = display.direct_mesh_gpu_bytes, - .source_data_cpu_bytes = display.source_data_cpu_bytes, - .known_memory_bytes = display.memory_used_bytes, - }; - if (!snapshot.enabled or last == null or !last.?.enabled) { - return .{ - .enabled = snapshot.enabled, - .pending_cpu_upload_bytes = gauges.pending_cpu_upload_bytes, - .deferred_deletion_bytes = gauges.deferred_deletion_bytes, - .deferred_deletion_cpu_bytes = gauges.deferred_deletion_cpu_bytes, - .pool_gpu_capacity_bytes = gauges.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = gauges.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = gauges.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = gauges.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = gauges.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = gauges.compact_pool_allocated_bytes, - .compact_pool_free_bytes = gauges.compact_pool_free_bytes, - .compact_pool_retired_bytes = gauges.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = gauges.direct_mesh_gpu_bytes, - .source_data_cpu_bytes = gauges.source_data_cpu_bytes, - .known_memory_bytes = gauges.known_memory_bytes, - .gpu_culling_overflows = snapshot.gpu_culling_overflows, - .gpu_culling_validation_mismatches = snapshot.gpu_culling_validation_mismatches, - .gpu_culling_requested = snapshot.gpu_culling_requested, - .gpu_culling_threshold = snapshot.gpu_culling_threshold, - .gpu_culling_candidate_count = snapshot.gpu_culling_candidate_count, - .gpu_culling_candidate_count_max = snapshot.gpu_culling_candidate_count_max, - .gpu_culling_draw_submissions = snapshot.gpu_culling_draw_submissions, - .gpu_culling_validation_generation = snapshot.gpu_culling_validation_generation, - .gpu_culling_validation_completed_generation = snapshot.gpu_culling_validation_completed_generation, - .gpu_culling_validation_completed_count = snapshot.gpu_culling_validation_completed_count, - }; - } - - const prior = last.?; - return .{ - .enabled = true, - // Manager update and render visibility work are separate main-thread - // scopes. Coverage is nested in visibility and is not added again. - .cpu_ms = cumulativeTimingDelta(snapshot.update_ms, prior.update_ms) + cumulativeTimingDelta(snapshot.visibility_ms, prior.visibility_ms), - .scheduling_ms = cumulativeTimingDelta(snapshot.scheduling_ms, prior.scheduling_ms), - .cache_ms = cumulativeTimingDelta(snapshot.cache_ms, prior.cache_ms), - .generation_dispatch_ms = cumulativeTimingDelta(snapshot.generation_dispatch_ms, prior.generation_dispatch_ms), - .state_transition_ms = cumulativeTimingDelta(snapshot.state_transition_ms, prior.state_transition_ms), - .upload_prep_ms = cumulativeTimingDelta(snapshot.upload_prep_ms, prior.upload_prep_ms), - .upload_submission_ms = cumulativeTimingDelta(snapshot.upload_submission_ms, prior.upload_submission_ms), - .visibility_ms = cumulativeTimingDelta(snapshot.visibility_ms, prior.visibility_ms), - .coverage_ms = cumulativeTimingDelta(snapshot.coverage_ms, prior.coverage_ms), - .eviction_ms = cumulativeTimingDelta(snapshot.eviction_ms, prior.eviction_ms), - .worker_generation_ms = cumulativeTimingDelta(snapshot.worker_generation_ms, prior.worker_generation_ms), - .worker_mesh_construction_ms = cumulativeTimingDelta(snapshot.worker_mesh_construction_ms, prior.worker_mesh_construction_ms), - .worker_far_expanded_mesh_construction_ms = cumulativeTimingDelta(snapshot.worker_far_expanded_mesh_construction_ms, prior.worker_far_expanded_mesh_construction_ms), - .worker_compact_encode_ms = cumulativeTimingDelta(snapshot.worker_compact_encode_ms, prior.worker_compact_encode_ms), - .manager_lock_wait_ms = cumulativeTimingDelta(snapshot.manager_lock_wait_ms, prior.manager_lock_wait_ms), - .manager_lock_hold_ms = cumulativeTimingDelta(snapshot.manager_lock_hold_ms, prior.manager_lock_hold_ms), - .upload_bytes = cumulativeCounterDelta(snapshot.upload_bytes, prior.upload_bytes), - .far_expanded_upload_bytes = cumulativeCounterDelta(snapshot.far_expanded_upload_bytes, prior.far_expanded_upload_bytes), - .compact_upload_bytes = cumulativeCounterDelta(snapshot.compact_upload_bytes, prior.compact_upload_bytes), - .pending_cpu_upload_bytes = gauges.pending_cpu_upload_bytes, - .staging_pressure_count = cumulativeCounterDelta(snapshot.staging_pressure_count, prior.staging_pressure_count), - .visible_count = cumulativeCounterDelta(snapshot.visible_count, prior.visible_count), - .rejected_count = cumulativeCounterDelta(snapshot.rejected_count, prior.rejected_count), - .coverage_count = cumulativeCounterDelta(snapshot.coverage_count, prior.coverage_count), - .visibility_levels = blk: { - var levels: [LOD_VISIBILITY_LEVEL_COUNT]LODVisibilityLevelDisplay = undefined; - for (&levels, 0..) |*level, index| level.* = visibilityLevelDelta(snapshot.visibility_levels[index], prior.visibility_levels[index]); - break :blk levels; - }, - .deferred_deletion_bytes = gauges.deferred_deletion_bytes, - .deferred_deletion_cpu_bytes = gauges.deferred_deletion_cpu_bytes, - .pool_gpu_capacity_bytes = gauges.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = gauges.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = gauges.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = gauges.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = gauges.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = gauges.compact_pool_allocated_bytes, - .compact_pool_free_bytes = gauges.compact_pool_free_bytes, - .compact_pool_retired_bytes = gauges.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = gauges.direct_mesh_gpu_bytes, - .source_data_cpu_bytes = gauges.source_data_cpu_bytes, - .known_memory_bytes = gauges.known_memory_bytes, - .wait_idle_count = cumulativeCounterDelta(snapshot.wait_idle_count, prior.wait_idle_count), - .wait_idle_ms = cumulativeTimingDelta(snapshot.wait_idle_ms, prior.wait_idle_ms), - .gpu_culling_overflows = snapshot.gpu_culling_overflows, - .gpu_culling_validation_mismatches = snapshot.gpu_culling_validation_mismatches, - .gpu_culling_requested = snapshot.gpu_culling_requested, - .gpu_culling_threshold = snapshot.gpu_culling_threshold, - .gpu_culling_candidate_count = snapshot.gpu_culling_candidate_count, - .gpu_culling_candidate_count_max = snapshot.gpu_culling_candidate_count_max, - .gpu_culling_draw_submissions = cumulativeCounterDelta(snapshot.gpu_culling_draw_submissions, prior.gpu_culling_draw_submissions), - .gpu_culling_validation_generation = snapshot.gpu_culling_validation_generation, - .gpu_culling_validation_completed_generation = snapshot.gpu_culling_validation_completed_generation, - .gpu_culling_validation_completed_count = snapshot.gpu_culling_validation_completed_count, - }; -} - -fn visibilityLevelDelta(current: LODVisibilityLevelDisplay, previous: LODVisibilityLevelDisplay) LODVisibilityLevelDisplay { - var result = LODVisibilityLevelDisplay{}; - inline for (std.meta.fields(LODVisibilityLevelDisplay)) |field| { - @field(result, field.name) = cumulativeCounterDelta(@field(current, field.name), @field(previous, field.name)); - } - return result; -} - -fn addVisibilityLevel(total: *LODVisibilityLevelDisplay, value: LODVisibilityLevelDisplay) void { - inline for (std.meta.fields(LODVisibilityLevelDisplay)) |field| { - @field(total.*, field.name) +|= @field(value, field.name); - } -} - -fn cumulativeTimingDelta(current: f64, previous: f64) f64 { - if (!std.math.isFinite(current) or current < 0) return 0; - if (!std.math.isFinite(previous) or previous < 0 or current < previous) return current; - return current - previous; -} - -fn cumulativeCounterDelta(current: u64, previous: u64) u64 { - return if (current < previous) current else current - previous; -} - -fn worstFrameForSample(index: usize, sample: FrameSample) WorstFrame { - var dominant_gpu_pass: []const u8 = "shadow"; - var dominant_gpu_pass_ms = sample.gpu_shadow_ms; - if (sample.gpu_opaque_ms > dominant_gpu_pass_ms) { - dominant_gpu_pass = "opaque"; - dominant_gpu_pass_ms = sample.gpu_opaque_ms; - } - if (sample.gpu_lod_terrain_ms > dominant_gpu_pass_ms) { - dominant_gpu_pass = "lod_terrain"; - dominant_gpu_pass_ms = sample.gpu_lod_terrain_ms; - } - if (sample.gpu_lod_water_ms > dominant_gpu_pass_ms) { - dominant_gpu_pass = "lod_water"; - dominant_gpu_pass_ms = sample.gpu_lod_water_ms; - } - if (sample.gpu_lod_culling_ms > dominant_gpu_pass_ms) { - dominant_gpu_pass = "lod_culling"; - dominant_gpu_pass_ms = sample.gpu_lod_culling_ms; - } - - const lod_attribution = dominantLodCpuCategory(sample.lod); - - return .{ - .frame_index = @intCast(index), - .frame_ms = sample.cpu_ms, - .gpu_total_ms = sample.gpu_total_ms, - .gpu_lod_terrain_ms = sample.gpu_lod_terrain_ms, - .gpu_lod_water_ms = sample.gpu_lod_water_ms, - .gpu_lod_culling_ms = sample.gpu_lod_culling_ms, - .dominant_gpu_pass = dominant_gpu_pass, - .dominant_gpu_pass_ms = dominant_gpu_pass_ms, - .lod_cpu_ms = sample.lod.cpu_ms, - .dominant_lod_cpu_category = lod_attribution.name, - .dominant_lod_cpu_category_ms = lod_attribution.ms, - .lod_worker_generation_ms = sample.lod.worker_generation_ms, - .lod_worker_mesh_construction_ms = sample.lod.worker_mesh_construction_ms, - .lod_upload_bytes = sample.lod.upload_bytes, - .lod_pending_cpu_upload_bytes = sample.lod.pending_cpu_upload_bytes, - .lod_deferred_deletion_bytes = sample.lod.deferred_deletion_bytes, - .lod_visible_count = sample.lod.visible_count, - .lod_rejected_count = sample.lod.rejected_count, - .lod_coverage_count = sample.lod.coverage_count, - .lod_wait_idle_count = sample.lod.wait_idle_count, - .lod_wait_idle_ms = sample.lod.wait_idle_ms, - .lod_staging_pressure_count = sample.lod.staging_pressure_count, - }; -} - -fn dominantLodCpuCategory(sample: LODFrameSample) struct { name: []const u8, ms: f64 } { - var name: []const u8 = "none"; - var ms: f64 = 0; - const candidates = [_]struct { name: []const u8, ms: f64 }{ - .{ .name = "scheduling", .ms = sample.scheduling_ms }, - .{ .name = "cache", .ms = sample.cache_ms }, - .{ .name = "generation_dispatch", .ms = sample.generation_dispatch_ms }, - .{ .name = "state_transition", .ms = sample.state_transition_ms }, - .{ .name = "upload_prep", .ms = sample.upload_prep_ms }, - .{ .name = "upload_submission", .ms = sample.upload_submission_ms }, - .{ .name = "visibility", .ms = sample.visibility_ms }, - .{ .name = "coverage", .ms = sample.coverage_ms }, - .{ .name = "eviction", .ms = sample.eviction_ms }, - }; - for (candidates) |candidate| { - if (candidate.ms > ms) { - name = candidate.name; - ms = candidate.ms; + const interpolate = scenario != .teleport_eviction; + var remaining = elapsed_s; + const total = pathDuration(path); + while (remaining >= total) remaining -= total; + for (path, 0..) |waypoint, index| { + const next = path[(index + 1) % path.len]; + const in_segment = if (interpolate) remaining <= waypoint.duration else remaining < waypoint.duration; + if (in_segment or index == path.len - 1) { + if (!interpolate) return .{ .pos = waypoint.pos, .look = waypoint.look.normalize() }; + const progress = std.math.clamp(remaining / @max(waypoint.duration, 0.0001), 0.0, 1.0); + const smooth = progress * progress * (3.0 - 2.0 * progress); + return .{ .pos = lerpVec3(waypoint.pos, next.pos, smooth), .look = lerpVec3(waypoint.look, next.look, smooth).normalize() }; } + remaining -= waypoint.duration; } - return .{ .name = name, .ms = ms }; + unreachable; } -fn summarizeSeries(allocator: std.mem.Allocator, values: []f32) !Summary { - if (values.len == 0) { - return .{ .min = 0, .avg = 0, .max = 0, .p1 = 0, .p5 = 0, .p50 = 0, .p95 = 0, .p99 = 0 }; - } - - const sorted = try allocator.dupe(f32, values); - defer allocator.free(sorted); - std.sort.block(f32, sorted, {}, lessThan); - - var sum: f64 = 0; - for (sorted) |value| sum += value; - - return .{ - .min = sorted[0], - .avg = sum / @as(f64, @floatFromInt(sorted.len)), - .max = sorted[sorted.len - 1], - .p1 = percentile(sorted, 0.01), - .p5 = percentile(sorted, 0.05), - .p50 = percentile(sorted, 0.50), - .p95 = percentile(sorted, 0.95), - .p99 = percentile(sorted, 0.99), - }; +fn pathDuration(path: []const Waypoint) f32 { + var total: f32 = 0; + for (path) |waypoint| total += waypoint.duration; + return total; } -fn percentile(sorted: []const f32, p: f64) f64 { - if (sorted.len == 0) return 0; - if (sorted.len == 1) return sorted[0]; - - const clamped = std.math.clamp(p, 0.0, 1.0); - const pos = clamped * @as(f64, @floatFromInt(sorted.len - 1)); - const lower: usize = @intFromFloat(@floor(pos)); - const upper = @min(lower + 1, sorted.len - 1); - const frac = pos - @as(f64, @floatFromInt(lower)); - return @as(f64, sorted[lower]) * (1.0 - frac) + @as(f64, sorted[upper]) * frac; +fn lerpVec3(a: Vec3, b: Vec3, t: f32) Vec3 { + return a.add(b.sub(a).scale(t)); } -fn lessThan(_: void, a: f32, b: f32) bool { - return a < b; -} -fn lessThanU64(_: void, a: u64, b: u64) bool { - return a < b; -} -fn percentileU64(sorted: []const u64, quantile: f64) u64 { - if (sorted.len == 0) return 0; - const index: usize = @intFromFloat(@ceil(quantile * @as(f64, @floatFromInt(sorted.len - 1)))); - return sorted[@min(index, sorted.len - 1)]; +fn nowMs() i64 { + return std.Io.Clock.real.now(std.Options.debug_io).toMilliseconds(); } -fn averageArray(values: []const f32) f32 { - if (values.len == 0) return 0; - var sum: f32 = 0; - for (values) |value| sum += value; - return sum / @as(f32, @floatFromInt(values.len)); +fn wallElapsedSeconds(self: *const BenchmarkRunner) f32 { + return @as(f32, @floatFromInt(@max(@as(i64, 0), nowMs() - self.start_ms))) / 1000.0; } -fn poseAtTime(scenario: Scenario, time_s: f32) Pose { - return switch (scenario) { - .stationary => poseAtWaypoints(&STATIONARY_PATH, time_s, true), - // Keep traversal byte-for-byte equivalent to the benchmark path that - // predated scenarios so its existing baseline remains meaningful. - .traversal => poseAtWaypoints(&BENCH_PATH, time_s, true), - .rapid_turn => poseAtWaypoints(&RAPID_TURN_PATH, time_s, true), - // Eviction pressure requires discontinuous player movement rather - // than interpolation between distant locations. - .teleport_eviction => poseAtWaypoints(&TELEPORT_EVICTION_PATH, time_s, false), - }; +fn environmentLabel(name: [:0]const u8) []const u8 { + const raw = std.c.getenv(name) orelse return "unknown"; + const value = std.mem.span(raw); + return if (value.len == 0) "unknown" else value; } -fn poseAtWaypoints(path: []const Waypoint, time_s: f32, interpolate: bool) Pose { - const total = pathDuration(path); - if (total <= 0.0) { - return .{ .pos = path[0].pos, .look = path[0].look.normalize() }; - } - - var t = time_s; - while (t >= total) t -= total; - while (t < 0) t += total; - for (path, 0..) |waypoint, i| { - const next = path[(i + 1) % path.len]; - const in_segment = if (interpolate) t <= waypoint.duration else t < waypoint.duration; - if (in_segment or i == path.len - 1) { - if (!interpolate) { - return .{ .pos = waypoint.pos, .look = waypoint.look.normalize() }; - } - const segment = @max(waypoint.duration, 0.0001); - const alpha = std.math.clamp(t / segment, 0.0, 1.0); - const eased = alpha * alpha * (3.0 - 2.0 * alpha); - return .{ - .pos = lerpVec3(waypoint.pos, next.pos, eased), - .look = lerpVec3(waypoint.look, next.look, eased).normalize(), - }; - } - t -= waypoint.duration; - } - - return .{ .pos = path[0].pos, .look = path[0].look.normalize() }; +fn environmentFlag(name: [:0]const u8) bool { + const raw = std.c.getenv(name) orelse return false; + const value = std.mem.span(raw); + return value.len > 0 and !std.mem.eql(u8, value, "0") and !std.ascii.eqlIgnoreCase(value, "false") and !std.ascii.eqlIgnoreCase(value, "off"); } -fn pathDuration(path: []const Waypoint) f32 { - var total: f32 = 0; - for (path) |waypoint| total += waypoint.duration; - return total; +fn provenanceFromEnvironment() BenchmarkProvenance { + return .{ .gpu_adapter = environmentLabel("ZIGCRAFT_BENCHMARK_GPU_ADAPTER"), .gpu_driver = environmentLabel("ZIGCRAFT_BENCHMARK_GPU_DRIVER"), .runner = environmentLabel("ZIGCRAFT_BENCHMARK_RUNNER"), .zig_toolchain = environmentLabel("ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN") }; } -fn lerpVec3(a: Vec3, b: Vec3, t: f32) Vec3 { - return a.add(b.sub(a).scale(t)); +fn validProvenance(provenance: BenchmarkProvenance) bool { + inline for (.{ provenance.gpu_adapter, provenance.gpu_driver, provenance.runner, provenance.zig_toolchain }) |value| { + if (value.len == 0 or std.ascii.eqlIgnoreCase(value, "unknown")) return false; + } + return true; } fn yawFromLook(look: Vec3) f32 { return std.math.atan2(look.z, look.x); } - fn pitchFromLook(look: Vec3) f32 { - return std.math.asin(std.math.clamp(look.y, -1.0, 1.0)); + return std.math.atan2(look.y, @sqrt(look.x * look.x + look.z * look.z)); +} + +test "benchmark scenarios loop with smooth traversal and discontinuous teleports" { + try std.testing.expectEqual(Scenario.traversal, try Scenario.parse("traversal")); + try std.testing.expectError(error.InvalidBenchmarkScenario, Scenario.parse("unbounded")); + const traversal_start = poseAtTime(.traversal, 0); + const traversal_mid = poseAtTime(.traversal, 2.5); + try std.testing.expect(traversal_mid.pos.x > traversal_start.pos.x); + try std.testing.expectApproxEqAbs(traversal_start.pos.x, poseAtTime(.traversal, pathDuration(&TRAVERSAL_PATH)).pos.x, 0.001); + try std.testing.expectApproxEqAbs(@as(f32, 8), poseAtTime(.teleport_eviction, 3.9).pos.x, 0.001); + try std.testing.expectApproxEqAbs(@as(f32, 1_536), poseAtTime(.teleport_eviction, 4.0).pos.x, 0.001); + try std.testing.expectApproxEqAbs(@as(f32, 8), poseAtTime(.teleport_eviction, pathDuration(&TELEPORT_EVICTION_PATH)).pos.x, 0.001); +} + +test "benchmark percentiles interpolate between adjacent samples" { + const samples = [_]f32{ 10, 20 }; + try std.testing.expectApproxEqAbs(@as(f64, 15), percentile(&samples, 0.5), 0.001); } diff --git a/modules/game-core/src/block_outline.zig b/modules/game-core/src/block_outline.zig index b8438781..489c7588 100644 --- a/modules/game-core/src/block_outline.zig +++ b/modules/game-core/src/block_outline.zig @@ -157,7 +157,7 @@ pub const BlockOutline = struct { const model = Mat4.translate(Vec3.init(rel_x, rel_y, rel_z)); ctx.setSelectionMode(true); - ctx.setModelMatrix(model, Vec3.one, 0); + ctx.setModelMatrix(model, Vec3.one); ctx.draw(self.buffer_handle, 288, .triangles); ctx.setSelectionMode(false); } diff --git a/modules/game-core/src/hand_renderer.zig b/modules/game-core/src/hand_renderer.zig index 562517ac..1bad260e 100644 --- a/modules/game-core/src/hand_renderer.zig +++ b/modules/game-core/src/hand_renderer.zig @@ -218,7 +218,7 @@ pub const HandRenderer = struct { const swing_rot = Mat4.rotateZ(swing_rot_z); m = m.multiply(swing_rot); - ctx.setModelMatrix(m, Vec3.one, 0); + ctx.setModelMatrix(m, Vec3.one); ctx.draw(self.buffer_handle, 36, .triangles); } diff --git a/modules/game-core/src/input_mapper.zig b/modules/game-core/src/input_mapper.zig index daf5b777..92285735 100644 --- a/modules/game-core/src/input_mapper.zig +++ b/modules/game-core/src/input_mapper.zig @@ -17,7 +17,7 @@ pub const MovementVector = input_interfaces.MovementVector; /// Default bindings for all actions. Stored as a static array to avoid heap allocation. pub const DEFAULT_BINDINGS = blk: { - var bindings: [GameAction.count]ActionBinding = undefined; + var bindings = [_]ActionBinding{ActionBinding.init(.{ .none = {} })} ** GameAction.count; // Movement bindings[@intFromEnum(GameAction.move_forward)] = ActionBinding.init(.{ .key = .w }); @@ -62,7 +62,6 @@ pub const DEFAULT_BINDINGS = blk: { bindings[@intFromEnum(GameAction.toggle_creative)] = ActionBinding.init(.{ .key = .f12 }); bindings[@intFromEnum(GameAction.toggle_debug_menu)] = ActionBinding.init(.{ .key = .f3 }); bindings[@intFromEnum(GameAction.toggle_timing_overlay)] = ActionBinding.init(.{ .key = .f4 }); - bindings[@intFromEnum(GameAction.toggle_lod_render)] = ActionBinding.init(.{ .key = .f6 }); bindings[@intFromEnum(GameAction.toggle_gpass_render)] = ActionBinding.init(.{ .key = .f7 }); bindings[@intFromEnum(GameAction.toggle_ssao)] = ActionBinding.init(.{ .key = .f8 }); bindings[@intFromEnum(GameAction.toggle_fog)] = ActionBinding.init(.{ .key = .f10 }); @@ -283,3 +282,11 @@ test "InputMapper resetActionToDefault" { mapper.resetActionToDefault(.move_forward); try std.testing.expect(mapper.getBinding(.move_forward).primary.key == .w); } + +test "InputMapper binds the timing overlay to F4 by default" { + const mapper = InputMapper.init(); + const binding = mapper.getBinding(.toggle_timing_overlay); + + try std.testing.expect(binding.primary == .key); + try std.testing.expectEqual(.f4, binding.primary.key); +} diff --git a/modules/game-core/src/input_settings.zig b/modules/game-core/src/input_settings.zig index b466100b..070ebf08 100644 --- a/modules/game-core/src/input_settings.zig +++ b/modules/game-core/src/input_settings.zig @@ -28,6 +28,10 @@ pub const InputSettings = struct { /// Version 2 introduced GameAction enum-based mapping. /// Version 3 introduced human-readable object-based mapping with action names. pub const CURRENT_VERSION = 3; + /// Version 2 arrays included this action. Version 3's named bindings made + /// action removal safe, but positional migration must continue to reserve + /// its historical slot so subsequent bindings are not shifted. + const LEGACY_TOGGLE_LOD_RENDER_INDEX = 40; /// Initialize a new InputSettings instance with default bindings. pub fn init(allocator: std.mem.Allocator) InputSettings { @@ -89,7 +93,6 @@ pub const InputSettings = struct { // --- SANITY CHECK FOR BROKEN MIGRATIONS --- // If critical debug actions are mapped to Escape (common symptom of shifted indices), reset them. const g_bind = settings.input_mapper.getBinding(.toggle_shadow_debug_vis); - const f4_bind = settings.input_mapper.getBinding(.toggle_timing_overlay); var healed = false; if (g_bind.primary == .key and g_bind.primary.key == .escape) { @@ -97,12 +100,6 @@ pub const InputSettings = struct { settings.input_mapper.resetActionToDefault(.toggle_shadow_debug_vis); healed = true; } - if (f4_bind.primary == .key and f4_bind.primary.key == .escape) { - log.log.warn("InputSettings: Detected broken F4-key mapping (mapped to Escape). Resetting to Default (F4).", .{}); - settings.input_mapper.resetActionToDefault(.toggle_timing_overlay); - healed = true; - } - if (healed) { settings.save() catch |err| { log.log.err("Failed to persist healed input settings: {}", .{err}); @@ -235,17 +232,11 @@ pub const InputSettings = struct { const array = bindings_val.array; log.log.info("Migrating input settings from version {} (array) to {} (object)", .{ version, CURRENT_VERSION }); - const count = @min(array.items.len, GameAction.count); - if (array.items.len > GameAction.count) { - log.log.warn("Migration: Dropping {} unrecognized bindings (source has {}, engine supports {})", .{ - array.items.len - GameAction.count, array.items.len, GameAction.count, - }); - } - - for (array.items[0..count], 0..) |item, i| { + for (array.items, 0..) |item, legacy_index| { + const action = actionForLegacyBindingIndex(legacy_index) orelse continue; const parsed_binding = try std.json.parseFromValue(ActionBinding, self.allocator, item, .{ .ignore_unknown_fields = true }); defer parsed_binding.deinit(); - self.input_mapper.bindings[i] = parsed_binding.value; + self.input_mapper.bindings[@intFromEnum(action)] = parsed_binding.value; } } else { // New object format @@ -262,6 +253,18 @@ pub const InputSettings = struct { } return migrated; } + + fn actionForLegacyBindingIndex(legacy_index: usize) ?GameAction { + if (legacy_index == LEGACY_TOGGLE_LOD_RENDER_INDEX) return null; + + const current_index = if (legacy_index > LEGACY_TOGGLE_LOD_RENDER_INDEX) + legacy_index - 1 + else + legacy_index; + if (current_index >= GameAction.count) return null; + + return @enumFromInt(current_index); + } }; test "InputSettings.load handles corrupt file" { @@ -329,3 +332,50 @@ test "InputSettings V3 object format" { try std.testing.expect(settings.input_mapper.getBinding(.move_forward).primary.key == .w); try std.testing.expect(settings.input_mapper.getBinding(.jump).primary.key == .space); } + +test "InputSettings migrates positional bindings without shifting past removed LOD action" { + const allocator = std.testing.allocator; + const legacy_action_count = GameAction.count + 1; + var bindings = [_]ActionBinding{ActionBinding.init(.{ .none = {} })} ** legacy_action_count; + bindings[39] = ActionBinding.init(.{ .key = .f4 }); + bindings[40] = ActionBinding.init(.{ .key = .f6 }); + bindings[41] = ActionBinding.init(.{ .key = .f7 }); + + const LegacySettings = struct { + version: u8, + bindings: [legacy_action_count]ActionBinding, + }; + const json = try std.json.Stringify.valueAlloc(allocator, LegacySettings{ + .version = 2, + .bindings = bindings, + }, .{}); + defer allocator.free(json); + + var settings = InputSettings.init(allocator); + defer settings.deinit(); + _ = try settings.parseJson(json); + + try std.testing.expectEqual(.f4, settings.input_mapper.getBinding(.toggle_timing_overlay).primary.key); + try std.testing.expectEqual(.f7, settings.input_mapper.getBinding(.toggle_gpass_render).primary.key); +} + +test "InputSettings ignores removed actions in named version 3 bindings" { + const allocator = std.testing.allocator; + const v3_json = + \\{ + \\ "version": 3, + \\ "bindings": { + \\ "toggle_timing_overlay": { "primary": { "key": 1073741885 }, "alternate": { "none": {} } }, + \\ "toggle_lod_render": { "primary": { "key": 1073741887 }, "alternate": { "none": {} } }, + \\ "toggle_gpass_render": { "primary": { "key": 1073741888 }, "alternate": { "none": {} } } + \\ } + \\} + ; + + var settings = InputSettings.init(allocator); + defer settings.deinit(); + _ = try settings.parseJson(v3_json); + + try std.testing.expectEqual(.f4, settings.input_mapper.getBinding(.toggle_timing_overlay).primary.key); + try std.testing.expectEqual(.f7, settings.input_mapper.getBinding(.toggle_gpass_render).primary.key); +} diff --git a/modules/game-core/src/root.zig b/modules/game-core/src/root.zig index ac6835f9..900dba7c 100644 --- a/modules/game-core/src/root.zig +++ b/modules/game-core/src/root.zig @@ -22,7 +22,6 @@ pub const BenchmarkRunner = benchmark.BenchmarkRunner; pub const BENCHMARK_WORLD_SEED = benchmark.BENCHMARK_WORLD_SEED; pub const BuildConfig = session.BuildConfig; pub const GameSession = session.GameSession; -pub const phase5CaptureReady = session.phase5CaptureReady; pub const InputMapper = input_mapper.InputMapper; pub const InputSettings = input_settings.InputSettings; pub const Inventory = inventory.Inventory; diff --git a/modules/game-core/src/session.zig b/modules/game-core/src/session.zig index ab4a895a..7bc26906 100644 --- a/modules/game-core/src/session.zig +++ b/modules/game-core/src/session.zig @@ -18,13 +18,8 @@ const Texture = @import("engine-rhi").Texture; const TextureAtlas = @import("engine-graphics").TextureAtlas; const Input = @import("engine-input").Input; const IRawInputProvider = @import("engine-input").IRawInputProvider; -const LODConfig = @import("world-lod").lod_chunk.LODConfig; -const LODLevel = @import("world-lod").LODLevel; -const render_settings = @import("engine-rhi").render_settings; -const RenderDistancePreset = render_settings.RenderDistancePreset; const log = @import("engine-core").log; const runtime_env = @import("engine-core").runtime_env; -const BlockType = @import("world-core").BlockType; const input_mapper_pkg = @import("input_mapper.zig"); const InputMapper = input_mapper_pkg.InputMapper; const IInputMapper = input_mapper_pkg.IInputMapper; @@ -39,32 +34,14 @@ fn getenv(name: [:0]const u8) ?[]const u8 { return std.mem.span(value); } -var phase5_capture_ready = std.atomic.Value(bool).init(false); - -/// Narrow bridge from the production-world fixture session to App's screenshot -/// exit condition. Only deterministic Phase 5 captures consult this state. -pub fn phase5CaptureReady() bool { - return phase5_capture_ready.load(.acquire); -} - -fn resetPhase5CaptureReady() void { - phase5_capture_ready.store(false, .release); -} - pub const BuildConfig = struct { auto_world: []const u8 = "", chunk_debug_enable: []const u8 = "", chunk_debug_mode: bool = false, screenshot_path: []const u8 = "", - phase5_visual_scene: []const u8 = "", - benchmark_fixture: []const u8 = "", - phase5_visual_run_id: []const u8 = "", shadow_test_scene: bool = false, shadow_test_variant: []const u8 = "cave-entrance", startup_diagnostic_seconds: u32 = 0, - /// This is injected only by the benchmark executable. Zero preserves the - /// selected render-distance preset's normal production admission budget. - benchmark_lod_memory_budget_mb: u32 = 0, }; const ECSManager = @import("engine-ecs").manager; @@ -81,20 +58,9 @@ const SpawnColumn = struct { info: @import("world-worldgen").ColumnInfo, }; -/// Keeps the projection volume large enough for the configured coarsest LOD -/// radius plus one outer-region margin. A fixed 10,000-block far plane clips a -/// 1,024-chunk horizon at roughly 625 chunks even when those regions are loaded. -pub fn cameraFarPlaneForHorizon(horizon_distance_chunks: i32) f32 { - const horizon_chunks: i64 = @max(horizon_distance_chunks, 1); - const horizon_blocks = horizon_chunks * 16; - const outer_region_margin: i64 = 1024; - return @floatFromInt(@max(horizon_blocks + outer_region_margin, 10_000)); -} - -/// Uses the effective horizon so a manually lowered horizon setting cannot -/// clip a larger full-detail render distance. -pub fn cameraFarPlaneForDistances(render_distance_chunks: i32, horizon_distance_chunks: i32) f32 { - return cameraFarPlaneForHorizon(@max(render_distance_chunks, horizon_distance_chunks)); +pub fn cameraFarPlaneForRenderDistance(render_distance_chunks: i32) f32 { + const blocks: i64 = @max(render_distance_chunks, 1) * 16; + return @floatFromInt(@max(blocks + 256, 10_000)); } pub const GameSession = struct { @@ -117,7 +83,6 @@ pub const GameSession = struct { atmosphere: Atmosphere, - lod_config: LODConfig, creative_mode: bool, debug_show_fps: bool = false, @@ -125,103 +90,25 @@ pub const GameSession = struct { debug_shadows: bool = false, debug_cascade_idx: usize = 0, build_config: BuildConfig = .{}, - phase5_water_fixture_observations: u32 = 0, - phase5_water_fixture_evidence_emitted: bool = false, - phase5_fixture_applied: bool = false, - phase5_gpu_evidence_emitted: bool = false, - phase5_settle_frames: u32 = 0, - phase5_ready_evidence_emitted: bool = false, - phase5_gpu_validation_complete_emitted: bool = false, - phase5_save_flushed: bool = false, - phase5_save_loaded: bool = false, - phase5_compact_wet_dry_evidence_emitted: bool = false, - phase5_motion_frames: u32 = 0, - phase5_motion_complete: bool = false, - phase5_motion_evidence_emitted: bool = false, - gpu_culling_scale_fixture_installed: bool = false, - - pub fn init(allocator: std.mem.Allocator, rhi: *RHI, atlas: *const TextureAtlas, seed: u64, render_distance: i32, horizon_distance: i32, lod_enabled: bool, compact_tiles_enabled: bool, generator_index: usize, render_distance_preset: RenderDistancePreset, build_config: BuildConfig) !*GameSession { + + pub fn init(allocator: std.mem.Allocator, rhi: *RHI, atlas: *const TextureAtlas, seed: u64, render_distance: i32, generator_index: usize, build_config: BuildConfig) !*GameSession { const session = try allocator.create(GameSession); errdefer allocator.destroy(session); - if (phase5EvidenceEnabled(build_config)) resetPhase5CaptureReady(); const safe_mode = runtime_env.safeModeEnabled(); const strict_safe_mode = runtime_env.strictSafeModeEnabled(); const effective_render_distance: i32 = render_distance; - const chunk_debug_restore_lod = chunkDebugRestoreEnabled(build_config, "lod"); - const effective_lod_enabled = if (build_config.chunk_debug_mode) - chunk_debug_restore_lod - else - lod_enabled; if (strict_safe_mode) { log.log.warn("ZIGCRAFT_SAFE_MODE enabled: keeping render distance {} with reduced GPU pressure", .{effective_render_distance}); } else if (safe_mode) { - log.log.warn("Wayland stability profile active: keeping configured render distance {} and LOD behavior", .{effective_render_distance}); + log.log.warn("Wayland stability profile active: keeping configured render distance {}", .{effective_render_distance}); } if (build_config.chunk_debug_mode) { log.log.warn("CHUNK DEBUG MODE enabled: restore='{s}'", .{build_config.chunk_debug_enable}); } - const preset_cfg = render_settings.getPresetConfig(render_distance_preset); - - const effective_horizon_distance = LODConfig.normalizeHorizonDistance(effective_render_distance, horizon_distance); - const manual_distance_expanded = effective_render_distance > preset_cfg.lod_radii[0] or effective_horizon_distance != preset_cfg.horizon_radius; - const chunk_render_radius = if (strict_safe_mode) - @min(effective_render_distance, 8) - else if (effective_lod_enabled and manual_distance_expanded) - @min(effective_render_distance, preset_cfg.lod_radii[0]) - else - effective_render_distance; - var preset_radii = if (strict_safe_mode) - LODConfig.radiiForDistances(chunk_render_radius, @max(effective_horizon_distance, 64)) - else if (effective_lod_enabled and manual_distance_expanded) - LODConfig.radiiForDistances(chunk_render_radius, effective_horizon_distance) - else - preset_cfg.lod_radii; - - const active_count = preset_cfg.active_lod_count; - if (active_count < LODLevel.count) { - var i: usize = active_count; - while (i < LODLevel.count) : (i += 1) { - preset_radii[i] = preset_radii[active_count - 1]; - } - } - - var lod_config = if (strict_safe_mode) - LODConfig{ - .chunk_render_radius = chunk_render_radius, - .radii = preset_radii, - .memory_budget_mb = @min(preset_cfg.memory_budget_mb, 256), - .max_uploads_per_frame = @min(preset_cfg.max_uploads_per_frame, 8), - .compact_tiles_enabled = compact_tiles_enabled, - } - else - LODConfig{ - .chunk_render_radius = chunk_render_radius, - .radii = preset_radii, - .fog_start_percent = preset_cfg.fog_start_percent, - .horizontal_detail = preset_cfg.horizontal_detail, - .sample_density = preset_cfg.sample_density, - .compact_tiles_enabled = compact_tiles_enabled, - .vertical_span_budget = preset_cfg.vertical_span_budget, - .mesh_path = preset_cfg.mesh_path, - .qem_triangle_targets = preset_cfg.qem_targets, - .memory_budget_mb = preset_cfg.memory_budget_mb, - .lod_store_size_cap_mb = preset_cfg.lod_store_size_cap_mb, - .max_uploads_per_frame = preset_cfg.max_uploads_per_frame, - .skip_cutout_lod2 = preset_cfg.skip_cutout_lod2, - .skip_lighting_lod3 = preset_cfg.skip_lighting_lod3, - .active_lod_count = active_count, - }; - - if (build_config.benchmark_lod_memory_budget_mb > 0) { - lod_config.memory_budget_mb = build_config.benchmark_lod_memory_budget_mb; - log.log.info("BENCHMARK: overriding LOD memory budget to {} MiB", .{lod_config.memory_budget_mb}); - } - session.* = undefined; - session.lod_config = lod_config; const world = try World.init(.{ .allocator = allocator, @@ -230,7 +117,6 @@ pub const GameSession = struct { .rhi = rhi.*, .atlas = atlas, .generator_index = generator_index, - .lod_config = if (effective_lod_enabled) session.lod_config.interface() else null, }); errdefer world.deinit(); @@ -263,7 +149,7 @@ pub const GameSession = struct { const spawn = findActualSpawnColumn(world_sim, seed_spawn.x, seed_spawn.z) orelse seed_spawn; const spawn_y: f32 = @floatFromInt(spawn.info.height + 16); var player = Player.init(Vec3.init(@floatFromInt(spawn.x), spawn_y, @floatFromInt(spawn.z)), true); - player.camera.far = cameraFarPlaneForDistances(effective_render_distance, effective_horizon_distance); + player.camera.far = cameraFarPlaneForRenderDistance(effective_render_distance); // Aim toward the terrain so the first frame shows the ground. player.camera.setYawPitch(player.camera.yaw, -std.math.degreesToRadians(35.0)); @@ -276,11 +162,6 @@ pub const GameSession = struct { player.position = scene.position; player.camera.position = player.getEyePosition(); player.camera.setYawPitch(scene.yaw, scene.pitch); - } else if (phase5VisualScene(build_config.phase5_visual_scene)) |scene| { - atmosphere.time.time_scale = 0.0; - player.position = scene.position; - player.camera.position = player.getEyePosition(); - player.camera.setYawPitch(scene.yaw, scene.pitch); } session.* = .{ @@ -299,7 +180,6 @@ pub const GameSession = struct { .ecs_render_system = ecs_render_system, .rhi = rhi, .atmosphere = atmosphere, - .lod_config = session.lod_config, .creative_mode = true, .build_config = build_config, }; @@ -407,94 +287,9 @@ pub const GameSession = struct { } if (!skip_world) { - if (!self.gpu_culling_scale_fixture_installed and std.ascii.eqlIgnoreCase(self.build_config.benchmark_fixture, "gpu-culling-scale")) { - // The Vulkan transfer queue is frame-owned. Install after - // App begins this frame so all 1024 compact-pool uploads - // use the production lifetime/synchronization path. - try self.world.installGpuCullingScaleFixture(); - self.gpu_culling_scale_fixture_installed = true; - } - if (phase5VisualScene(self.build_config.phase5_visual_scene)) |scene| { - // This is a production flat-world launch, not the shadow - // test renderer. Reapply the deterministic pose because - // normal player simulation is still active in captures. - // Motion scenes advance a bounded frame-script here rather - // than being static labels passed to the capture runner. - self.applyPhase5VisualPose(scene); - } const world_sim = self.world.interface().simulation(); try world_sim.update(self.player.camera.position, dt); - if (!self.phase5_fixture_applied and (self.build_config.phase5_visual_scene.len > 0 or (self.build_config.benchmark_fixture.len > 0 and !std.ascii.eqlIgnoreCase(self.build_config.benchmark_fixture, "gpu-culling-scale")))) { - try self.applyPhase5VisualFixture(world_sim); - } - - // The Phase 5 visual gate uses the fixed test-world pool as a - // full-detail fluid fixture. Count resident water cells before - // capture so its image ROI is backed by runtime evidence, not - // merely a requested camera pose. - if (self.build_config.screenshot_path.len > 0 and (std.ascii.eqlIgnoreCase(self.build_config.phase5_visual_scene, "water") or isSavedWorldScene(self.build_config.phase5_visual_scene))) { - const fixture = if (std.ascii.eqlIgnoreCase(self.build_config.phase5_visual_scene, "water")) waterFixtureCells() else savedWorldWetCells(); - var observed: u32 = 0; - for (fixture) |cell| { - if (world_sim.getBlock(cell[0], cell[1], cell[2]) == .water) observed += 1; - } - self.phase5_water_fixture_observations +%= observed; - if (!self.phase5_water_fixture_evidence_emitted and self.phase5_water_fixture_observations >= @as(u32, @intCast(fixture.len))) { - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_WATER_FIXTURE: run={s} scene={s} observations={} fixture_cells={}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, self.phase5_water_fixture_observations, fixture.len }); - std.debug.print("PHASE5_WATER_FIXTURE: run={s} scene={s} observations={} fixture_cells={}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, self.phase5_water_fixture_observations, fixture.len }); - } - self.phase5_water_fixture_evidence_emitted = true; - } - } - - if (!self.phase5_gpu_evidence_emitted and phase5SceneRequiresGpuValidation(self.build_config.phase5_visual_scene) and (!phase5SceneHasMotion(self.build_config.phase5_visual_scene) or self.phase5_motion_complete)) { - if (self.world.getLODStats()) |lod_stats| { - const candidates = lod_stats.gpu_terrain_candidates + lod_stats.gpu_fluid_candidates; - if (candidates > 0) { - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_GPU_CULLING: run={s} scene={s} candidates={} validation_mismatches={} overflows={}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, candidates, lod_stats.gpu_culling_validation_mismatches, lod_stats.gpu_culling_overflows }); - std.debug.print("PHASE5_GPU_CULLING: run={s} scene={s} candidates={} validation_mismatches={} overflows={}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, candidates, lod_stats.gpu_culling_validation_mismatches, lod_stats.gpu_culling_overflows }); - } - self.phase5_gpu_evidence_emitted = true; - } - } - } - - if (self.phase5_gpu_evidence_emitted and !self.phase5_gpu_validation_complete_emitted and getenv("ZIGCRAFT_LOD_GPU_CULLING_VALIDATE") != null) { - // A queued generation is not validation evidence. The RHI - // advances these counters only after consuming the mapped - // readback when its frame slot is reused. - if (phase5EvidenceEnabled(self.build_config)) if (self.world.getLODStats()) |lod_stats| { - const completed_generation = lod_stats.gpu_culling_validation_completed_generation; - const completed_count = lod_stats.gpu_culling_validation_completed_count; - if (completed_generation > 0 and completed_count > 0 and completed_generation <= lod_stats.gpu_culling_validation_generation) { - log.log.warn("PHASE5_GPU_VALIDATION_COMPLETE: run={s} scene={s} generation={} validations={} validation_mismatches={}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, completed_generation, completed_count, lod_stats.gpu_culling_validation_mismatches }); - std.debug.print("PHASE5_GPU_VALIDATION_COMPLETE: run={s} scene={s} generation={} validations={} validation_mismatches={}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, completed_generation, completed_count, lod_stats.gpu_culling_validation_mismatches }); - self.phase5_gpu_validation_complete_emitted = true; - } - }; - } - - if (isSavedWorldReloadScene(self.build_config.phase5_visual_scene) and self.phase5_save_loaded and !self.phase5_compact_wet_dry_evidence_emitted) { - if (self.world.getLODStats()) |lod_stats| { - // The persisted wet fixture proves reload from the - // save, while direct-mesh bytes keep unsupported - // shoreline topology on the expanded fallback. Dry - // distant terrain must have live compact residency. - if (lod_stats.compact_pool_allocated_bytes > 0 and lod_stats.direct_mesh_gpu_bytes > 0) { - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_COMPACT_WET_DRY: run={s} scene={s} dry_compact_bytes={} wet_fallback_bytes={} wet_cells={} dry_cells={}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, lod_stats.compact_pool_allocated_bytes, lod_stats.direct_mesh_gpu_bytes, savedWorldWetCells().len, savedWorldDryCells().len }); - std.debug.print("PHASE5_COMPACT_WET_DRY: run={s} scene={s} dry_compact_bytes={} wet_fallback_bytes={} wet_cells={} dry_cells={}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, lod_stats.compact_pool_allocated_bytes, lod_stats.direct_mesh_gpu_bytes, savedWorldWetCells().len, savedWorldDryCells().len }); - } - self.phase5_compact_wet_dry_evidence_emitted = true; - } - } - } - - self.updatePhase5CaptureReadiness(); - // ECS Updates ECSPhysicsSystem.update(&self.ecs_registry, world_sim.collisionWorld(), dt); } @@ -512,212 +307,8 @@ pub const GameSession = struct { pub fn drawHUD(self: *GameSession, ui: *UISystem, atlas: *const TextureAtlas, active_pack: ?[]const u8, fps: f32, screen_w: f32, screen_h: f32, mouse_x: f32, mouse_y: f32, mouse_clicked: bool) !void { try session_hud.draw(self, ui, atlas, active_pack, fps, screen_w, screen_h, mouse_x, mouse_y, mouse_clicked); } - - fn applyPhase5VisualFixture(self: *GameSession, world_sim: IWorldSimulation) !void { - const scene = if (self.build_config.phase5_visual_scene.len > 0) self.build_config.phase5_visual_scene else self.build_config.benchmark_fixture; - // Mutations are deliberately small, local, and above the flat-world - // grass. They make the production launch visually identifiable while - // retaining the normal generator, streamer, mesher, and LOD pipeline. - if (std.ascii.eqlIgnoreCase(scene, "saved-world-reload")) { - const wet_loaded = fixtureCellsMatch(world_sim, savedWorldWetCells(), .water); - const dry_loaded = fixtureCellsMatch(world_sim, savedWorldDryCells(), .white_terracotta); - if (wet_loaded and dry_loaded) { - self.phase5_fixture_applied = true; - self.phase5_save_loaded = true; - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_SAVE_LOADED: run={s} scene={s} wet_cells={} dry_cells={}", .{ self.build_config.phase5_visual_run_id, scene, savedWorldWetCells().len, savedWorldDryCells().len }); - std.debug.print("PHASE5_SAVE_LOADED: run={s} scene={s} wet_cells={} dry_cells={}\n", .{ self.build_config.phase5_visual_run_id, scene, savedWorldWetCells().len, savedWorldDryCells().len }); - log.log.warn("PHASE5_FIXTURE: run={s} scene={s} applied=1", .{ self.build_config.phase5_visual_run_id, scene }); - std.debug.print("PHASE5_FIXTURE: run={s} scene={s} applied=1\n", .{ self.build_config.phase5_visual_run_id, scene }); - } - } - return; - } else if (std.ascii.eqlIgnoreCase(scene, "seam")) { - for (15..17) |x| for (4..12) |z| for (65..70) |y| { - try world_sim.setBlock(@intCast(x), @intCast(y), @intCast(z), if (x == 15) .stone else .white_terracotta); - }; - } else if (std.ascii.eqlIgnoreCase(scene, "water")) { - for (20..31) |x| for (0..10) |z| { - try world_sim.setBlock(@intCast(x), 65, @intCast(z), .water); - }; - } else if (std.ascii.eqlIgnoreCase(scene, "lod-handoff") or std.ascii.eqlIgnoreCase(scene, "lod-aerial") or std.ascii.eqlIgnoreCase(scene, "lod-handoff-traversal") or std.ascii.eqlIgnoreCase(scene, "teleport-handoff")) { - const motion_scene = std.ascii.eqlIgnoreCase(scene, "lod-handoff-traversal") or std.ascii.eqlIgnoreCase(scene, "teleport-handoff"); - const base_x: i32 = if (motion_scene) -130 else -2; - const base_z: i32 = if (motion_scene) -24 else 8; - for (0..5) |x| for (0..5) |z| for (65..76) |y| { - if (x == 0 or x == 4 or z == 0 or z == 4 or y == 75) try world_sim.setBlock(base_x + @as(i32, @intCast(x)), @intCast(y), base_z + @as(i32, @intCast(z)), .stone); - }; - } else if (std.ascii.eqlIgnoreCase(scene, "fog-rapid-turn")) { - // A fixed horizon landmark makes the rotation exercise visible in - // the production world while the changing view drives frustum/fog - // churn through the normal renderer. - for (0..5) |x| for (65..82) |y| { - try world_sim.setBlock(@as(i32, @intCast(x)) - 2, @intCast(y), 32, if (y % 2 == 0) .stone else .white_terracotta); - }; - } else if (std.ascii.eqlIgnoreCase(scene, "saved-world-create")) { - for (18..33) |x| for (0..12) |z_offset| { - try world_sim.setBlock(@intCast(x), 65, @as(i32, @intCast(z_offset)) - 76, .water); - }; - for (0..10) |x| for (0..4) |z_offset| for (65..71) |y| { - try world_sim.setBlock(@intCast(x), @intCast(y), @as(i32, @intCast(z_offset)) - 64, .white_terracotta); - }; - for (savedWorldWetCells()) |cell| try world_sim.setBlock(cell[0], cell[1], cell[2], .water); - for (savedWorldDryCells()) |cell| try world_sim.setBlock(cell[0], cell[1], cell[2], .white_terracotta); - } else { - log.log.warn("Unknown Phase 5 visual scene '{s}'", .{scene}); - return; - } - const fixture_applied = if (std.ascii.eqlIgnoreCase(scene, "saved-world-create")) - fixtureCellsMatch(world_sim, savedWorldWetCells(), .water) and fixtureCellsMatch(world_sim, savedWorldDryCells(), .white_terracotta) - else blk: { - const expected = if (std.ascii.eqlIgnoreCase(scene, "seam")) BlockType.stone else if (std.ascii.eqlIgnoreCase(scene, "water")) BlockType.water else BlockType.stone; - const probe = if (std.ascii.eqlIgnoreCase(scene, "seam")) [3]i32{ 15, 65, 4 } else if (std.ascii.eqlIgnoreCase(scene, "water")) [3]i32{ 25, 65, 4 } else if (std.ascii.eqlIgnoreCase(scene, "fog-rapid-turn")) [3]i32{ -2, 66, 32 } else if (std.ascii.eqlIgnoreCase(scene, "lod-handoff-traversal") or std.ascii.eqlIgnoreCase(scene, "teleport-handoff")) [3]i32{ -130, 65, -24 } else [3]i32{ -2, 65, 8 }; - break :blk world_sim.getBlock(probe[0], probe[1], probe[2]) == expected; - }; - if (fixture_applied) { - self.phase5_fixture_applied = true; - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_FIXTURE: run={s} scene={s} applied=1", .{ self.build_config.phase5_visual_run_id, scene }); - std.debug.print("PHASE5_FIXTURE: run={s} scene={s} applied=1\n", .{ self.build_config.phase5_visual_run_id, scene }); - } - if (std.ascii.eqlIgnoreCase(scene, "saved-world-create")) { - self.world.saveAllModifiedChunks(); - const failures = self.world.takeSaveFailureWarningCount(); - if (failures == 0) { - self.phase5_save_flushed = true; - if (phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_SAVE_FLUSH: run={s} scene={s} saved=1 failures=0", .{ self.build_config.phase5_visual_run_id, scene }); - std.debug.print("PHASE5_SAVE_FLUSH: run={s} scene={s} saved=1 failures=0\n", .{ self.build_config.phase5_visual_run_id, scene }); - } - } - } - } - } - - fn updatePhase5CaptureReadiness(self: *GameSession) void { - if (self.build_config.phase5_visual_scene.len == 0) return; - const scene = phase5VisualScene(self.build_config.phase5_visual_scene) orelse { - self.phase5_settle_frames = 0; - return; - }; - - const world_stats = self.world.getStats(); - const render_stats = self.world.getRenderStats(); - const lod_stats = self.world.getLODStats() orelse { - self.phase5_settle_frames = 0; - return; - }; - const queues_settled = world_stats.gen_queue == 0 and world_stats.mesh_queue == 0 and world_stats.upload_queue == 0 and - allZero(lod_stats.generating) and allZero(lod_stats.generated) and allZero(lod_stats.meshing) and allZero(lod_stats.mesh_ready) and allZero(lod_stats.uploading) and - allZero(lod_stats.gen_queue_depth) and allZero(lod_stats.upload_queue_depth) and lod_stats.upgrades_pending == 0 and lod_stats.downgrades_pending == 0 and lod_stats.ingestion_backlog == 0; - const renderable = render_stats.chunks_rendered > 0 and lod_stats.totalLoaded() > 0; - const motion_ready = scene.motion == .fixed or self.phase5_motion_complete; - const saved_world_ready = if (std.ascii.eqlIgnoreCase(self.build_config.phase5_visual_scene, "saved-world-create")) - self.phase5_save_flushed - else if (isSavedWorldReloadScene(self.build_config.phase5_visual_scene)) - self.phase5_save_loaded and self.phase5_compact_wet_dry_evidence_emitted and self.phase5_gpu_validation_complete_emitted - else if (phase5SceneRequiresGpuValidation(self.build_config.phase5_visual_scene)) - self.phase5_gpu_validation_complete_emitted - else - true; - if (!self.phase5_fixture_applied or !motion_ready or !queues_settled or !renderable or !saved_world_ready) { - self.phase5_settle_frames = 0; - return; - } - - self.phase5_settle_frames +|= 1; - if (self.phase5_settle_frames < phase5SettleFrameTarget()) return; - phase5_capture_ready.store(true, .release); - if (!self.phase5_ready_evidence_emitted and phase5EvidenceEnabled(self.build_config)) { - log.log.warn("PHASE5_READY: run={s} scene={s} stable_frames={} chunks_rendered={} lod_loaded={}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, self.phase5_settle_frames, render_stats.chunks_rendered, lod_stats.totalLoaded() }); - std.debug.print("PHASE5_READY: run={s} scene={s} stable_frames={} chunks_rendered={} lod_loaded={}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, self.phase5_settle_frames, render_stats.chunks_rendered, lod_stats.totalLoaded() }); - self.phase5_ready_evidence_emitted = true; - } - } - - fn applyPhase5VisualPose(self: *GameSession, scene: Phase5VisualScene) void { - const pose = phase5VisualPoseAtFrame(scene, self.phase5_motion_frames); - self.player.position = pose.position; - self.player.camera.position = self.player.getEyePosition(); - self.player.camera.setYawPitch(pose.yaw, pose.pitch); - self.camera = self.player.camera; - - // Hold the initial pose until the identifying fixture is actually - // resident and verified. Advancing immediately can stream the origin - // out before `setBlock` succeeds, making readiness impossible no - // matter how long the screenshot waits. - if (scene.motion == .fixed or self.phase5_motion_complete or !self.phase5_fixture_applied) return; - - self.phase5_motion_frames +|= 1; - if (self.phase5_motion_frames < phase5MotionFrameTarget(scene.motion)) return; - - self.phase5_motion_complete = true; - if (!self.phase5_motion_evidence_emitted and phase5EvidenceEnabled(self.build_config)) { - const movement = phase5MotionEvidence(scene.motion); - log.log.warn("PHASE5_MOTION: run={s} scene={s} kind={s} completed=1 frames={} distance={d:.1} yaw_degrees={d:.1}", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, scene.motion.name(), self.phase5_motion_frames, movement.distance, movement.yaw_degrees }); - std.debug.print("PHASE5_MOTION: run={s} scene={s} kind={s} completed=1 frames={} distance={d:.1} yaw_degrees={d:.1}\n", .{ self.build_config.phase5_visual_run_id, self.build_config.phase5_visual_scene, scene.motion.name(), self.phase5_motion_frames, movement.distance, movement.yaw_degrees }); - self.phase5_motion_evidence_emitted = true; - } - } }; -fn allZero(values: anytype) bool { - for (values) |value| if (value != 0) return false; - return true; -} - -fn waterFixtureCells() []const [3]i32 { - return &.{ .{ 22, 65, 4 }, .{ 25, 65, 4 }, .{ 28, 65, 4 } }; -} - -/// These cells span a wet pool and a contrasting dry wall in two persisted -/// chunks. Reload probes every one, so generated-world coincidence cannot -/// satisfy the saved-world qualification. -fn savedWorldWetCells() []const [3]i32 { - return &.{ .{ 20, 65, -72 }, .{ 25, 65, -72 }, .{ 30, 65, -72 } }; -} - -fn savedWorldDryCells() []const [3]i32 { - return &.{ .{ 0, 65, -64 }, .{ 4, 67, -64 }, .{ 8, 69, -64 } }; -} - -fn fixtureCellsMatch(world_sim: IWorldSimulation, cells: []const [3]i32, expected: BlockType) bool { - for (cells) |cell| { - if (world_sim.getBlock(cell[0], cell[1], cell[2]) != expected) return false; - } - return true; -} - -fn isSavedWorldScene(scene: []const u8) bool { - return std.ascii.eqlIgnoreCase(scene, "saved-world-create") or isSavedWorldReloadScene(scene); -} - -fn isSavedWorldReloadScene(scene: []const u8) bool { - return std.ascii.eqlIgnoreCase(scene, "saved-world-reload"); -} - -fn phase5SceneRequiresGpuValidation(scene: []const u8) bool { - if (std.ascii.eqlIgnoreCase(scene, "lod-handoff") or isSavedWorldReloadScene(scene)) return true; - if (phase5VisualScene(scene)) |visual_scene| return visual_scene.motion != .fixed; - return false; -} - -fn phase5SceneHasMotion(scene: []const u8) bool { - if (phase5VisualScene(scene)) |visual_scene| return visual_scene.motion != .fixed; - return false; -} - -fn phase5SettleFrameTarget() u32 { - if (getenv("ZIGCRAFT_PHASE5_SETTLE_FRAMES")) |value| { - return std.fmt.parseInt(u32, value, 10) catch 180; - } - return 180; -} - -fn phase5EvidenceEnabled(build_config: BuildConfig) bool { - return build_config.phase5_visual_scene.len > 0 and build_config.phase5_visual_run_id.len > 0; -} - const LightingBaselineScene = struct { position: Vec3, yaw: f32, @@ -725,99 +316,6 @@ const LightingBaselineScene = struct { time_of_day: f32, }; -pub const Phase5VisualMotion = enum { - fixed, - lod_handoff_traversal, - fog_rapid_turn, - teleport_handoff, - - pub fn name(self: Phase5VisualMotion) []const u8 { - return switch (self) { - .fixed => "fixed", - .lod_handoff_traversal => "traversal", - .fog_rapid_turn => "rapid-turn", - .teleport_handoff => "teleport", - }; - } -}; - -pub const Phase5VisualScene = struct { - position: Vec3, - yaw: f32, - pitch: f32, - motion: Phase5VisualMotion = .fixed, -}; - -pub fn parsePhase5VisualScene(name: []const u8) ?Phase5VisualScene { - const forward_z = std.math.pi / 2.0; - if (std.ascii.eqlIgnoreCase(name, "seam")) return .{ .position = Vec3.init(16.0, 74.0, -18.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(15.0) }; - if (std.ascii.eqlIgnoreCase(name, "water")) return .{ .position = Vec3.init(25.0, 75.0, -16.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(17.0) }; - if (std.ascii.eqlIgnoreCase(name, "lod-handoff")) return .{ .position = Vec3.init(0.0, 110.0, -80.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(13.0) }; - if (std.ascii.eqlIgnoreCase(name, "lod-aerial")) return .{ .position = Vec3.init(0.0, 900.0, -100.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(60.0) }; - if (std.ascii.eqlIgnoreCase(name, "saved-world-create") or std.ascii.eqlIgnoreCase(name, "saved-world-reload")) return .{ .position = Vec3.init(8.0, 78.0, -88.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(18.0) }; - if (std.ascii.eqlIgnoreCase(name, "lod-handoff-traversal")) return .{ .position = Vec3.init(-128.0, 110.0, -32.0), .yaw = 0.0, .pitch = -std.math.degreesToRadians(13.0), .motion = .lod_handoff_traversal }; - if (std.ascii.eqlIgnoreCase(name, "fog-rapid-turn")) return .{ .position = Vec3.init(0.0, 110.0, 0.0), .yaw = -std.math.pi, .pitch = -std.math.degreesToRadians(10.0), .motion = .fog_rapid_turn }; - if (std.ascii.eqlIgnoreCase(name, "teleport-handoff")) return .{ .position = Vec3.init(-128.0, 110.0, -32.0), .yaw = 0.0, .pitch = -std.math.degreesToRadians(13.0), .motion = .teleport_handoff }; - return null; -} - -fn phase5VisualScene(name: []const u8) ?Phase5VisualScene { - return parsePhase5VisualScene(name); -} - -fn phase5VisualPoseAtFrame(scene: Phase5VisualScene, frame: u32) Phase5VisualScene { - const target_frames = phase5MotionFrameTarget(scene.motion); - const clamped_frame = @min(frame, target_frames); - const progress: f32 = @as(f32, @floatFromInt(clamped_frame)) / @as(f32, @floatFromInt(target_frames)); - var pose = scene; - switch (scene.motion) { - .fixed => {}, - .lod_handoff_traversal => pose.position.x += 256.0 * progress, - .fog_rapid_turn => pose.yaw += std.math.tau * 2.0 * progress, - .teleport_handoff => { - if (frame >= 60) pose.position = Vec3.init(128.0, 110.0, 32.0); - }, - } - return pose; -} - -fn phase5MotionFrameTarget(motion: Phase5VisualMotion) u32 { - return switch (motion) { - .fixed => 1, - .lod_handoff_traversal => 180, - .fog_rapid_turn => 160, - .teleport_handoff => 120, - }; -} - -fn phase5MotionEvidence(motion: Phase5VisualMotion) struct { distance: f32, yaw_degrees: f32 } { - return switch (motion) { - .fixed => .{ .distance = 0.0, .yaw_degrees = 0.0 }, - .lod_handoff_traversal => .{ .distance = 256.0, .yaw_degrees = 0.0 }, - .fog_rapid_turn => .{ .distance = 0.0, .yaw_degrees = 720.0 }, - .teleport_handoff => .{ .distance = 262.9, .yaw_degrees = 0.0 }, - }; -} - -test "Phase 5 visual scene parser exposes bounded motion poses" { - const aerial = parsePhase5VisualScene("lod-aerial").?; - const traversal = parsePhase5VisualScene("lod-handoff-traversal").?; - const turn = parsePhase5VisualScene("fog-rapid-turn").?; - const teleport = parsePhase5VisualScene("teleport-handoff").?; - try std.testing.expectEqual(Phase5VisualMotion.lod_handoff_traversal, traversal.motion); - try std.testing.expectEqual(Phase5VisualMotion.fog_rapid_turn, turn.motion); - try std.testing.expectEqual(Phase5VisualMotion.teleport_handoff, teleport.motion); - try std.testing.expect(aerial.position.y >= 900.0); - try std.testing.expect(parsePhase5VisualScene("unbounded-motion") == null); - - const traversal_end = phase5VisualPoseAtFrame(traversal, phase5MotionFrameTarget(traversal.motion)); - try std.testing.expect(traversal_end.position.x > traversal.position.x); - const turn_end = phase5VisualPoseAtFrame(turn, phase5MotionFrameTarget(turn.motion)); - try std.testing.expect(turn_end.yaw > turn.yaw); - const teleport_end = phase5VisualPoseAtFrame(teleport, phase5MotionFrameTarget(teleport.motion)); - try std.testing.expect(teleport_end.position.x != teleport.position.x); -} - fn lightingBaselineScene(name: []const u8) LightingBaselineScene { const forward_z = std.math.pi / 2.0; const slight_down = -std.math.degreesToRadians(8.0); @@ -828,9 +326,6 @@ fn lightingBaselineScene(name: []const u8) LightingBaselineScene { if (std.ascii.eqlIgnoreCase(name, "foliage-cutout")) return .{ .position = Vec3.init(12.0, 67.0, 10.0), .yaw = forward_z, .pitch = slight_down, .time_of_day = 0.5 }; if (std.ascii.eqlIgnoreCase(name, "water")) return .{ .position = Vec3.init(25.0, 68.0, -12.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(18.0), .time_of_day = 0.5 }; if (std.ascii.eqlIgnoreCase(name, "cross-chunk-corridor")) return .{ .position = Vec3.init(10.0, 65.0, 32.0), .yaw = 0.0, .pitch = 0.0, .time_of_day = 0.5 }; - // Looks over the authored fixtures from a resident full-detail area into - // the LOD horizon. The scene keeps time fixed and has no input-driven pose. - if (std.ascii.eqlIgnoreCase(name, "lod-handoff")) return .{ .position = Vec3.init(0.0, 112.0, -96.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(12.0), .time_of_day = 0.5 }; if (std.ascii.eqlIgnoreCase(name, "bend")) return .{ .position = Vec3.init(5.5, 65.0, -14.0), .yaw = forward_z, .pitch = slight_down, .time_of_day = 0.5 }; if (std.ascii.eqlIgnoreCase(name, "noon")) return .{ .position = Vec3.init(0.0, 68.0, 18.0), .yaw = forward_z, .pitch = slight_down, .time_of_day = 0.5 }; return .{ .position = Vec3.init(0.0, 65.0, -16.0), .yaw = forward_z, .pitch = -std.math.degreesToRadians(5.0), .time_of_day = 0.5 }; diff --git a/modules/game-core/src/settings.zig b/modules/game-core/src/settings.zig index 285d3176..64fb85c2 100644 --- a/modules/game-core/src/settings.zig +++ b/modules/game-core/src/settings.zig @@ -18,4 +18,3 @@ pub const PresetConfig = json_presets.PresetConfig; // Data-driven settings support pub const SettingMetadata = data.SettingMetadata; -pub const sanitizeRuntimeConflicts = data.sanitizeRuntimeConflicts; diff --git a/modules/game-core/src/settings/data.zig b/modules/game-core/src/settings/data.zig index 302686ea..4a70de8a 100644 --- a/modules/game-core/src/settings/data.zig +++ b/modules/game-core/src/settings/data.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const RenderDistancePreset = @import("engine-rhi").RenderDistancePreset; pub const ShadowDebugChannel = enum(u32) { off = 0, @@ -48,15 +47,6 @@ pub fn clearTerrainDebugViews(settings: *@This().Settings) void { settings.debug_outdoor_factor_active = false; } -pub fn sanitizeRuntimeConflicts(settings: *@This().Settings) bool { - if (settings.lod_enabled and settings.taa_enabled) { - settings.taa_enabled = false; - settings.fxaa_enabled = true; - return true; - } - return false; -} - pub const ShadowQuality = struct { resolution: u32, label: []const u8, @@ -86,8 +76,9 @@ pub const RESOLUTIONS = [_]Resolution{ }; pub const Settings = struct { + /// Full-detail chunk streaming radius. This is the only distance policy; + /// distant terrain is not represented by a secondary LOD range. render_distance: i32 = 15, - horizon_distance: i32 = 512, mouse_sensitivity: f32 = 50.0, vsync: bool = true, fov: f32 = 45.0, @@ -117,8 +108,6 @@ pub const Settings = struct { ui_scale: f32 = 1.0, // Manual UI scale multiplier (0.5 to 2.0) window_width: u32 = 1920, window_height: u32 = 1080, - lod_enabled: bool = true, - render_distance_preset: RenderDistancePreset = .high, texture_pack: []const u8 = "default", environment_map: []const u8 = "default", // "default" or filename.exr/hdr @@ -200,11 +189,6 @@ pub const Settings = struct { .label = "RENDER DISTANCE", .kind = .{ .int_range = .{ .min = 2, .max = std.math.maxInt(i32), .step = 1 } }, }; - pub const horizon_distance = SettingMetadata{ - .label = "DISTANT LOD LIMIT", - .description = "Maximum distant-terrain radius from the player", - .kind = .{ .int_range = .{ .min = 256, .max = 512, .step = 2 } }, - }; pub const mouse_sensitivity = SettingMetadata{ .label = "SENSITIVITY", .kind = .{ .slider = .{ .min = 1.0, .max = 200.0, .step = 1.0 } }, @@ -297,11 +281,6 @@ pub const Settings = struct { .values = &[_]u32{ 16, 32, 64, 128, 256, 512 }, } }, }; - pub const lod_enabled = SettingMetadata{ - .label = "LOD SYSTEM", - .description = "Enables high-distance simplified terrain rendering", - .kind = .toggle, - }; pub const ssao_enabled = SettingMetadata{ .label = "SSAO", .kind = .toggle, diff --git a/modules/game-core/src/settings/json_presets.zig b/modules/game-core/src/settings/json_presets.zig index 7c3aa912..857733cc 100644 --- a/modules/game-core/src/settings/json_presets.zig +++ b/modules/game-core/src/settings/json_presets.zig @@ -2,7 +2,6 @@ const std = @import("std"); const fs = @import("fs"); const data = @import("data.zig"); const Settings = data.Settings; -const RenderDistancePreset = @import("engine-rhi").RenderDistancePreset; const log = @import("engine-core").log; const sync = @import("engine-core").sync; @@ -39,10 +38,7 @@ pub const PresetConfig = struct { lpv_propagation_iterations: u32 = 3, clouds_enabled: bool = true, clouds_3d_enabled: bool = true, - lod_enabled: bool, render_distance: i32, - horizon_distance: ?i32 = null, - render_distance_preset: RenderDistancePreset = .high, fxaa_enabled: bool, bloom_enabled: bool, bloom_intensity: f32, @@ -122,12 +118,6 @@ pub fn initPresets(allocator: std.mem.Allocator) !void { log.log.warn("Skipping preset '{s}': invalid render_distance {}", .{ p.name, p.render_distance }); continue; } - if (p.horizon_distance) |horizon_distance| { - if (horizon_distance < 1) { - log.log.warn("Skipping preset '{s}': invalid horizon_distance {}", .{ p.name, horizon_distance }); - continue; - } - } p.name = try allocator.dupe(u8, preset.name); errdefer allocator.free(p.name); try graphics_presets.append(allocator, p); @@ -191,14 +181,10 @@ fn applyConfig(settings: *Settings, config: PresetConfig) void { settings.lpv_propagation_iterations = config.lpv_propagation_iterations; settings.clouds_enabled = config.clouds_enabled; settings.clouds_3d_enabled = config.clouds_3d_enabled; - settings.lod_enabled = config.lod_enabled; settings.render_distance = config.render_distance; - settings.horizon_distance = config.horizon_distance orelse @import("engine-rhi").render_settings.getPresetConfig(config.render_distance_preset).horizon_radius; - settings.render_distance_preset = config.render_distance_preset; settings.fxaa_enabled = config.fxaa_enabled and !config.taa_enabled; settings.bloom_enabled = config.bloom_enabled; settings.bloom_intensity = config.bloom_intensity; - _ = data.sanitizeRuntimeConflicts(settings); } pub fn getIndex(settings: *const Settings) usize { @@ -232,8 +218,6 @@ fn matches(settings: *const Settings, preset: PresetConfig) bool { std.math.approxEqAbs(f32, settings.exposure, preset.exposure, epsilon) and std.math.approxEqAbs(f32, settings.saturation, preset.saturation, epsilon) and settings.render_distance == preset.render_distance and - settings.horizon_distance == (preset.horizon_distance orelse @import("engine-rhi").render_settings.getPresetConfig(preset.render_distance_preset).horizon_radius) and - settings.render_distance_preset == preset.render_distance_preset and settings.volumetric_lighting_enabled == preset.volumetric_lighting_enabled and std.math.approxEqAbs(f32, settings.volumetric_density, preset.volumetric_density, epsilon) and settings.volumetric_steps == preset.volumetric_steps and @@ -246,7 +230,6 @@ fn matches(settings: *const Settings, preset: PresetConfig) bool { settings.lpv_propagation_iterations == preset.lpv_propagation_iterations and settings.clouds_enabled == preset.clouds_enabled and settings.clouds_3d_enabled == preset.clouds_3d_enabled and - settings.lod_enabled == preset.lod_enabled and settings.fxaa_enabled == preset.fxaa_enabled and settings.bloom_enabled == preset.bloom_enabled and std.math.approxEqAbs(f32, settings.bloom_intensity, preset.bloom_intensity, epsilon); @@ -272,7 +255,7 @@ pub fn findAndApplyNamed(settings: *Settings, preset_name: []const u8) ?[]const defer graphics_presets_mutex.unlock(); for (graphics_presets.items) |preset| { - if (std.ascii.eqlIgnoreCase(preset.name, preset_name) or std.ascii.eqlIgnoreCase(@tagName(preset.render_distance_preset), preset_name)) { + if (std.ascii.eqlIgnoreCase(preset.name, preset_name)) { applyConfig(settings, preset); return preset.name; } diff --git a/modules/game-core/src/settings/persistence.zig b/modules/game-core/src/settings/persistence.zig index 3dfd3127..1665454a 100644 --- a/modules/game-core/src/settings/persistence.zig +++ b/modules/game-core/src/settings/persistence.zig @@ -50,43 +50,32 @@ pub fn load(allocator: std.mem.Allocator) Settings { }; defer allocator.free(content); - const parsed = std.json.parseFromSlice(Settings, allocator, content, .{ - .ignore_unknown_fields = true, - }) catch |err| { + const settings = parseSettingsJson(allocator, content) catch |err| { log.log.warn("Failed to parse settings JSON: {}. Using defaults.", .{err}); return .{}; }; - defer parsed.deinit(); - - var settings = parsed.value; - - // Deep copy string fields so they survive parsed.deinit() - const texture_pack = dupStringField(allocator, settings.texture_pack) catch { - log.log.warn("Failed to allocate texture_pack string, using default", .{}); - settings.texture_pack = "default"; - settings.environment_map = "default"; - return settings; - }; - - const environment_map = dupStringField(allocator, settings.environment_map) catch { - log.log.warn("Failed to allocate environment_map string, using default", .{}); - freeStringField(allocator, texture_pack); // Clean up successful first allocation - settings.texture_pack = "default"; - settings.environment_map = "default"; - return settings; - }; - settings.texture_pack = texture_pack; - settings.environment_map = environment_map; + log.log.info("Settings loaded from ~/{s}", .{config_path}); + return settings; +} - if (data.sanitizeRuntimeConflicts(&settings)) { - log.log.warn("Disabling TAA for loaded settings because it currently jitters distant LOD terrain", .{}); - } +fn parseSettingsJson(allocator: std.mem.Allocator, content: []const u8) !Settings { + const parsed = try std.json.parseFromSlice(Settings, allocator, content, .{ + .ignore_unknown_fields = true, + }); + defer parsed.deinit(); - log.log.info("Settings loaded from ~/{s}", .{config_path}); + var settings = parsed.value; + settings.texture_pack = try dupStringField(allocator, settings.texture_pack); + errdefer freeStringField(allocator, settings.texture_pack); + settings.environment_map = try dupStringField(allocator, settings.environment_map); return settings; } +fn stringifySettings(allocator: std.mem.Allocator, settings: *const Settings) ![]u8 { + return std.json.Stringify.valueAlloc(allocator, settings.*, .{ .whitespace = .indent_2 }); +} + pub fn deinit(settings: *Settings, allocator: std.mem.Allocator) void { freeStringField(allocator, settings.texture_pack); freeStringField(allocator, settings.environment_map); @@ -129,10 +118,33 @@ pub fn save(settings: *const Settings, allocator: std.mem.Allocator) !void { defer file.close(); // Serialize settings to JSON and write to file - const json_str = try std.json.Stringify.valueAlloc(allocator, settings.*, .{ .whitespace = .indent_2 }); + const json_str = try stringifySettings(allocator, settings); defer allocator.free(json_str); try file.writeAll(json_str); log.log.info("Settings saved to ~/{s}", .{config_path}); } + +test "settings JSON ignores removed LOD fields and omits them when saved" { + const allocator = std.testing.allocator; + const legacy_json = + \\{ + \\ "render_distance": 23, + \\ "vsync": false, + \\ "horizon_distance": 512, + \\ "lod_enabled": true + \\} + ; + + var settings = try parseSettingsJson(allocator, legacy_json); + defer deinit(&settings, allocator); + try std.testing.expectEqual(@as(i32, 23), settings.render_distance); + try std.testing.expect(!settings.vsync); + + const saved_json = try stringifySettings(allocator, &settings); + defer allocator.free(saved_json); + try std.testing.expect(std.mem.indexOf(u8, saved_json, "render_distance") != null); + try std.testing.expect(std.mem.indexOf(u8, saved_json, "horizon_distance") == null); + try std.testing.expect(std.mem.indexOf(u8, saved_json, "lod_enabled") == null); +} diff --git a/modules/game-core/src/settings/tests.zig b/modules/game-core/src/settings/tests.zig index 7b640320..e075a596 100644 --- a/modules/game-core/src/settings/tests.zig +++ b/modules/game-core/src/settings/tests.zig @@ -2,69 +2,31 @@ const std = @import("std"); const data = @import("data.zig"); const Settings = data.Settings; const presets = @import("json_presets.zig"); -const persistence = @import("persistence.zig"); -const RenderDistancePreset = @import("engine-rhi").RenderDistancePreset; -test "distance terrain is enabled by default" { - try std.testing.expect((Settings{}).lod_enabled); +test "settings default to a full-detail chunk distance" { + const settings = Settings{}; + try std.testing.expectEqual(@as(i32, 15), settings.render_distance); } -test "Persistence Roundtrip" { - const allocator = std.testing.allocator; - _ = allocator; - var settings = Settings{}; - settings.shadow_quality = 3; - settings.render_distance = 25; - settings.lod_enabled = true; -} - -test "Preset Application" { +test "graphics presets apply their configured chunk distance" { const allocator = std.testing.allocator; try presets.initPresets(allocator); defer presets.deinitPresets(allocator); var settings = Settings{}; presets.apply(&settings, 0); - try std.testing.expectEqual(@as(u32, 0), settings.shadow_quality); try std.testing.expectEqual(@as(i32, 6), settings.render_distance); - try std.testing.expectEqual(RenderDistancePreset.low, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); - try std.testing.expectEqual(false, settings.lpv_enabled); - - presets.apply(&settings, 3); - try std.testing.expectEqual(@as(u32, 3), settings.shadow_quality); - try std.testing.expectEqual(@as(i32, 14), settings.render_distance); - try std.testing.expectEqual(RenderDistancePreset.ultra, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); - - presets.apply(&settings, 4); - try std.testing.expectEqual(@as(u32, 3), settings.shadow_quality); - try std.testing.expectEqual(@as(i32, 16), settings.render_distance); - try std.testing.expectEqual(RenderDistancePreset.extreme, settings.render_distance_preset); - try std.testing.expectEqual(true, settings.lod_enabled); + try std.testing.expectEqual(@as(u32, 0), settings.shadow_quality); } -test "Preset Matching" { +test "preset matching identifies settings changed from a preset" { const allocator = std.testing.allocator; try presets.initPresets(allocator); defer presets.deinitPresets(allocator); var settings = Settings{}; - presets.apply(&settings, 1); // Medium + presets.apply(&settings, 1); try std.testing.expectEqual(@as(usize, 1), presets.getIndex(&settings)); - - // Modify a value to make it Custom settings.shadow_quality = 3; try std.testing.expectEqual(presets.count(), presets.getIndex(&settings)); } - -test "TAA and LOD conflict sanitization" { - var settings = Settings{}; - settings.lod_enabled = true; - settings.taa_enabled = true; - settings.fxaa_enabled = false; - - try std.testing.expectEqual(true, data.sanitizeRuntimeConflicts(&settings)); - try std.testing.expectEqual(false, settings.taa_enabled); - try std.testing.expectEqual(true, settings.fxaa_enabled); -} diff --git a/modules/game-core/src/ui/session_hud.zig b/modules/game-core/src/ui/session_hud.zig index 81c7e2cd..34915c12 100644 --- a/modules/game-core/src/ui/session_hud.zig +++ b/modules/game-core/src/ui/session_hud.zig @@ -16,7 +16,6 @@ const TelemetryRow = enum(u8) { position, chunks, visible, - lods, queued_gen, queued_mesh, pending_upload, @@ -58,7 +57,7 @@ pub fn draw(session: anytype, ui: *UISystem, atlas: *const TextureAtlas, active_ const rs = telemetry.getRenderStats(); const pc = worldToChunkFromFloat(session.camera.position.x, session.camera.position.z); const fault_count = session.rhi.query().getFaultCount(); - const hud_h: f32 = if (fault_count > 0) 230 else 210; + const hud_h: f32 = if (fault_count > 0) 210 else 190; ui.drawRect(.{ .x = 10, .y = TELEMETRY_PANEL_Y, .width = 220, .height = hud_h }, Color.rgba(0, 0, 0, 0.6)); Font.drawText(ui, "POS:", 15, telemetryRowY(.position), 1.5, Color.white); Font.drawNumber(ui, pc.chunk_x, 120, telemetryRowY(.position), Color.white); @@ -68,11 +67,6 @@ pub fn draw(session: anytype, ui: *UISystem, atlas: *const TextureAtlas, active_ Font.drawText(ui, "VISIBLE:", 15, telemetryRowY(.visible), 1.5, Color.white); Font.drawNumber(ui, @intCast(rs.chunks_rendered), 140, telemetryRowY(.visible), Color.white); - if (telemetry.getLODStats()) |ls| { - Font.drawText(ui, "LODS:", 15, telemetryRowY(.lods), 1.5, Color.rgba(0.5, 0.8, 1.0, 1.0)); - Font.drawNumber(ui, @intCast(ls.totalLoaded()), 140, telemetryRowY(.lods), Color.rgba(0.5, 0.8, 1.0, 1.0)); - } - Font.drawText(ui, "QUEUED GEN:", 15, telemetryRowY(.queued_gen), 1.5, Color.white); Font.drawNumber(ui, @intCast(stats.gen_queue), 140, telemetryRowY(.queued_gen), Color.white); Font.drawText(ui, "QUEUED MESH:", 15, telemetryRowY(.queued_mesh), 1.5, Color.white); diff --git a/modules/game-ui/src/screens/graphics.zig b/modules/game-ui/src/screens/graphics.zig index b75e2eb7..61c8568e 100644 --- a/modules/game-ui/src/screens/graphics.zig +++ b/modules/game-ui/src/screens/graphics.zig @@ -3,15 +3,12 @@ const UISystem = @import("engine-ui").UISystem; const Font = @import("engine-ui").font; const Theme = @import("../menu_theme.zig"); const SettingsUi = @import("../settings_ui.zig"); -const Color = Theme.Color; const Rect = Theme.Rect; const Screen = @import("../screen.zig"); const IScreen = Screen.IScreen; const EngineContext = Screen.EngineContext; const settings_pkg = @import("game-core").settings; const Settings = settings_pkg.Settings; -const render_settings_mod = @import("engine-rhi").render_settings; -const RenderDistancePreset = render_settings_mod.RenderDistancePreset; const PANEL_WIDTH_MAX = 1280.0; const StepResult = SettingsUi.StepResult; @@ -81,7 +78,7 @@ pub const GraphicsScreen = struct { const panel_h: f32 = screen_h - margin * 2.0; const panel_x: f32 = (screen_w - panel_w) * 0.5; const panel_y: f32 = margin; - const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "RENDER", "VISUALS", "Lighting, materials, fog, bloom, and distant terrain."); + const shell = Theme.drawShell(ui, .{ .x = panel_x, .y = panel_y, .width = panel_w, .height = panel_h }, ui_scale, "RENDER", "VISUALS", "Lighting, materials, fog, and bloom."); const content_top = shell.content.y; const content_bottom = shell.content.y + shell.content.height; @@ -93,17 +90,14 @@ pub const GraphicsScreen = struct { const scroll_dy = ctx.input.getScrollDelta().y; self.scroll_offset -= scroll_dy * 36.0 * ui_scale; - var total_rows: usize = 2; + var total_rows: usize = 1; inline for (comptime std.meta.declarations(Settings.metadata)) |decl| { if (comptime std.mem.eql(u8, decl.name, "msaa_samples")) continue; - if (comptime std.mem.eql(u8, decl.name, "render_distance_preset")) continue; if (comptime std.mem.eql(u8, decl.name, "render_distance")) continue; - if (comptime std.mem.eql(u8, decl.name, "horizon_distance")) continue; total_rows += 1; } const section_extra = @as(f32, @floatFromInt(countSettingSections())) * 34.0 * ui_scale; - const warning_extra: f32 = if (render_settings_mod.getPresetConfig(settings.render_distance_preset).show_warning) 38.0 * ui_scale else 0.0; - const total_content_h: f32 = @as(f32, @floatFromInt(total_rows)) * (row_height + 8.0 * ui_scale) + 40.0 * ui_scale + section_extra + warning_extra; + const total_content_h: f32 = @as(f32, @floatFromInt(total_rows)) * (row_height + 8.0 * ui_scale) + 40.0 * ui_scale + section_extra; const max_scroll = @max(0.0, total_content_h - content_h); self.scroll_offset = @max(0.0, @min(self.scroll_offset, max_scroll)); @@ -139,40 +133,11 @@ pub const GraphicsScreen = struct { } sy += row_height + 8.0 * ui_scale; - if (rowFullyVisible(sy, row_height, content_top, content_bottom)) { - const current_rdp: u32 = @intFromEnum(settings.render_distance_preset); - const rdp_label = settings.render_distance_preset.label(); - const rdp_count = @as(u32, RenderDistancePreset.count); - const step = drawStepperRow(ui, .{ .x = row_x, .y = sy, .width = row_w, .height = row_height }, "RENDER DISTANCE", "LOD radius profile and streamer pressure.", rdp_label, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, ui_scale); - if (step == .previous or step == .next) { - const next_value = if (step == .previous) if (current_rdp == 0) rdp_count - 1 else current_rdp - 1 else (current_rdp + 1) % rdp_count; - settings.render_distance_preset = @enumFromInt(next_value); - const preset_cfg = render_settings_mod.getPresetConfig(settings.render_distance_preset); - settings.render_distance = preset_cfg.lod_radii[0]; - settings.horizon_distance = preset_cfg.horizon_radius; - } - } - sy += row_height + 8.0 * ui_scale; - - { - const preset_cfg = render_settings_mod.getPresetConfig(settings.render_distance_preset); - if (preset_cfg.show_warning) { - if (rowFullyVisible(sy, 30.0 * ui_scale, content_top, content_bottom)) { - ui.drawRect(.{ .x = row_x, .y = sy, .width = row_w, .height = 30.0 * ui_scale }, Color.rgba(0.28, 0.040, 0.030, 0.92)); - ui.drawRect(.{ .x = row_x, .y = sy, .width = 5.0 * ui_scale, .height = 30.0 * ui_scale }, Theme.danger); - Font.drawText(ui, "EXTREME DISTANCE CAN DESTABILIZE GPUS BELOW 8GB VRAM", row_x + 16.0 * ui_scale, sy + 8.0 * ui_scale, 0.82 * ui_scale, Theme.title); - } - sy += 38.0 * ui_scale; - } - } - var buf: [64]u8 = undefined; inline for (comptime std.meta.declarations(Settings.metadata)) |decl| { if (comptime std.mem.eql(u8, decl.name, "msaa_samples")) continue; - if (comptime std.mem.eql(u8, decl.name, "render_distance_preset")) continue; if (comptime std.mem.eql(u8, decl.name, "render_distance")) continue; - if (comptime std.mem.eql(u8, decl.name, "horizon_distance")) continue; sy = drawSectionBoundary(ui, decl.name, row_x, sy, content_top, content_bottom, ui_scale); @@ -263,9 +228,7 @@ pub const GraphicsScreen = struct { } if (val_ptr.* != old_val) { - const sanitized_conflict = settings_pkg.sanitizeRuntimeConflicts(settings); SettingsUi.applyChangedSetting(decl.name, settings, rs); - if (sanitized_conflict) rs.setFXAA(settings.fxaa_enabled and !settings.taa_enabled); } sy += row_height + 8.0 * ui_scale; @@ -318,9 +281,7 @@ fn countSettingSections() usize { comptime var count: usize = 0; inline for (comptime std.meta.declarations(Settings.metadata)) |decl| { if (comptime std.mem.eql(u8, decl.name, "msaa_samples")) continue; - if (comptime std.mem.eql(u8, decl.name, "render_distance_preset")) continue; if (comptime std.mem.eql(u8, decl.name, "render_distance")) continue; - if (comptime std.mem.eql(u8, decl.name, "horizon_distance")) continue; if (comptime sectionBoundaryLabel(decl.name).len > 0) count += 1; } return count; diff --git a/modules/game-ui/src/screens/rml_settings.zig b/modules/game-ui/src/screens/rml_settings.zig index 5a3281ec..67f3bcf9 100644 --- a/modules/game-ui/src/screens/rml_settings.zig +++ b/modules/game-ui/src/screens/rml_settings.zig @@ -15,9 +15,6 @@ const SettingsUi = @import("../settings_ui.zig"); const settings_pkg = @import("game-core").settings; const apply_logic = settings_pkg.apply_logic; const Settings = settings_pkg.Settings; -const render_settings_mod = @import("engine-rhi").render_settings; -const RenderDistancePreset = render_settings_mod.RenderDistancePreset; -const LODConfig = @import("world-lod").lod_chunk.LODConfig; const SettingsTab = enum { display, camera, world, rendering }; const SettingAction = enum { previous, next, toggle }; @@ -165,18 +162,6 @@ pub const RmlSettingsScreen = struct { settings.render_distance -= 1; } else if (std.mem.eql(u8, id, "render-distance-next") and settings.render_distance < std.math.maxInt(i32)) { settings.render_distance += 1; - settings.horizon_distance = LODConfig.normalizeUserHorizonDistance(settings.render_distance, settings.horizon_distance); - } else if (std.mem.eql(u8, id, "horizon-distance-prev") or std.mem.eql(u8, id, "horizon-distance-next")) { - settings.horizon_distance = LODConfig.stepHorizonDistance( - settings.render_distance, - settings.horizon_distance, - std.mem.eql(u8, id, "horizon-distance-next"), - ); - } else if (std.mem.eql(u8, id, "lod-toggle")) { - settings.lod_enabled = !settings.lod_enabled; - if (settings_pkg.sanitizeRuntimeConflicts(settings)) { - apply_logic.applyToRenderSettings(settings, self.context.render_settings); - } } } @@ -185,10 +170,6 @@ pub const RmlSettingsScreen = struct { self.stepOverallPreset(.previous); } else if (std.mem.eql(u8, id, "preset-next")) { self.stepOverallPreset(.next); - } else if (std.mem.eql(u8, id, "render-distance-preset-prev")) { - self.stepRenderDistancePreset(.previous); - } else if (std.mem.eql(u8, id, "render-distance-preset-next")) { - self.stepRenderDistancePreset(.next); } else if (std.mem.eql(u8, id, "wireframe-toggle")) { self.context.settings.wireframe_enabled = !self.context.settings.wireframe_enabled; apply_logic.applyToRenderSettings(self.context.settings, self.context.render_settings); @@ -216,21 +197,6 @@ pub const RmlSettingsScreen = struct { } } - fn stepRenderDistancePreset(self: *@This(), action: SettingAction) void { - const settings = self.context.settings; - const current: u32 = @intFromEnum(settings.render_distance_preset); - const count: u32 = @as(u32, RenderDistancePreset.count); - const next = switch (action) { - .previous => if (current == 0) count - 1 else current - 1, - .next => (current + 1) % count, - .toggle => return, - }; - settings.render_distance_preset = @enumFromInt(next); - const config = render_settings_mod.getPresetConfig(settings.render_distance_preset); - settings.render_distance = config.lod_radii[0]; - settings.horizon_distance = config.horizon_radius; - } - fn applyMetadataAction(self: *@This(), comptime name: []const u8, action: SettingAction) void { const settings = self.context.settings; const meta = @field(Settings.metadata, name); @@ -271,12 +237,10 @@ pub const RmlSettingsScreen = struct { } if (value.* != old_value) { - const sanitized_conflict = settings_pkg.sanitizeRuntimeConflicts(settings); SettingsUi.applyChangedSetting(name, settings, self.context.render_settings); if (comptime std.mem.eql(u8, name, "msaa_samples")) { self.context.render_settings.setMSAA(settings.msaa_samples); } - if (sanitized_conflict) self.context.render_settings.setFXAA(settings.fxaa_enabled and !settings.taa_enabled); } } @@ -328,14 +292,9 @@ pub const RmlSettingsScreen = struct { fn appendWorldRows(self: *@This(), out: *std.ArrayList(u8)) !void { var buffer: [32]u8 = undefined; const settings = self.context.settings; - settings.horizon_distance = LODConfig.normalizeUserHorizonDistance(settings.render_distance, settings.horizon_distance); try appendSection(out, self.context.allocator, "DISTANCE"); const render_distance = try std.fmt.bufPrint(&buffer, "{} CHUNKS", .{settings.render_distance}); try appendStepperRow(out, self.context.allocator, "RENDER DISTANCE", "Full-detail chunk radius.", render_distance, "render-distance"); - const horizon_distance = try std.fmt.bufPrint(&buffer, "{} CHUNKS", .{settings.horizon_distance}); - try appendStepperRow(out, self.context.allocator, "DISTANT LOD LIMIT", "Maximum distant-terrain radius from the player.", horizon_distance, "horizon-distance"); - try appendSection(out, self.context.allocator, "STREAMING"); - try appendToggleRow(out, self.context.allocator, "LOD SYSTEM", "Distance terrain streaming.", settings.lod_enabled, "lod"); } fn appendRenderingRows(self: *@This(), out: *std.ArrayList(u8)) !void { @@ -343,7 +302,6 @@ pub const RmlSettingsScreen = struct { try appendSection(out, self.context.allocator, "BASELINE"); const preset_index = if (settings_pkg.json_presets.count() > 0) settings_pkg.json_presets.getIndex(settings) else 0; try appendStepperRow(out, self.context.allocator, "OVERALL QUALITY", "Preset target for renderer cost and quality.", SettingsUi.getPresetLabel(preset_index), "preset"); - try appendStepperRow(out, self.context.allocator, "RENDER DISTANCE", "LOD radius profile and streamer pressure.", settings.render_distance_preset.label(), "render-distance-preset"); try appendToggleRow(out, self.context.allocator, "WIREFRAME", "Debug mesh visibility.", settings.wireframe_enabled, "wireframe"); try appendMetadataSection(out, self.context.allocator, settings, "MATERIALS", MATERIAL_SETTING_NAMES); diff --git a/modules/game-ui/src/screens/settings.zig b/modules/game-ui/src/screens/settings.zig index 43dac3b0..98320ef3 100644 --- a/modules/game-ui/src/screens/settings.zig +++ b/modules/game-ui/src/screens/settings.zig @@ -3,7 +3,6 @@ const UISystem = @import("engine-ui").UISystem; const Font = @import("engine-ui").font; const Theme = @import("../menu_theme.zig"); const SettingsUi = @import("../settings_ui.zig"); -const Color = Theme.Color; const Rect = Theme.Rect; const Screen = @import("../screen.zig"); const IScreen = Screen.IScreen; @@ -11,9 +10,6 @@ const EngineContext = Screen.EngineContext; const settings_pkg = @import("game-core").settings; const apply_logic = settings_pkg.apply_logic; const Settings = settings_pkg.Settings; -const render_settings_mod = @import("engine-rhi").render_settings; -const RenderDistancePreset = render_settings_mod.RenderDistancePreset; -const LODConfig = @import("world-lod").lod_chunk.LODConfig; const PANEL_WIDTH_MAX = 1360.0; const PANEL_HEIGHT_MAX = 820.0; @@ -289,8 +285,8 @@ fn drawCameraTab(ui: *UISystem, settings: anytype, layout: ColumnLayout, row_h: } fn drawWorldTab(ui: *UISystem, settings: anytype, rs: anytype, layout: ColumnLayout, row_h: f32, label_scale: f32, value_scale: f32, button_scale: f32, mouse_x: f32, mouse_y: f32, mouse_clicked: bool, scale: f32) void { + _ = rs; var num_buf: [32]u8 = undefined; - settings.horizon_distance = LODConfig.normalizeUserHorizonDistance(settings.render_distance, settings.horizon_distance); var y_left = layout.top_y; Theme.drawSectionLabel(ui, layout.left_x, y_left, "DISTANCE", scale); @@ -301,30 +297,13 @@ fn drawWorldTab(ui: *UISystem, settings: anytype, rs: anytype, layout: ColumnLay if (step == .previous and settings.render_distance > 2) settings.render_distance -= 1; if (step == .next and settings.render_distance < std.math.maxInt(i32)) { settings.render_distance += 1; - settings.horizon_distance = LODConfig.normalizeUserHorizonDistance(settings.render_distance, settings.horizon_distance); } } - y_left += row_h + 8.0 * scale; - - const horizon_distance_label = std.fmt.bufPrint(&num_buf, "{} CHUNKS", .{settings.horizon_distance}) catch "?"; - if (drawStepperRow(ui, .{ .x = layout.left_x, .y = y_left, .width = layout.col_w, .height = row_h }, "DISTANT LOD LIMIT", "Maximum distant-terrain radius from the player.", horizon_distance_label, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, scale)) |step| { - settings.horizon_distance = LODConfig.stepHorizonDistance(settings.render_distance, settings.horizon_distance, step == .next); - } - - var y_right = if (layout.two_column) layout.top_y else y_left + row_h + 22.0 * scale; - Theme.drawSectionLabel(ui, layout.right_x, y_right, "STREAMING", scale); - y_right += 28.0 * scale; - - if (drawToggleRow(ui, .{ .x = layout.right_x, .y = y_right, .width = layout.col_w, .height = row_h }, "LOD SYSTEM", "Distance terrain streaming.", settings.lod_enabled, label_scale, value_scale, mouse_x, mouse_y, mouse_clicked, scale)) { - settings.lod_enabled = !settings.lod_enabled; - if (settings_pkg.sanitizeRuntimeConflicts(settings)) apply_logic.applyToRenderSettings(settings, rs); - } } fn drawRenderingTab(ui: *UISystem, self: *SettingsScreen, ctx: EngineContext, settings: *Settings, rs: anytype, inner: Rect, layout: ColumnLayout, row_h: f32, label_scale: f32, value_scale: f32, button_scale: f32, mouse_x: f32, mouse_y: f32, mouse_clicked: bool, scale: f32) void { const row_gap = 8.0 * scale; const section_h = 28.0 * scale; - const warning_h: f32 = if (render_settings_mod.getPresetConfig(settings.render_distance_preset).show_warning) 34.0 * scale else 0.0; const material_settings = .{ "textures_enabled", "pbr_enabled", "pbr_quality", "anisotropic_filtering", "max_texture_resolution", "water_quality" }; const shadow_settings = .{ "shadow_quality", "shadow_pcf_samples", "shadow_cascade_blend", "shadow_distance", "shadow_caster_distance" }; const image_settings = .{ "taa_enabled", "taa_blend_factor", "taa_velocity_rejection", "fxaa_enabled", "ssao_enabled", "bloom_enabled", "bloom_intensity", "vignette_enabled", "vignette_intensity", "film_grain_enabled", "film_grain_intensity" }; @@ -332,9 +311,9 @@ fn drawRenderingTab(ui: *UISystem, self: *SettingsScreen, ctx: EngineContext, se const dynamic_resolution_settings = .{ "dynamic_resolution_enabled", "dynamic_resolution_min_scale", "dynamic_resolution_max_scale", "target_fps" }; const left_sections = .{ "BASELINE", "MATERIALS", "SHADOWS" }; const right_sections = .{ "IMAGE", "ATMOSPHERE AND GI", "DYNAMIC RESOLUTION" }; - const baseline_rows = .{ "OVERALL QUALITY", "RENDER DISTANCE", "WIREFRAME" }; + const baseline_rows = .{ "OVERALL QUALITY", "WIREFRAME" }; - const left_content_h = renderingContentHeight(left_sections.len, baseline_rows.len + material_settings.len + shadow_settings.len, row_h, row_gap, section_h, scale) + warning_h; + const left_content_h = renderingContentHeight(left_sections.len, baseline_rows.len + material_settings.len + shadow_settings.len, row_h, row_gap, section_h, scale); const right_content_h = renderingContentHeight(right_sections.len, image_settings.len + atmosphere_settings.len + dynamic_resolution_settings.len, row_h, row_gap, section_h, scale); const column_gap: f32 = 28.0 * scale; const total_content_h = if (layout.two_column) @@ -354,7 +333,6 @@ fn drawRenderingTab(ui: *UISystem, self: *SettingsScreen, ctx: EngineContext, se drawRenderSection(ui, layout.left_x, y_left, left_sections[0], top, bottom, scale); y_left += section_h; y_left = drawPresetRow(ui, settings, rs, .{ .x = layout.left_x, .y = y_left, .width = layout.col_w, .height = row_h }, top, bottom, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, scale) + row_gap; - y_left = drawRenderDistancePresetRow(ui, settings, .{ .x = layout.left_x, .y = y_left, .width = layout.col_w, .height = row_h }, top, bottom, label_scale, value_scale, button_scale, mouse_x, mouse_y, mouse_clicked, scale) + row_gap; if (rowVisible(y_left, row_h, top, bottom)) { if (drawToggleRow(ui, .{ .x = layout.left_x, .y = y_left, .width = layout.col_w, .height = row_h }, "WIREFRAME", "Debug mesh visibility.", settings.wireframe_enabled, label_scale, value_scale, mouse_x, mouse_y, mouse_clicked, scale)) { settings.wireframe_enabled = !settings.wireframe_enabled; @@ -362,15 +340,6 @@ fn drawRenderingTab(ui: *UISystem, self: *SettingsScreen, ctx: EngineContext, se } } y_left += row_h + row_gap; - if (render_settings_mod.getPresetConfig(settings.render_distance_preset).show_warning) { - if (rowVisible(y_left, warning_h, top, bottom)) { - ui.drawRect(.{ .x = layout.left_x, .y = y_left, .width = layout.col_w, .height = 30.0 * scale }, Color.rgba(0.28, 0.040, 0.030, 0.80)); - ui.drawRect(.{ .x = layout.left_x, .y = y_left, .width = 5.0 * scale, .height = 30.0 * scale }, Theme.danger); - Font.drawText(ui, "EXTREME DISTANCE CAN DESTABILIZE GPUS BELOW 8GB VRAM", layout.left_x + 16.0 * scale, y_left + 8.0 * scale, 0.90 * scale, Theme.title); - } - y_left += warning_h; - } - drawRenderSection(ui, layout.left_x, y_left, left_sections[1], top, bottom, scale); y_left += section_h; inline for (material_settings) |name| { @@ -437,21 +406,6 @@ fn drawPresetRow(ui: *UISystem, settings: *Settings, rs: anytype, row: Rect, top return row.y + row.height; } -fn drawRenderDistancePresetRow(ui: *UISystem, settings: *Settings, row: Rect, top: f32, bottom: f32, label_scale: f32, value_scale: f32, button_scale: f32, mx: f32, my: f32, clicked: bool, scale: f32) f32 { - if (!rowVisible(row.y, row.height, top, bottom)) return row.y + row.height; - const current_rdp: u32 = @intFromEnum(settings.render_distance_preset); - const rdp_label = settings.render_distance_preset.label(); - const rdp_count = @as(u32, RenderDistancePreset.count); - if (drawStepperRow(ui, row, "RENDER DISTANCE", "LOD radius profile and streamer pressure.", rdp_label, label_scale, value_scale, button_scale, mx, my, clicked, scale)) |step| { - const next_value = if (step == .previous) if (current_rdp == 0) rdp_count - 1 else current_rdp - 1 else (current_rdp + 1) % rdp_count; - settings.render_distance_preset = @enumFromInt(next_value); - const preset_cfg = render_settings_mod.getPresetConfig(settings.render_distance_preset); - settings.render_distance = preset_cfg.lod_radii[0]; - settings.horizon_distance = preset_cfg.horizon_radius; - } - return row.y + row.height; -} - fn drawSettingRow(ui: *UISystem, comptime name: []const u8, settings: *Settings, rs: anytype, row: Rect, top: f32, bottom: f32, label_scale: f32, value_scale: f32, button_scale: f32, mx: f32, my: f32, clicked: bool, scale: f32) f32 { if (!rowVisible(row.y, row.height, top, bottom)) return row.y + row.height; @@ -532,9 +486,7 @@ fn drawSettingRow(ui: *UISystem, comptime name: []const u8, settings: *Settings, } if (val_ptr.* != old_val) { - const sanitized_conflict = settings_pkg.sanitizeRuntimeConflicts(settings); SettingsUi.applyChangedSetting(name, settings, rs); - if (sanitized_conflict) rs.setFXAA(settings.fxaa_enabled and !settings.taa_enabled); } return row.y + row.height; diff --git a/modules/game-ui/src/screens/world.zig b/modules/game-ui/src/screens/world.zig index ea1185bb..4fd3f18f 100644 --- a/modules/game-ui/src/screens/world.zig +++ b/modules/game-ui/src/screens/world.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const LODConfig = @import("world-lod").lod_chunk.LODConfig; const UISystem = @import("engine-ui").UISystem; const Screen = @import("../screen.zig"); const IScreen = Screen.IScreen; @@ -27,7 +26,6 @@ const Font = @import("engine-ui").font; const Color = @import("engine-ui").Color; const Rect = @import("engine-ui").Rect; const WorldStats = @import("engine-ui").WorldStats; -const LODStatsDisplay = @import("engine-ui").LODStatsDisplay; const log = @import("engine-core").log; const CSM = @import("engine-graphics").csm; const settings_data = @import("game-core").settings.data; @@ -95,34 +93,16 @@ pub const WorldScreen = struct { }; pub fn init(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize) !*WorldScreen { - return initWithDistance(allocator, context, seed, generator_index, context.settings.render_distance, context.settings.horizon_distance, context.settings.lod_enabled, true, false); + return initWithDistance(allocator, context, seed, generator_index, context.settings.render_distance, false); } pub fn initMenuPreview(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize) !*WorldScreen { - // Keep the distant hierarchy in the rotating preview, but use its - // expanded fallback: continuously streaming compact tiles underneath a - // retained RmlUi overlay can trigger a RADV rejection on RDNA1. - return initWithDistance( - allocator, - context, - seed, - generator_index, - context.settings.render_distance, - context.settings.horizon_distance, - context.settings.lod_enabled, - false, - true, - ); + return initWithDistance(allocator, context, seed, generator_index, context.settings.render_distance, true); } - fn initWithDistance(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize, render_distance: i32, horizon_distance: i32, lod_enabled: bool, compact_tiles_enabled: bool, menu_preview: bool) !*WorldScreen { + fn initWithDistance(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize, render_distance: i32, menu_preview: bool) !*WorldScreen { const render_system = context.render_system; - const diagnostic_horizon = context.benchmark_runner != null or context.build_config.phase5_visual_scene.len > 0 or context.build_config.benchmark_fixture.len > 0; - const effective_horizon_distance = if (diagnostic_horizon) - LODConfig.normalizeHorizonDistance(render_distance, horizon_distance) - else - LODConfig.normalizeUserHorizonDistance(render_distance, horizon_distance); - const session = try GameSession.init(allocator, render_system.getRHI(), render_system.getAtlas(), seed, render_distance, effective_horizon_distance, lod_enabled, compact_tiles_enabled, generator_index, context.settings.render_distance_preset, context.build_config); + const session = try GameSession.init(allocator, render_system.getRHI(), render_system.getAtlas(), seed, render_distance, generator_index, context.build_config); errdefer session.deinit(); const world = session.world.interface(); @@ -183,25 +163,11 @@ pub const WorldScreen = struct { const cam = &self.session.player.camera; if (!self.menu_preview) { // Keep persisted/manual values aligned with the supported UI range - // so the displayed Distant LOD Limit matches the runtime radius. - const diagnostic_horizon = benchmark_mode or ctx.build_config.phase5_visual_scene.len > 0 or ctx.build_config.benchmark_fixture.len > 0; - ctx.settings.horizon_distance = if (diagnostic_horizon) - LODConfig.normalizeHorizonDistance(ctx.settings.render_distance, ctx.settings.horizon_distance) - else - LODConfig.normalizeUserHorizonDistance(ctx.settings.render_distance, ctx.settings.horizon_distance); - // The World settings control is explicitly the full-detail chunk - // radius. Presets seed startup budgets, but a live manual value - // must be allowed to raise or lower that radius after the menu - // closes instead of remaining silently capped by the preset. - self.session.world.setLODChunkRenderRadiusLimit(ctx.settings.render_distance); - cam.far = @import("game-core").session.cameraFarPlaneForDistances(ctx.settings.render_distance, ctx.settings.horizon_distance); + cam.far = @import("game-core").session.cameraFarPlaneForRenderDistance(ctx.settings.render_distance); const world_telemetry = self.world.telemetry(); if (world_telemetry.getRenderDistance() != ctx.settings.render_distance) { world_telemetry.setRenderDistance(ctx.settings.render_distance); } - if (world_telemetry.getHorizonDistance() != ctx.settings.horizon_distance) { - world_telemetry.setHorizonDistance(ctx.settings.horizon_distance); - } } ctx.audio_system.setListener(cam.position, cam.forward, cam.up); @@ -273,15 +239,6 @@ pub const WorldScreen = struct { options.setShadowDebugChannel(resolveShadowDebugChannel(ctx.settings)); self.last_debug_toggle_time = now; } - if (can_toggle_debug and ctx.input_mapper.isActionPressed(ctx.input, .toggle_lod_render)) { - if (!self.world.isLODEnabled()) { - log.log.warn("LOD toggle requested but LOD system is not initialized", .{}); - } else { - const enabled = self.world.telemetry().toggleLODRendering(); - log.log.info("LOD rendering {s}", .{if (enabled) "enabled" else "disabled"}); - } - self.last_debug_toggle_time = now; - } if (can_toggle_debug and ctx.input_mapper.isActionPressed(ctx.input, .toggle_gpass_render)) { const new_val = !render_system.getDisableGPassDraw(); render_system.setDisableGPassDraw(new_val); @@ -328,14 +285,12 @@ pub const WorldScreen = struct { const stats = world_telemetry.getStats(); const render_stats = world_telemetry.getRenderStats(); const state_counts = world_telemetry.getChunkStateCounts(); - const lod_stats = world_telemetry.getLODStats(); - const lod_radii = self.session.lod_config.radii; const elapsed = now - self.startup_diagnostic_start; const frames_elapsed = self.context.time.frame_count - self.startup_diagnostic_start_frame; const avg_fps = if (elapsed > 0.001) @as(f32, @floatFromInt(frames_elapsed)) / elapsed else self.context.time.fps; log.log.warn( - "STARTUP_DIAG: generator='{s}' elapsed={d:.2}s fps={d:.1} avg_fps={d:.1} frames={} rd={} lod0={} chunks_loaded={} chunks_total={} chunks_rendered={} chunks_culled={} gen_queue={} mesh_queue={} upload_queue={} lod_loaded={}", + "STARTUP_DIAG: generator='{s}' elapsed={d:.2}s fps={d:.1} avg_fps={d:.1} frames={} rd={} chunks_loaded={} chunks_total={} chunks_rendered={} chunks_culled={} gen_queue={} mesh_queue={} upload_queue={}", .{ world_telemetry.getGeneratorName(), elapsed, @@ -343,7 +298,6 @@ pub const WorldScreen = struct { avg_fps, frames_elapsed, world_telemetry.getRenderDistance(), - lod_radii[0], stats.chunks_loaded, render_stats.chunks_total, render_stats.chunks_rendered, @@ -351,7 +305,6 @@ pub const WorldScreen = struct { stats.gen_queue, stats.mesh_queue, stats.upload_queue, - if (lod_stats) |ls| ls.totalLoaded() else @as(u32, 0), }, ); log.log.warn( @@ -367,28 +320,6 @@ pub const WorldScreen = struct { render_stats.vertices_rendered, }, ); - if (lod_stats) |ls| { - log.log.warn( - "STARTUP_DIAG_LOD_STATES: loaded=[{},{},{},{},{}] generating=[{},{},{},{},{}] generated=[{},{},{},{},{}] meshing=[{},{},{},{},{}]", - .{ - ls.loaded[0], ls.loaded[1], ls.loaded[2], ls.loaded[3], ls.loaded[4], - ls.generating[0], ls.generating[1], ls.generating[2], ls.generating[3], ls.generating[4], - ls.generated[0], ls.generated[1], ls.generated[2], ls.generated[3], ls.generated[4], - ls.meshing[0], ls.meshing[1], ls.meshing[2], ls.meshing[3], ls.meshing[4], - }, - ); - log.log.warn( - "STARTUP_DIAG_LOD_QUEUES: mesh_ready=[{},{},{},{},{}] uploading=[{},{},{},{},{}] meshes=[{},{},{},{},{}] genQ={} uploadQ=[{},{},{},{},{}] memory_mb={} cache_hit/miss={}/{} store_hit/miss={}/{}", - .{ - ls.mesh_ready[0], ls.mesh_ready[1], ls.mesh_ready[2], ls.mesh_ready[3], ls.mesh_ready[4], - ls.uploading[0], ls.uploading[1], ls.uploading[2], ls.uploading[3], ls.uploading[4], - ls.mesh_count[0], ls.mesh_count[1], ls.mesh_count[2], ls.mesh_count[3], ls.mesh_count[4], - ls.gen_queue_depth[ls.gen_queue_depth.len - 1], ls.upload_queue_depth[0], ls.upload_queue_depth[1], ls.upload_queue_depth[2], ls.upload_queue_depth[3], - ls.upload_queue_depth[4], ls.memory_used_mb, ls.cache_hits, ls.cache_misses, ls.store_hits, - ls.store_misses, - }, - ); - } self.startup_diagnostic_logged = true; self.context.input.setShouldQuit(true); } @@ -674,7 +605,6 @@ pub const WorldScreen = struct { if (self.debug_ui.menuEnabled()) { const debug_state = world_debug.ScreenDebugState{ .session = self.session, - .world_telemetry = world_telemetry, .last_debug_toggle_time = &self.last_debug_toggle_time, .chunk_inspector_overlay = &self.chunk_inspector_overlay, }; @@ -687,20 +617,6 @@ pub const WorldScreen = struct { } fn drawBackground(ptr: *anyopaque, ui: *UISystem) !void { - const self: *@This() = @ptrCast(@alignCast(ptr)); - const telemetry = self.world.telemetry(); - const restore_lod = telemetry.isLODRenderingEnabled(); - - // Compact vertex-pulling draws beneath a retained menu overlay can make - // RADV on RDNA1 reject the combined command stream. The nearby - // full-detail world remains a useful pause backdrop, so omit distant LOD - // only while this screen is rendered as another screen's background and - // restore the user's setting before leaving the draw call. - if (restore_lod) _ = telemetry.toggleLODRendering(); - defer { - if (restore_lod) _ = telemetry.toggleLODRendering(); - } - try draw(ptr, ui); } @@ -722,105 +638,6 @@ pub const WorldScreen = struct { const ws = self.world.telemetry(); const rs = ws.getRenderStats(); const stats = ws.getStats(); - var lod_display: ?LODStatsDisplay = null; - var lod_renderable_regions: u64 = 0; - if (ws.getLODStats()) |ls| { - for (ls.loaded) |count| lod_renderable_regions += count; - lod_display = .{ - .loaded = ls.loaded, - .memory_used_mb = ls.memory_used_mb, - .memory_used_bytes = ls.memory_used_bytes, - .pool_gpu_capacity_bytes = ls.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = ls.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = ls.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = ls.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = ls.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = ls.compact_pool_allocated_bytes, - .compact_pool_free_bytes = ls.compact_pool_free_bytes, - .compact_pool_retired_bytes = ls.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = ls.direct_mesh_gpu_bytes, - .source_data_cpu_bytes = ls.source_data_cpu_bytes, - .pending_cpu_upload_bytes = ls.pending_cpu_upload_bytes, - .deferred_deletion_gpu_bytes = ls.deferred_deletion_gpu_bytes, - .deferred_deletion_cpu_bytes = ls.deferred_deletion_cpu_bytes, - .profiling = .{ - .enabled = ls.profiling.enabled, - .update_ms = ls.profiling.update_ms, - .scheduling_ms = ls.profiling.scheduling_ms, - .cache_ms = ls.profiling.cache_ms, - .generation_dispatch_ms = ls.profiling.generation_dispatch_ms, - .state_transition_ms = ls.profiling.state_transition_ms, - .upload_prep_ms = ls.profiling.upload_prep_ms, - .upload_submission_ms = ls.profiling.upload_submission_ms, - .visibility_ms = ls.profiling.visibility_ms, - .coverage_ms = ls.profiling.coverage_ms, - .eviction_ms = ls.profiling.eviction_ms, - .worker_generation_ms = ls.profiling.worker_generation_ms, - .worker_mesh_construction_ms = ls.profiling.worker_mesh_construction_ms, - .worker_far_expanded_mesh_construction_ms = ls.profiling.worker_far_expanded_mesh_construction_ms, - .worker_compact_encode_ms = ls.profiling.worker_compact_encode_ms, - .manager_lock_wait_ms = ls.profiling.manager_lock_wait_ms, - .manager_lock_hold_ms = ls.profiling.manager_lock_hold_ms, - .upload_bytes = ls.profiling.upload_bytes, - .far_expanded_upload_bytes = ls.profiling.far_expanded_upload_bytes, - .compact_upload_bytes = ls.profiling.compact_upload_bytes, - .pending_cpu_upload_bytes = ls.profiling.pending_cpu_upload_bytes, - .staging_pressure_count = ls.profiling.staging_pressure_count, - .visible_count = ls.profiling.visible_count, - .rejected_count = ls.profiling.rejected_count, - .coverage_count = ls.profiling.coverage_count, - .visibility_levels = blk: { - var levels: [@import("engine-ui").LOD_VISIBILITY_LEVEL_COUNT]@import("engine-ui").LODVisibilityLevelDisplay = undefined; - for (&levels, 0..) |*level, index| level.* = .{ - .candidates = ls.profiling.visibility_levels[index].candidates, - .accepted = ls.profiling.visibility_levels[index].accepted, - .rejected_no_draw = ls.profiling.visibility_levels[index].rejected_no_draw, - .rejected_not_ready = ls.profiling.visibility_levels[index].rejected_not_ready, - .rejected_missing_region = ls.profiling.visibility_levels[index].rejected_missing_region, - .rejected_not_renderable = ls.profiling.visibility_levels[index].rejected_not_renderable, - .rejected_finer_coverage = ls.profiling.visibility_levels[index].rejected_finer_coverage, - .rejected_range = ls.profiling.visibility_levels[index].rejected_range, - .rejected_frustum = ls.profiling.visibility_levels[index].rejected_frustum, - .rejected_chunk_coverage = ls.profiling.visibility_levels[index].rejected_chunk_coverage, - .coverage_checks = ls.profiling.visibility_levels[index].coverage_checks, - }; - break :blk levels; - }, - .deferred_deletion_bytes = ls.profiling.deferred_deletion_bytes, - .deferred_deletion_cpu_bytes = ls.profiling.deferred_deletion_cpu_bytes, - .pool_gpu_capacity_bytes = ls.profiling.pool_gpu_capacity_bytes, - .pool_gpu_allocated_bytes = ls.profiling.pool_gpu_allocated_bytes, - .pool_gpu_slack_bytes = ls.profiling.pool_gpu_slack_bytes, - .pool_cpu_shadow_bytes = ls.profiling.pool_cpu_shadow_bytes, - .compact_pool_capacity_bytes = ls.profiling.compact_pool_capacity_bytes, - .compact_pool_allocated_bytes = ls.profiling.compact_pool_allocated_bytes, - .compact_pool_free_bytes = ls.profiling.compact_pool_free_bytes, - .compact_pool_retired_bytes = ls.profiling.compact_pool_retired_bytes, - .direct_mesh_gpu_bytes = ls.profiling.direct_mesh_gpu_bytes, - .known_memory_bytes = ls.profiling.known_memory_bytes, - .wait_idle_count = ls.profiling.wait_idle_count, - .wait_idle_ms = ls.profiling.wait_idle_ms, - .gpu_culling_overflows = ls.profiling.gpu_culling_overflows, - .gpu_culling_validation_mismatches = ls.profiling.gpu_culling_validation_mismatches, - .gpu_culling_requested = ls.profiling.gpu_culling_requested, - .gpu_culling_threshold = ls.profiling.gpu_culling_threshold, - .gpu_culling_candidate_count = ls.profiling.gpu_culling_candidate_count, - .gpu_culling_candidate_count_max = ls.profiling.gpu_culling_candidate_count_max, - .gpu_culling_draw_submissions = ls.profiling.gpu_culling_draw_submissions, - .gpu_culling_validation_generation = ls.profiling.gpu_culling_validation_generation, - .gpu_culling_validation_completed_generation = ls.profiling.gpu_culling_validation_completed_generation, - .gpu_culling_validation_completed_count = ls.profiling.gpu_culling_validation_completed_count, - .compact_selected = ls.profiling.compact_selected, - .compact_build_rejected = ls.profiling.compact_build_rejected, - .compact_upload_failures = ls.profiling.compact_upload_failures, - .compact_draw_unavailable = ls.profiling.compact_draw_unavailable, - .compact_draw_failures = ls.profiling.compact_draw_failures, - .compact_submissions = ls.profiling.compact_submissions, - .compact_recoveries = ls.profiling.compact_recoveries, - .compact_disabled = ls.profiling.compact_disabled, - }, - }; - } return .{ .chunks_total = rs.chunks_total, .chunks_rendered = rs.chunks_rendered, @@ -829,8 +646,6 @@ pub const WorldScreen = struct { .gen_queue = stats.gen_queue, .mesh_queue = stats.mesh_queue, .upload_queue = stats.upload_queue, - .lod_renderable_regions = lod_renderable_regions, - .lod = lod_display, }; } @@ -978,8 +793,6 @@ pub const WorldScreen = struct { states[@intFromEnum(DebugFeature.sky_fill_debug)] = ctx.settings.debug_sky_fill_active; states[@intFromEnum(DebugFeature.block_light_debug)] = ctx.settings.debug_block_light_active; states[@intFromEnum(DebugFeature.outdoor_factor_debug)] = ctx.settings.debug_outdoor_factor_active; - states[@intFromEnum(DebugFeature.timing_overlay)] = ctx.ui_manager.timing_overlay.enabled; - states[@intFromEnum(DebugFeature.lod_render)] = self.world.telemetry().isLODRenderingEnabled(); states[@intFromEnum(DebugFeature.gpass_render)] = !render_system.getDisableGPassDraw(); states[@intFromEnum(DebugFeature.ssao)] = !render_system.getDisableSSAO(); states[@intFromEnum(DebugFeature.fog)] = self.session.atmosphere.fog_enabled; @@ -1104,18 +917,6 @@ pub const WorldScreen = struct { options.setDebugShadowView(settings_data.anyTerrainDebugActive(ctx.settings)); options.setShadowDebugChannel(resolveShadowDebugChannel(ctx.settings)); }, - .timing_overlay => { - ctx.ui_manager.timing_overlay.toggle(); - rhi.timing().setTimingEnabled(ctx.ui_manager.timing_overlay.enabled); - }, - .lod_render => { - const telemetry = self.world.telemetry(); - if (!telemetry.isLODEnabled()) { - log.log.warn("LOD toggle requested but LOD system is not initialized", .{}); - } else { - _ = telemetry.toggleLODRendering(); - } - }, .gpass_render => { const new_val = !render_system.getDisableGPassDraw(); render_system.setDisableGPassDraw(new_val); diff --git a/modules/game-ui/src/screens/world_debug.zig b/modules/game-ui/src/screens/world_debug.zig index e23b6686..9c483a2f 100644 --- a/modules/game-ui/src/screens/world_debug.zig +++ b/modules/game-ui/src/screens/world_debug.zig @@ -2,15 +2,12 @@ const DebugFeature = @import("engine-ui").DebugFeature; const RenderSystem = @import("engine-graphics").RenderSystem; const rhi_pkg = @import("engine-rhi"); const settings_data = @import("game-core").settings.data; -const log = @import("engine-core").log; const GameSession = @import("game-core").GameSession; const ChunkInspectorOverlay = @import("engine-ui").ChunkInspectorOverlay; const WorldContext = @import("../screen.zig").WorldContext; -const IWorldTelemetry = @import("world-runtime").IWorldTelemetry; pub const ScreenDebugState = struct { session: *GameSession, - world_telemetry: IWorldTelemetry, last_debug_toggle_time: *f32, chunk_inspector_overlay: *ChunkInspectorOverlay, }; @@ -33,8 +30,6 @@ pub fn collectStates(screen: ScreenDebugState, ctx: WorldContext, render_system: states[@intFromEnum(DebugFeature.sky_fill_debug)] = ctx.settings.debug_sky_fill_active; states[@intFromEnum(DebugFeature.block_light_debug)] = ctx.settings.debug_block_light_active; states[@intFromEnum(DebugFeature.outdoor_factor_debug)] = ctx.settings.debug_outdoor_factor_active; - states[@intFromEnum(DebugFeature.timing_overlay)] = ctx.ui_manager.timing_overlay.enabled; - states[@intFromEnum(DebugFeature.lod_render)] = screen.world_telemetry.isLODRenderingEnabled(); states[@intFromEnum(DebugFeature.gpass_render)] = !render_system.getDisableGPassDraw(); states[@intFromEnum(DebugFeature.ssao)] = !render_system.getDisableSSAO(); states[@intFromEnum(DebugFeature.fog)] = screen.session.atmosphere.fog_enabled; @@ -86,13 +81,7 @@ pub fn applyToggle(screen: ScreenDebugState, feature: DebugFeature, ctx: WorldCo }, .shadow_debug, .shadow_cascade_index, .shadow_caster_coverage, .shadow_seam_diag => applyShadowDebugToggle(feature, ctx, render_system, rhi), .direct_key_debug, .sky_fill_debug, .block_light_debug, .outdoor_factor_debug => applyTerrainDebugToggle(feature, ctx, rhi), - .timing_overlay => { - ctx.ui_manager.timing_overlay.toggle(); - rhi.timing().setTimingEnabled(ctx.ui_manager.timing_overlay.enabled); - }, - .lod_render => { - if (!screen.world_telemetry.isLODEnabled()) log.log.warn("LOD toggle requested but LOD system is not initialized", .{}) else _ = screen.world_telemetry.toggleLODRendering(); - }, + .timing_overlay => {}, .gpass_render => render_system.setDisableGPassDraw(!render_system.getDisableGPassDraw()), .ssao => render_system.setDisableSSAO(!render_system.getDisableSSAO()), .fog => screen.session.atmosphere.fog_enabled = !screen.session.atmosphere.fog_enabled, diff --git a/modules/game-ui/src/settings_ui.zig b/modules/game-ui/src/settings_ui.zig index 9942eb22..de26d6a5 100644 --- a/modules/game-ui/src/settings_ui.zig +++ b/modules/game-ui/src/settings_ui.zig @@ -81,7 +81,6 @@ pub fn applyChangedSetting(comptime name: []const u8, settings: *Settings, rs: a } pub fn applyPresetSideEffects(settings: *Settings, rs: anytype) void { - _ = settings_pkg.sanitizeRuntimeConflicts(settings); rs.setAnisotropicFiltering(settings.anisotropic_filtering); rs.setTexturesEnabled(settings.textures_enabled); rs.setShadowResolution(settings.getShadowResolution()); diff --git a/modules/game-ui/src/settings_ui_tests.zig b/modules/game-ui/src/settings_ui_tests.zig index 6f46ec97..7e88ae6f 100644 --- a/modules/game-ui/src/settings_ui_tests.zig +++ b/modules/game-ui/src/settings_ui_tests.zig @@ -211,17 +211,6 @@ test "applyChangedSetting forwards film grain settings" { try testing.expectEqual(@as(f32, 0.25), rs.film_grain_intensity); } -test "applyPresetSideEffects sanitizes LOD and TAA conflict" { - var settings = Settings{ .lod_enabled = true, .taa_enabled = true, .fxaa_enabled = false }; - var rs = MockRenderSettings{}; - - settings_ui.applyPresetSideEffects(&settings, &rs); - - try testing.expect(!settings.taa_enabled); - try testing.expect(settings.fxaa_enabled); - try testing.expect(rs.fxaa_enabled); -} - test "applyPresetSideEffects forwards stable render toggles" { var settings = Settings{ .anisotropic_filtering = 4, .textures_enabled = false, .taa_blend_factor = 0.5, .taa_velocity_rejection = 0.03 }; var rs = MockRenderSettings{}; diff --git a/modules/world-core/src/lod_data.zig b/modules/world-core/src/lod_data.zig deleted file mode 100644 index 87b719e5..00000000 --- a/modules/world-core/src/lod_data.zig +++ /dev/null @@ -1,811 +0,0 @@ -const std = @import("std"); - -const engine_core = @import("engine-core"); -const world_core = @import("root.zig"); - -pub const LODLevel = engine_core.LODLevel; - -pub const LODDataVersion = enum(u16) { - simplified_v1 = 1, - rich_v2 = 2, -}; - -pub const LODColumnProvenance = enum(u8) { - worldgen = 0, - chunk_derived = 1, - edited = 2, - - pub fn canOverwrite(new: LODColumnProvenance, old: LODColumnProvenance) bool { - return @intFromEnum(new) >= @intFromEnum(old); - } -}; - -pub const MAX_LOD_VERTICAL_SPANS: usize = 4; - -pub const LODMaterialLayers = struct { - surface: world_core.BlockType, - subsurface: world_core.BlockType, - foundation: world_core.BlockType, - - pub fn default(surface: world_core.BlockType) LODMaterialLayers { - return .{ - .surface = surface, - .subsurface = surface, - .foundation = surface, - }; - } -}; - -pub const LODWaterState = struct { - is_surface: bool, - surface_height: f32, - depth: f32, - coverage: f32, - - pub const empty: LODWaterState = .{ - .is_surface = false, - .surface_height = 0.0, - .depth = 0.0, - .coverage = 0.0, - }; -}; - -pub const LODLightingHint = struct { - sky_light: u8, - block_light: u8, - ambient_occlusion: f32, - - pub const daylight: LODLightingHint = .{ - .sky_light = 15, - .block_light = 0, - .ambient_occlusion = 1.0, - }; -}; - -pub const LODVegetationHint = struct { - tree_coverage: f32, - avg_tree_height: f32, - offset_x: f32, - offset_z: f32, - trunk: world_core.BlockType, - leaves: world_core.BlockType, - - pub const empty: LODVegetationHint = .{ - .tree_coverage = 0.0, - .avg_tree_height = 0.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .air, - .leaves = .air, - }; -}; - -pub const LODVerticalSpan = struct { - min_height: f32, - max_height: f32, - biome: world_core.BiomeId, - material_layers: LODMaterialLayers, - color: u32, - water: LODWaterState, - lighting: LODLightingHint, - vegetation: LODVegetationHint, - - pub fn fromColumn( - height: f32, - biome: world_core.BiomeId, - layers: LODMaterialLayers, - color: u32, - water_state: LODWaterState, - lighting_hint: LODLightingHint, - vegetation_hint: LODVegetationHint, - ) LODVerticalSpan { - return .{ - .min_height = height, - .max_height = height, - .biome = biome, - .material_layers = layers, - .color = color, - .water = water_state, - .lighting = lighting_hint, - .vegetation = vegetation_hint, - }; - } -}; - -pub fn regionSizeBlocks(lod_level: LODLevel) u32 { - return lod_level.regionSizeBlocks(world_core.CHUNK_SIZE_X); -} - -/// Simplified world data for distant LOD generation. -pub const LODSimplifiedData = struct { - version: LODDataVersion, - width: u32, - heightmap: []f32, - biomes: []world_core.BiomeId, - top_blocks: []world_core.BlockType, - colors: []u32, - material_layers: []LODMaterialLayers, - water: []LODWaterState, - lighting: []LODLightingHint, - vegetation: []LODVegetationHint, - provenance: []LODColumnProvenance, - vertical_span_counts: ?[]u8, - vertical_spans: ?[]LODVerticalSpan, - allocator: std.mem.Allocator, - - pub fn getGridSize(lod_level: LODLevel) u32 { - return switch (lod_level) { - // LOD4 is the fast horizon fallback; LOD3 retains the dense grid - // and replaces it as refinement arrives. - .lod0 => 33, - .lod1 => 65, - .lod2 => 65, - .lod3 => 129, - .lod4 => 65, - }; - } - - pub fn getCellSizeBlocks(lod_level: LODLevel) u32 { - const region_size = regionSizeBlocks(lod_level); - const grid_size = getGridSize(lod_level); - return region_size / @max(grid_size - 1, 1); - } - - /// Returns a seam-friendly (power-of-two cells plus one edge sample) grid - /// at a configurable density. The edge samples are retained so adjacent - /// regions still meet exactly after far-level decimation. - pub fn getGridSizeForDensity(lod_level: LODLevel, density: f32) u32 { - const base_cells = getGridSize(lod_level) - 1; - const clamped = std.math.clamp(density, 0.0625, 1.0); - const requested: u32 = @max(1, @as(u32, @intFromFloat(@floor(@as(f32, @floatFromInt(base_cells)) * clamped)))); - var cells: u32 = 1; - while (cells * 2 <= requested) : (cells *= 2) {} - return cells + 1; - } - - /// Valid source grids retain a power-of-two cell count that divides the - /// region's block width. This makes every cell an integral world-space - /// span, which is required by heightfield seams and compact index topology. - pub fn isSupportedGridSize(lod_level: LODLevel, width: u32) bool { - if (width < 2) return false; - const cells = width - 1; - if ((cells & (cells - 1)) != 0) return false; - return regionSizeBlocks(lod_level) % cells == 0; - } - - pub fn init(allocator: std.mem.Allocator, lod_level: LODLevel) !LODSimplifiedData { - return initWithSampleDensity(allocator, lod_level, 1.0); - } - - /// Allocates source storage using the requested LOD sample density. This - /// changes worker sampling and mesh input cost, not just draw culling. - pub fn initWithSampleDensity(allocator: std.mem.Allocator, lod_level: LODLevel, density: f32) !LODSimplifiedData { - const grid_size = getGridSizeForDensity(lod_level, density); - return initWithGridSize(allocator, lod_level, grid_size); - } - - /// Allocates a validated, seam-compatible source grid. Cache loading uses - /// this exact-width constructor so reduced-density payloads never get - /// silently expanded into a different array topology. - pub fn initWithGridSize(allocator: std.mem.Allocator, lod_level: LODLevel, grid_size: u32) !LODSimplifiedData { - if (!isSupportedGridSize(lod_level, grid_size)) return error.InvalidGridSize; - const count = grid_size * grid_size; - - const heightmap = try allocator.alloc(f32, count); - errdefer allocator.free(heightmap); - const biomes = try allocator.alloc(world_core.BiomeId, count); - errdefer allocator.free(biomes); - const top_blocks = try allocator.alloc(world_core.BlockType, count); - errdefer allocator.free(top_blocks); - const colors = try allocator.alloc(u32, count); - errdefer allocator.free(colors); - const material_layers = try allocator.alloc(LODMaterialLayers, count); - errdefer allocator.free(material_layers); - const water = try allocator.alloc(LODWaterState, count); - errdefer allocator.free(water); - const lighting = try allocator.alloc(LODLightingHint, count); - errdefer allocator.free(lighting); - const vegetation = try allocator.alloc(LODVegetationHint, count); - errdefer allocator.free(vegetation); - const provenance = try allocator.alloc(LODColumnProvenance, count); - errdefer allocator.free(provenance); - - @memset(heightmap, 0.0); - @memset(biomes, .plains); - @memset(top_blocks, .air); - @memset(colors, 0); - @memset(material_layers, LODMaterialLayers.default(.air)); - @memset(water, LODWaterState.empty); - @memset(lighting, LODLightingHint.daylight); - @memset(vegetation, LODVegetationHint.empty); - @memset(provenance, .worldgen); - - return .{ - .version = .rich_v2, - .width = grid_size, - .heightmap = heightmap, - .biomes = biomes, - .top_blocks = top_blocks, - .colors = colors, - .material_layers = material_layers, - .water = water, - .lighting = lighting, - .vegetation = vegetation, - .provenance = provenance, - .vertical_span_counts = null, - .vertical_spans = null, - .allocator = allocator, - }; - } - - pub fn initWithVerticalSpans(allocator: std.mem.Allocator, lod_level: LODLevel) !LODSimplifiedData { - return initWithVerticalSpansSampleDensity(allocator, lod_level, 1.0); - } - - pub fn initWithVerticalSpansSampleDensity(allocator: std.mem.Allocator, lod_level: LODLevel, density: f32) !LODSimplifiedData { - var data = try initWithSampleDensity(allocator, lod_level, density); - errdefer data.deinit(); - try data.enableVerticalSpans(); - return data; - } - - pub fn initWithVerticalSpansGridSize(allocator: std.mem.Allocator, lod_level: LODLevel, grid_size: u32) !LODSimplifiedData { - var data = try initWithGridSize(allocator, lod_level, grid_size); - errdefer data.deinit(); - try data.enableVerticalSpans(); - return data; - } - - pub fn deinit(self: *LODSimplifiedData) void { - self.allocator.free(self.heightmap); - self.allocator.free(self.biomes); - self.allocator.free(self.top_blocks); - self.allocator.free(self.colors); - self.allocator.free(self.material_layers); - self.allocator.free(self.water); - self.allocator.free(self.lighting); - self.allocator.free(self.vegetation); - self.allocator.free(self.provenance); - if (self.vertical_span_counts) |counts| self.allocator.free(counts); - if (self.vertical_spans) |spans| self.allocator.free(spans); - self.* = undefined; - } - - pub fn enableVerticalSpans(self: *LODSimplifiedData) !void { - if (self.vertical_spans != null) return; - - const count = self.width * self.width; - const span_count = @as(usize, @intCast(count)) * MAX_LOD_VERTICAL_SPANS; - const counts = try self.allocator.alloc(u8, count); - errdefer self.allocator.free(counts); - const spans = try self.allocator.alloc(LODVerticalSpan, span_count); - errdefer self.allocator.free(spans); - - @memset(counts, 0); - @memset(spans, LODVerticalSpan.fromColumn(0.0, .plains, LODMaterialLayers.default(.air), 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty)); - - self.vertical_span_counts = counts; - self.vertical_spans = spans; - } - - pub fn hasVerticalSpans(self: *const LODSimplifiedData) bool { - return self.vertical_span_counts != null and self.vertical_spans != null; - } - - pub fn getHeight(self: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - if (gx >= self.width or gz >= self.width) return 0; - return self.heightmap[gz * self.width + gx]; - } - - pub fn setHeight(self: *LODSimplifiedData, gx: u32, gz: u32, height: f32) void { - if (gx >= self.width or gz >= self.width) return; - self.heightmap[gz * self.width + gx] = height; - } - - pub fn setColumn( - self: *LODSimplifiedData, - gx: u32, - gz: u32, - height: f32, - biome: world_core.BiomeId, - layers: LODMaterialLayers, - color: u32, - water_state: LODWaterState, - lighting_hint: LODLightingHint, - vegetation_hint: LODVegetationHint, - ) void { - if (gx >= self.width or gz >= self.width) return; - const idx = gz * self.width + gx; - self.heightmap[idx] = height; - self.biomes[idx] = biome; - self.top_blocks[idx] = layers.surface; - self.colors[idx] = color; - self.material_layers[idx] = layers; - self.water[idx] = water_state; - self.lighting[idx] = lighting_hint; - self.vegetation[idx] = vegetation_hint; - if (self.hasVerticalSpans()) { - _ = self.setVerticalSpan(gx, gz, 0, LODVerticalSpan.fromColumn(height, biome, layers, color, water_state, lighting_hint, vegetation_hint)); - } - } - - pub fn setGeneratedColumn( - self: *LODSimplifiedData, - gx: u32, - gz: u32, - height: f32, - biome: world_core.BiomeId, - layers: LODMaterialLayers, - color: u32, - water_state: LODWaterState, - lighting_hint: LODLightingHint, - vegetation_hint: LODVegetationHint, - ) void { - // Provenance-aware: worldgen generation never overwrites a column held - // by chunk_derived or edited data (issue #752 Phase 2.4). This makes - // every generator's heightmap pass respect real chunk data, so a region - // can regenerate its worldgen columns while preserving chunk-derived - // columns without any special-casing at the call sites. - if (!LODColumnProvenance.worldgen.canOverwrite(self.getColumnProvenance(gx, gz))) return; - - self.setColumn(gx, gz, height, biome, layers, color, water_state, lighting_hint, vegetation_hint); - if (!self.hasVerticalSpans()) return; - - self.clearVerticalSpans(gx, gz); - const terrain_layers = terrainLayersForGeneratedSpan(layers, water_state); - const terrain_height = terrainHeightForGeneratedSpan(height, water_state); - _ = self.setVerticalSpan(gx, gz, 0, .{ - .min_height = 0.0, - .max_height = terrain_height, - .biome = biome, - .material_layers = terrain_layers, - .color = color, - .water = LODWaterState.empty, - .lighting = lighting_hint, - .vegetation = vegetation_hint, - }); - - var span_index: u8 = 1; - - if (water_state.is_surface and water_state.coverage > 0.0 and span_index < MAX_LOD_VERTICAL_SPANS) { - _ = self.setVerticalSpan(gx, gz, span_index, .{ - .min_height = terrain_height, - .max_height = @max(water_state.surface_height, terrain_height), - .biome = biome, - .material_layers = .{ - .surface = .water, - .subsurface = .water, - .foundation = terrain_layers.foundation, - }, - .color = color, - .water = water_state, - .lighting = lighting_hint, - .vegetation = LODVegetationHint.empty, - }); - span_index += 1; - } - - if (vegetation_hint.tree_coverage > 0.0 and vegetation_hint.avg_tree_height >= 2.0 and span_index < MAX_LOD_VERTICAL_SPANS) { - const canopy_top = height + vegetation_hint.avg_tree_height; - const canopy_bottom = @max(height + 1.0, canopy_top - @max(vegetation_hint.avg_tree_height * 0.45, 2.0)); - const leaves = if (vegetation_hint.leaves == .air) world_core.BlockType.leaves else vegetation_hint.leaves; - _ = self.setVerticalSpan(gx, gz, span_index, .{ - .min_height = canopy_bottom, - .max_height = canopy_top, - .biome = biome, - .material_layers = .{ - .surface = leaves, - .subsurface = leaves, - .foundation = terrain_layers.foundation, - }, - .color = blockDefaultColor(leaves, color), - .water = LODWaterState.empty, - .lighting = lighting_hint, - .vegetation = vegetation_hint, - }); - } - } - - fn blockDefaultColor(block: world_core.BlockType, fallback: u32) u32 { - if (block == .air) return fallback; - const color = world_core.block_registry.getBlockDefinition(block).default_color; - const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); - const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); - const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); - return (r << 16) | (g << 8) | b; - } - - fn terrainLayersForGeneratedSpan(layers: LODMaterialLayers, water_state: LODWaterState) LODMaterialLayers { - if (!water_state.is_surface or layers.surface != .water) return layers; - const floor_block = if (layers.subsurface == .air) layers.foundation else layers.subsurface; - return .{ - .surface = floor_block, - .subsurface = floor_block, - .foundation = layers.foundation, - }; - } - - fn terrainHeightForGeneratedSpan(height: f32, water_state: LODWaterState) f32 { - if (!water_state.is_surface) return height; - if (water_state.depth <= 0.0) return @min(height, water_state.surface_height); - return @max(0.0, water_state.surface_height - water_state.depth); - } - - pub fn setColumnProvenance(self: *LODSimplifiedData, gx: u32, gz: u32, provenance: LODColumnProvenance) void { - if (gx >= self.width or gz >= self.width) return; - self.provenance[gz * self.width + gx] = provenance; - } - - pub fn getColumnProvenance(self: *const LODSimplifiedData, gx: u32, gz: u32) LODColumnProvenance { - if (gx >= self.width or gz >= self.width) return .worldgen; - return self.provenance[gz * self.width + gx]; - } - - /// Reset every worldgen-provenance column (height -> 0, spans cleared), - /// leaving chunk_derived/edited columns untouched. Used on generator - /// mismatch so a region can regenerate its worldgen columns from the new - /// generator while preserving real chunk data (issue #752 Phase 2.4). - /// Returns the number of columns that survived (chunk_derived or edited). - pub fn purgeWorldgenColumns(self: *LODSimplifiedData) u32 { - var survived: u32 = 0; - const count = self.width * self.width; - var idx: usize = 0; - while (idx < count) : (idx += 1) { - const p = self.provenance[idx]; - if (p == .worldgen) { - self.heightmap[idx] = 0.0; - if (self.hasVerticalSpans()) { - const gx: u32 = @intCast(idx % self.width); - const gz: u32 = @intCast(idx / self.width); - self.clearVerticalSpans(gx, gz); - } - } else { - survived += 1; - } - } - return survived; - } - - /// True if any column is chunk_derived or edited (i.e. the region carries - /// real chunk data worth preserving across a generator change). - pub fn hasNonWorldgenColumns(self: *const LODSimplifiedData) bool { - for (self.provenance) |p| { - if (p != .worldgen) return true; - } - return false; - } - - pub fn verticalSpanCount(self: *const LODSimplifiedData, gx: u32, gz: u32) u8 { - if (gx >= self.width or gz >= self.width) return 0; - const counts = self.vertical_span_counts orelse return 0; - return counts[gz * self.width + gx]; - } - - pub fn getVerticalSpan(self: *const LODSimplifiedData, gx: u32, gz: u32, span_index: u8) ?LODVerticalSpan { - if (gx >= self.width or gz >= self.width) return null; - if (span_index >= self.verticalSpanCount(gx, gz)) return null; - const spans = self.vertical_spans orelse return null; - const column_idx = @as(usize, @intCast(gz * self.width + gx)); - const idx = column_idx * MAX_LOD_VERTICAL_SPANS + span_index; - return spans[idx]; - } - - pub fn setVerticalSpan(self: *LODSimplifiedData, gx: u32, gz: u32, span_index: u8, span: LODVerticalSpan) bool { - if (gx >= self.width or gz >= self.width) return false; - if (@as(usize, span_index) >= MAX_LOD_VERTICAL_SPANS) return false; - const counts = self.vertical_span_counts orelse return false; - const spans = self.vertical_spans orelse return false; - const column_idx = gz * self.width + gx; - spans[@as(usize, @intCast(column_idx)) * MAX_LOD_VERTICAL_SPANS + span_index] = span; - counts[column_idx] = @max(counts[column_idx], span_index + 1); - return true; - } - - pub fn clearVerticalSpans(self: *LODSimplifiedData, gx: u32, gz: u32) void { - if (gx >= self.width or gz >= self.width) return; - const counts = self.vertical_span_counts orelse return; - counts[gz * self.width + gx] = 0; - } - - pub fn totalMemoryBytes(self: *const LODSimplifiedData) usize { - const count = self.width * self.width; - const count_usize = @as(usize, @intCast(count)); - var total: usize = count_usize * (@sizeOf(f32) + @sizeOf(world_core.BiomeId) + @sizeOf(world_core.BlockType) + @sizeOf(u32) + @sizeOf(LODMaterialLayers) + @sizeOf(LODWaterState) + @sizeOf(LODLightingHint) + @sizeOf(LODVegetationHint) + @sizeOf(LODColumnProvenance)); - if (self.vertical_span_counts != null) total += count_usize * @sizeOf(u8); - if (self.vertical_spans != null) total += @as(usize, @intCast(count)) * MAX_LOD_VERTICAL_SPANS * @sizeOf(LODVerticalSpan); - return total; - } - - /// Reuses exact coincident parent samples for one of its four child - /// regions. Reuse is permitted only when both grids have the same - /// world-space cell size (`parent.width == child.width * 2 - 1`); that - /// restriction keeps shared edges byte-for-byte identical rather than - /// blending unrelated coarse footprints. Returns copied sample count. - pub fn reuseAlignedParentSamples(child: *LODSimplifiedData, parent: *const LODSimplifiedData, child_x: u1, child_z: u1) u32 { - if (parent.width != child.width * 2 - 1) return 0; - const offset_x = @as(u32, child_x) * (child.width - 1); - const offset_z = @as(u32, child_z) * (child.width - 1); - var copied: u32 = 0; - var z: u32 = 0; - while (z < child.width) : (z += 1) { - var x: u32 = 0; - while (x < child.width) : (x += 1) { - if (child.getColumnProvenance(x, z) != .worldgen) continue; - const dst = x + z * child.width; - const src = (x + offset_x) + (z + offset_z) * parent.width; - child.heightmap[dst] = parent.heightmap[src]; - child.biomes[dst] = parent.biomes[src]; - child.top_blocks[dst] = parent.top_blocks[src]; - child.colors[dst] = parent.colors[src]; - child.material_layers[dst] = parent.material_layers[src]; - child.water[dst] = parent.water[src]; - child.lighting[dst] = parent.lighting[src]; - child.vegetation[dst] = parent.vegetation[src]; - copied += 1; - } - } - return copied; - } -}; - -test "LODSimplifiedData initializes rich column defaults" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod3); - defer data.deinit(); - - try std.testing.expectEqual(@as(f32, 0.0), data.getHeight(0, 0)); - try std.testing.expectEqual(world_core.BlockType.air, data.top_blocks[0]); - try std.testing.expectEqual(world_core.BlockType.air, data.material_layers[0].surface); - try std.testing.expect(!data.water[0].is_surface); - try std.testing.expectEqual(@as(u8, 15), data.lighting[0].sky_light); - try std.testing.expectEqual(@as(f32, 0.0), data.vegetation[0].tree_coverage); -} - -test "LODSimplifiedData far levels use configured detail grids" { - try std.testing.expectEqual(@as(u32, 33), LODSimplifiedData.getGridSize(.lod0)); - try std.testing.expectEqual(@as(u32, 65), LODSimplifiedData.getGridSize(.lod1)); - try std.testing.expectEqual(@as(u32, 65), LODSimplifiedData.getGridSize(.lod2)); - try std.testing.expectEqual(@as(u32, 129), LODSimplifiedData.getGridSize(.lod3)); - try std.testing.expectEqual(@as(u32, 65), LODSimplifiedData.getGridSize(.lod4)); -} - -test "LODSimplifiedData density reduces far worker and mesh samples" { - const allocator = std.testing.allocator; - var lod3 = try LODSimplifiedData.initWithSampleDensity(allocator, .lod3, 0.5); - defer lod3.deinit(); - var lod4 = try LODSimplifiedData.initWithSampleDensity(allocator, .lod4, 0.25); - defer lod4.deinit(); - - try std.testing.expectEqual(@as(u32, 65), lod3.width); - try std.testing.expectEqual(@as(u32, 17), lod4.width); - try std.testing.expect(lod3.heightmap.len < @as(usize, 129 * 129)); - try std.testing.expect(lod4.heightmap.len < @as(usize, 65 * 65)); -} - -test "LODSimplifiedData accepts every integral far density grid" { - try std.testing.expect(LODSimplifiedData.isSupportedGridSize(.lod3, 17)); - try std.testing.expect(LODSimplifiedData.isSupportedGridSize(.lod3, 65)); - try std.testing.expect(LODSimplifiedData.isSupportedGridSize(.lod3, 129)); - try std.testing.expect(LODSimplifiedData.isSupportedGridSize(.lod4, 17)); - try std.testing.expect(LODSimplifiedData.isSupportedGridSize(.lod4, 65)); - try std.testing.expect(!LODSimplifiedData.isSupportedGridSize(.lod4, 18)); -} - -test "LODSimplifiedData reuses only aligned coarse parent samples" { - const allocator = std.testing.allocator; - var parent = try LODSimplifiedData.initWithSampleDensity(allocator, .lod4, 1.0); - defer parent.deinit(); - var child = try LODSimplifiedData.initWithSampleDensity(allocator, .lod3, 0.5); - defer child.deinit(); - parent.setHeight(64, 64, 91.0); - try std.testing.expectEqual(@as(u32, 65 * 65), child.reuseAlignedParentSamples(&parent, 1, 1)); - try std.testing.expectEqual(@as(f32, 91.0), child.getHeight(0, 0)); -} - -test "LODColumnProvenance orders overwrite authority" { - try std.testing.expect(LODColumnProvenance.chunk_derived.canOverwrite(.worldgen)); - try std.testing.expect(LODColumnProvenance.edited.canOverwrite(.chunk_derived)); - try std.testing.expect(!LODColumnProvenance.worldgen.canOverwrite(.chunk_derived)); - try std.testing.expect(!LODColumnProvenance.chunk_derived.canOverwrite(.edited)); -} - -test "LODSimplifiedData setColumn stores rich representative data" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - data.setColumn(1, 2, 63.0, .ocean, .{ - .surface = .water, - .subsurface = .sand, - .foundation = .stone, - }, 0x3355AA, .{ - .is_surface = true, - .surface_height = 63.0, - .depth = 12.0, - .coverage = 1.0, - }, .{ - .sky_light = 15, - .block_light = 0, - .ambient_occlusion = 0.9, - }, .{ - .tree_coverage = 0.5, - .avg_tree_height = 7.0, - .offset_x = 0.25, - .offset_z = -0.25, - .trunk = .wood, - .leaves = .leaves, - }); - - const idx = 1 + 2 * data.width; - try std.testing.expectEqual(@as(f32, 63.0), data.heightmap[idx]); - try std.testing.expectEqual(world_core.BlockType.water, data.top_blocks[idx]); - try std.testing.expectEqual(world_core.BlockType.sand, data.material_layers[idx].subsurface); - try std.testing.expect(data.water[idx].is_surface); - try std.testing.expectEqual(@as(f32, 12.0), data.water[idx].depth); - try std.testing.expectEqual(@as(f32, 0.9), data.lighting[idx].ambient_occlusion); - try std.testing.expectEqual(world_core.BlockType.leaves, data.vegetation[idx].leaves); -} - -test "LODSimplifiedData tracks bounded vertical spans when enabled" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - - try std.testing.expectEqual(LODDataVersion.rich_v2, data.version); - try std.testing.expect(data.hasVerticalSpans()); - try std.testing.expectEqual(@as(u8, 0), data.verticalSpanCount(3, 4)); - - try std.testing.expect(data.setVerticalSpan(3, 4, 0, .{ - .min_height = 72.0, - .max_height = 76.0, - .biome = .plains, - .material_layers = .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, - .color = 0x66AA44, - .water = LODWaterState.empty, - .lighting = LODLightingHint.daylight, - .vegetation = LODVegetationHint.empty, - })); - try std.testing.expect(data.setVerticalSpan(3, 4, 1, .{ - .min_height = 44.0, - .max_height = 48.0, - .biome = .plains, - .material_layers = .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, - .color = 0x777777, - .water = LODWaterState.empty, - .lighting = .{ .sky_light = 8, .block_light = 0, .ambient_occlusion = 0.6 }, - .vegetation = LODVegetationHint.empty, - })); - - try std.testing.expectEqual(@as(u8, 2), data.verticalSpanCount(3, 4)); - const lower = data.getVerticalSpan(3, 4, 1) orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(f32, 44.0), lower.min_height); - try std.testing.expectEqual(world_core.BlockType.stone, lower.material_layers.surface); - try std.testing.expect(!data.setVerticalSpan(3, 4, @intCast(MAX_LOD_VERTICAL_SPANS), lower)); -} - -test "LODSimplifiedData memory accounting includes optional vertical spans" { - const allocator = std.testing.allocator; - var baseline = try LODSimplifiedData.init(allocator, .lod1); - defer baseline.deinit(); - var rich = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod1); - defer rich.deinit(); - - const count = @as(usize, @intCast(baseline.width * baseline.width)); - const span_bytes = count * (@sizeOf(u8) + MAX_LOD_VERTICAL_SPANS * @sizeOf(LODVerticalSpan)); - try std.testing.expectEqual(baseline.totalMemoryBytes() + span_bytes, rich.totalMemoryBytes()); -} - -test "LODSimplifiedData setColumn seeds representative span when enabled" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod1); - defer data.deinit(); - - data.setColumn(2, 2, 80.0, .forest, .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }, 0x22AA44, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - - try std.testing.expectEqual(@as(u8, 1), data.verticalSpanCount(2, 2)); - const span = data.getVerticalSpan(2, 2, 0) orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(f32, 80.0), span.max_height); - try std.testing.expectEqual(world_core.BlockType.grass, span.material_layers.surface); -} - -test "LODSimplifiedData generated columns emit surface and water spans" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod1); - defer data.deinit(); - - data.setGeneratedColumn(1, 1, 64.0, .ocean, .{ - .surface = .water, - .subsurface = .sand, - .foundation = .stone, - }, 0x3355AA, .{ - .is_surface = true, - .surface_height = 64.0, - .depth = 10.0, - .coverage = 1.0, - }, LODLightingHint.daylight, LODVegetationHint.empty); - - try std.testing.expectEqual(@as(f32, 64.0), data.getHeight(1, 1)); - try std.testing.expectEqual(world_core.BlockType.water, data.top_blocks[1 + data.width]); - try std.testing.expectEqual(@as(u8, 2), data.verticalSpanCount(1, 1)); - - const surface = data.getVerticalSpan(1, 1, 0) orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(f32, 0.0), surface.min_height); - try std.testing.expectEqual(@as(f32, 54.0), surface.max_height); - try std.testing.expectEqual(world_core.BlockType.sand, surface.material_layers.surface); - try std.testing.expect(!surface.water.is_surface); - - const water = data.getVerticalSpan(1, 1, 1) orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(f32, 54.0), water.min_height); - try std.testing.expectEqual(@as(f32, 64.0), water.max_height); - try std.testing.expectEqual(world_core.BlockType.water, water.material_layers.surface); - try std.testing.expect(water.water.is_surface); -} - -test "LODSimplifiedData generated columns emit canopy spans" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod1); - defer data.deinit(); - - data.setGeneratedColumn(2, 2, 70.0, .forest, .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }, 0x2E7A32, LODWaterState.empty, LODLightingHint.daylight, .{ - .tree_coverage = 0.75, - .avg_tree_height = 8.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }); - - try std.testing.expectEqual(@as(u8, 2), data.verticalSpanCount(2, 2)); - const canopy = data.getVerticalSpan(2, 2, 1) orelse return error.TestExpectedEqual; - try std.testing.expectEqual(world_core.BlockType.leaves, canopy.material_layers.surface); - try std.testing.expect(canopy.min_height > 70.0); - try std.testing.expectEqual(@as(f32, 78.0), canopy.max_height); - try std.testing.expectEqual(@as(f32, 0.75), canopy.vegetation.tree_coverage); -} - -test "setGeneratedColumn never overwrites chunk_derived or edited columns" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - // A chunk_derived column at height 70. - data.setColumn(2, 2, 70.0, .forest, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - data.setColumnProvenance(2, 2, .chunk_derived); - - // Worldgen regen attempts to overwrite it with a different height. - data.setGeneratedColumn(2, 2, 10.0, .desert, .{ .surface = .sand, .subsurface = .sand, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - - try std.testing.expectEqual(@as(f32, 70.0), data.getHeight(2, 2)); - try std.testing.expectEqual(LODColumnProvenance.chunk_derived, data.getColumnProvenance(2, 2)); - - // A neighboring worldgen column is still filled normally. - data.setGeneratedColumn(3, 3, 40.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - try std.testing.expectEqual(@as(f32, 40.0), data.getHeight(3, 3)); -} - -test "purgeWorldgenColumns preserves chunk_derived and edited data" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - - data.setGeneratedColumn(0, 0, 30.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - data.setColumn(1, 1, 80.0, .mountains, .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - data.setColumnProvenance(1, 1, .edited); - - const survived = data.purgeWorldgenColumns(); - try std.testing.expectEqual(@as(u32, 1), survived); - try std.testing.expect(data.hasNonWorldgenColumns()); - - // Worldgen column reset, edited column preserved. - try std.testing.expectEqual(@as(f32, 0.0), data.getHeight(0, 0)); - try std.testing.expectEqual(@as(f32, 80.0), data.getHeight(1, 1)); - try std.testing.expectEqual(LODColumnProvenance.edited, data.getColumnProvenance(1, 1)); -} diff --git a/modules/world-core/src/root.zig b/modules/world-core/src/root.zig index 0aae6f6e..ba3d03fb 100644 --- a/modules/world-core/src/root.zig +++ b/modules/world-core/src/root.zig @@ -4,7 +4,6 @@ pub const chunk = @import("chunk.zig"); pub const chunk_constants = @import("chunk_constants.zig"); pub const chunk_key = @import("chunk_key.zig"); pub const light = @import("light.zig"); -pub const lod_data = @import("lod_data.zig"); pub const telemetry = @import("telemetry.zig"); pub const biome_and_block_tests = @import("biome_and_block_tests.zig"); pub const block_biome_tests = @import("block_biome_tests.zig"); @@ -47,16 +46,5 @@ pub const worldToChunkFromFloat = chunk.worldToChunkFromFloat; pub const worldToChunk = chunk.worldToChunk; pub const worldToLocal = chunk.worldToLocal; pub const PackedLight = light.PackedLight; -pub const LODLevel = lod_data.LODLevel; -pub const LODDataVersion = lod_data.LODDataVersion; -pub const LODLightingHint = lod_data.LODLightingHint; -pub const LODMaterialLayers = lod_data.LODMaterialLayers; -pub const LODSimplifiedData = lod_data.LODSimplifiedData; -pub const LODColumnProvenance = lod_data.LODColumnProvenance; -pub const LODVegetationHint = lod_data.LODVegetationHint; -pub const LODVerticalSpan = lod_data.LODVerticalSpan; -pub const LODWaterState = lod_data.LODWaterState; -pub const MAX_LOD_VERTICAL_SPANS = lod_data.MAX_LOD_VERTICAL_SPANS; -pub const regionSizeBlocks = lod_data.regionSizeBlocks; pub const ChunkStateCounts = telemetry.ChunkStateCounts; pub const WorldStateData = telemetry.WorldStateData; diff --git a/modules/world-lod/build.zig b/modules/world-lod/build.zig deleted file mode 100644 index 4a39e747..00000000 --- a/modules/world-lod/build.zig +++ /dev/null @@ -1,5 +0,0 @@ -const std = @import("std"); - -pub fn build(b: *std.Build) void { - _ = b.addModule("world-lod", .{ .root_source_file = b.path("src/root.zig") }); -} diff --git a/modules/world-lod/build.zig.zon b/modules/world-lod/build.zig.zon deleted file mode 100644 index 20465054..00000000 --- a/modules/world-lod/build.zig.zon +++ /dev/null @@ -1 +0,0 @@ -.{ .name = .world_lod, .version = "0.1.0", .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon", "src" }, .dependencies = .{} } diff --git a/modules/world-lod/src/biome_color_provider.zig b/modules/world-lod/src/biome_color_provider.zig deleted file mode 100644 index 7ea67af1..00000000 --- a/modules/world-lod/src/biome_color_provider.zig +++ /dev/null @@ -1,122 +0,0 @@ -const world_core = @import("world-core"); -const BiomeId = world_core.BiomeId; -const BlockType = world_core.BlockType; - -pub const BiomeColors = struct { - grass: [3]f32 = .{ 0.22, 0.72, 0.16 }, - foliage: [3]f32 = .{ 0.14, 0.58, 0.12 }, - water: [3]f32 = .{ 0.12, 0.38, 0.78 }, -}; - -pub fn getBiomeColors(biome_id: BiomeId) BiomeColors { - return switch (biome_id) { - .deep_ocean => .{ .water = .{ 0.1, 0.2, 0.5 } }, - .frozen_ocean => .{ .grass = .{ 0.78, 0.88, 0.92 }, .foliage = .{ 0.66, 0.78, 0.82 }, .water = .{ 0.32, 0.54, 0.74 } }, - .cold_ocean => .{ .water = .{ 0.10, 0.32, 0.62 } }, - .warm_ocean => .{ .water = .{ 0.08, 0.50, 0.82 } }, - .tropical => .{ .grass = .{ 0.18, 0.74, 0.18 }, .foliage = .{ 0.10, 0.62, 0.10 }, .water = .{ 0.05, 0.55, 0.85 } }, - .plains => .{}, - .forest => .{ .grass = .{ 0.18, 0.64, 0.16 }, .foliage = .{ 0.12, 0.52, 0.12 } }, - .birch_forest => .{ .grass = .{ 0.24, 0.68, 0.18 }, .foliage = .{ 0.18, 0.58, 0.14 } }, - .dark_forest => .{ .grass = .{ 0.12, 0.46, 0.12 }, .foliage = .{ 0.08, 0.36, 0.08 } }, - .flower_forest => .{ .grass = .{ 0.30, 0.72, 0.18 }, .foliage = .{ 0.20, 0.58, 0.12 } }, - .taiga => .{ .grass = .{ 0.24, 0.56, 0.24 }, .foliage = .{ 0.18, 0.46, 0.18 } }, - .snowy_taiga => .{ .grass = .{ 0.62, 0.74, 0.70 }, .foliage = .{ 0.16, 0.40, 0.24 } }, - .old_growth_taiga => .{ .grass = .{ 0.20, 0.48, 0.28 }, .foliage = .{ 0.12, 0.34, 0.20 } }, - .desert => .{ .grass = .{ 0.75, 0.70, 0.35 } }, - .snow_tundra => .{ .grass = .{ 0.7, 0.75, 0.8 } }, - .snowy_mountains => .{ .grass = .{ 0.85, 0.90, 0.95 } }, - .stony_shore => .{ .grass = .{ 0.48, 0.52, 0.50 }, .foliage = .{ 0.34, 0.42, 0.36 }, .water = .{ 0.10, 0.34, 0.62 } }, - .snowy_beach => .{ .grass = .{ 0.82, 0.90, 0.94 }, .foliage = .{ 0.68, 0.78, 0.82 }, .water = .{ 0.22, 0.46, 0.70 } }, - .meadow => .{ .grass = .{ 0.32, 0.74, 0.24 }, .foliage = .{ 0.20, 0.58, 0.18 } }, - .grove => .{ .grass = .{ 0.20, 0.48, 0.24 }, .foliage = .{ 0.16, 0.38, 0.18 } }, - .snowy_slopes => .{ .grass = .{ 0.82, 0.88, 0.94 }, .foliage = .{ 0.64, 0.72, 0.70 } }, - .jagged_peaks => .{ .grass = .{ 0.56, 0.56, 0.54 }, .foliage = .{ 0.44, 0.46, 0.42 } }, - .frozen_peaks => .{ .grass = .{ 0.76, 0.88, 0.96 }, .foliage = .{ 0.68, 0.78, 0.82 } }, - .stony_peaks => .{ .grass = .{ 0.62, 0.58, 0.48 }, .foliage = .{ 0.46, 0.44, 0.34 } }, - .swamp => .{ .grass = .{ 0.26, 0.58, 0.18 }, .foliage = .{ 0.22, 0.52, 0.16 }, .water = .{ 0.16, 0.38, 0.30 } }, - .frozen_river => .{ .grass = .{ 0.78, 0.88, 0.92 }, .foliage = .{ 0.66, 0.78, 0.82 }, .water = .{ 0.36, 0.58, 0.78 } }, - .jungle => .{ .grass = .{ 0.10, 0.76, 0.08 }, .foliage = .{ 0.08, 0.62, 0.08 } }, - .savanna => .{ .grass = .{ 0.55, 0.55, 0.30 }, .foliage = .{ 0.50, 0.50, 0.28 } }, - .badlands => .{ .grass = .{ 0.5, 0.4, 0.3 } }, - .mushroom_fields => .{ .grass = .{ 0.4, 0.8, 0.4 } }, - .foothills => .{ .grass = .{ 0.24, 0.62, 0.22 }, .foliage = .{ 0.18, 0.50, 0.16 } }, - .dry_plains => .{ .grass = .{ 0.55, 0.50, 0.28 } }, - .coastal_plains => .{ .grass = .{ 0.24, 0.66, 0.24 }, .foliage = .{ 0.18, 0.52, 0.16 } }, - else => .{}, - }; -} - -pub fn getGrassTintColor(biome_id: BiomeId) u32 { - return packRgb(getBiomeColors(biome_id).grass); -} - -pub fn getFoliageTintColor(biome_id: BiomeId) u32 { - return packRgb(getBiomeColors(biome_id).foliage); -} - -pub fn getWaterTintColor(biome_id: BiomeId) u32 { - return packRgb(getBiomeColors(biome_id).water); -} - -pub fn getBlockTintColor(biome_id: BiomeId, block: BlockType) u32 { - if (block == .grass) return getGrassTintColor(biome_id); - if (block == .water) return getWaterTintColor(biome_id); - if (isLeafBlock(block)) return getLeafTintColor(biome_id, block); - return 0xFFFFFF; -} - -pub fn getLeafTintColor(biome_id: BiomeId, block: BlockType) u32 { - const foliage = getBiomeColors(biome_id).foliage; - if (block == .leaves) return packRgb(foliage); - - const base = world_core.block_registry.getBlockDefinition(block).default_color; - return packRgb(.{ - base[0] * 0.70 + foliage[0] * 0.30, - base[1] * 0.70 + foliage[1] * 0.30, - base[2] * 0.70 + foliage[2] * 0.30, - }); -} - -fn isLeafBlock(block: BlockType) bool { - return switch (block) { - .leaves, - .mangrove_leaves, - .jungle_leaves, - .acacia_leaves, - .birch_leaves, - .spruce_leaves, - => true, - else => false, - }; -} - -fn packRgb(color: [3]f32) u32 { - const r: u32 = @intFromFloat(@round(clamp01(color[0]) * 255.0)); - const g: u32 = @intFromFloat(@round(clamp01(color[1]) * 255.0)); - const b: u32 = @intFromFloat(@round(clamp01(color[2]) * 255.0)); - return (r << 16) | (g << 8) | b; -} - -fn clamp01(value: f32) f32 { - if (value < 0.0) return 0.0; - if (value > 1.0) return 1.0; - return value; -} - -pub fn getBiomeColor(biome_id: BiomeId) u32 { - return switch (biome_id) { - .deep_ocean, .ocean, .warm_ocean, .cold_ocean, .frozen_ocean, .river, .frozen_river => getWaterTintColor(biome_id), - else => getGrassTintColor(biome_id), - }; -} - -test "LOD biome color provider matches chunk grass tint" { - try @import("std").testing.expectEqual(@as(u32, 0x38B829), getGrassTintColor(.plains)); - try @import("std").testing.expectEqual(@as(u32, 0x2EA329), getGrassTintColor(.forest)); -} - -test "LOD biome color provider matches chunk foliage tint" { - try @import("std").testing.expectEqual(@as(u32, 0x24941F), getFoliageTintColor(.plains)); - try @import("std").testing.expectEqual(@as(u32, 0x1F851F), getFoliageTintColor(.forest)); -} diff --git a/modules/world-lod/src/lod_cache.zig b/modules/world-lod/src/lod_cache.zig deleted file mode 100644 index c3c96305..00000000 --- a/modules/world-lod/src/lod_cache.zig +++ /dev/null @@ -1,682 +0,0 @@ -//! Disk serialization for generated LOD source data. - -const std = @import("std"); - -const world_core = @import("world-core"); -const LODLevel = @import("lod_types.zig").LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; -const LODMaterialLayers = world_core.LODMaterialLayers; -const LODWaterState = world_core.LODWaterState; -const LODLightingHint = world_core.LODLightingHint; -const LODVegetationHint = world_core.LODVegetationHint; -const LODVerticalSpan = world_core.LODVerticalSpan; -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; - -pub const MAGIC: u32 = 0x5A4C4F44; // "ZLOD" -pub const CACHE_VERSION: u8 = 11; -const CACHE_VERSION_V10: u8 = 10; -pub const CACHE_VERSION_V1: u8 = 1; -pub const HEADER_SIZE: usize = 42; - -pub const Key = struct { - seed: u64, - generator_identity_hash: u64, - generator_version: u32, - rx: i32, - rz: i32, - lod: LODLevel, -}; - -pub const CacheError = error{ - InvalidMagic, - UnsupportedVersion, - DataTooShort, - InvalidKey, - InvalidWidth, - InvalidBiome, - InvalidBlock, - InvalidSpanCount, - InvalidProvenance, - MissingProvenance, - ChecksumMismatch, -}; - -pub const ColumnProvenance = world_core.LODColumnProvenance; - -const BIOME_COUNT: usize = @typeInfo(BiomeId).@"enum".fields.len; -const BLOCK_COUNT: usize = @typeInfo(BlockType).@"enum".fields.len; -const HEIGHT_WIRE_SIZE: usize = @sizeOf(f32); -const BIOME_WIRE_SIZE: usize = @sizeOf(BiomeId); -const BLOCK_WIRE_SIZE: usize = @sizeOf(BlockType); -const COLOR_WIRE_SIZE: usize = @sizeOf(u32); -const MATERIAL_LAYERS_WIRE_SIZE: usize = 3 * BLOCK_WIRE_SIZE; -const WATER_WIRE_SIZE: usize = @sizeOf(u8) + 3 * @sizeOf(f32); -const LIGHTING_WIRE_SIZE: usize = 2 * @sizeOf(u8) + @sizeOf(f32); -const VEGETATION_WIRE_SIZE: usize = 4 * @sizeOf(f32) + 2 * BLOCK_WIRE_SIZE; -const CELL_WIRE_SIZE: usize = HEIGHT_WIRE_SIZE + BIOME_WIRE_SIZE + BLOCK_WIRE_SIZE + COLOR_WIRE_SIZE + MATERIAL_LAYERS_WIRE_SIZE + WATER_WIRE_SIZE + LIGHTING_WIRE_SIZE + VEGETATION_WIRE_SIZE; -const SPAN_FLAGS_WIRE_SIZE: usize = @sizeOf(u8); -const SPAN_WIRE_SIZE: usize = 2 * HEIGHT_WIRE_SIZE + BIOME_WIRE_SIZE + MATERIAL_LAYERS_WIRE_SIZE + COLOR_WIRE_SIZE + WATER_WIRE_SIZE + LIGHTING_WIRE_SIZE + VEGETATION_WIRE_SIZE; - -comptime { - std.debug.assert(MATERIAL_LAYERS_WIRE_SIZE == 3); - std.debug.assert(WATER_WIRE_SIZE == 13); - std.debug.assert(LIGHTING_WIRE_SIZE == 6); - std.debug.assert(VEGETATION_WIRE_SIZE == 18); - std.debug.assert(CELL_WIRE_SIZE == 50); - std.debug.assert(SPAN_WIRE_SIZE == 53); -} - -fn payloadSize(count: usize) usize { - return count * CELL_WIRE_SIZE; -} - -pub fn serializedSize(data: *const LODSimplifiedData) usize { - const count = @as(usize, @intCast(data.width)) * @as(usize, @intCast(data.width)); - var total = HEADER_SIZE + payloadSize(count) + SPAN_FLAGS_WIRE_SIZE + count; - if (data.hasVerticalSpans()) { - total += count; // vertical span counts - total += count * world_core.MAX_LOD_VERTICAL_SPANS * SPAN_WIRE_SIZE; - } - return total; -} - -/// Creates an allocator-owned snapshot suitable for handing to an asynchronous -/// serializer. Source data remains owned by the LOD region and may be edited -/// as soon as this returns. -pub fn cloneSourceData(data: *const LODSimplifiedData, lod: LODLevel, allocator: std.mem.Allocator) !LODSimplifiedData { - var copy = if (data.hasVerticalSpans()) - try LODSimplifiedData.initWithVerticalSpansGridSize(allocator, lod, data.width) - else - try LODSimplifiedData.initWithGridSize(allocator, lod, data.width); - errdefer copy.deinit(); - - copy.version = data.version; - @memcpy(copy.heightmap, data.heightmap); - @memcpy(copy.biomes, data.biomes); - @memcpy(copy.top_blocks, data.top_blocks); - @memcpy(copy.colors, data.colors); - @memcpy(copy.material_layers, data.material_layers); - @memcpy(copy.water, data.water); - @memcpy(copy.lighting, data.lighting); - @memcpy(copy.vegetation, data.vegetation); - @memcpy(copy.provenance, data.provenance); - if (data.vertical_span_counts) |counts| @memcpy(copy.vertical_span_counts.?, counts); - if (data.vertical_spans) |spans| @memcpy(copy.vertical_spans.?, spans); - return copy; -} - -fn writeF32(buf: []u8, value: f32) void { - std.mem.writeInt(u32, buf[0..4], @as(u32, @bitCast(value)), .little); -} - -fn readF32(buf: []const u8) f32 { - return @as(f32, @bitCast(std.mem.readInt(u32, buf[0..4], .little))); -} - -fn writeBlock(buf: []u8, block: BlockType) void { - buf[0] = @intFromEnum(block); -} - -fn readBlock(byte: u8) !BlockType { - if (byte >= BLOCK_COUNT) return CacheError.InvalidBlock; - return std.enums.fromInt(BlockType, byte) orelse CacheError.InvalidBlock; -} - -fn readBiome(byte: u8) !BiomeId { - if (byte >= BIOME_COUNT) return CacheError.InvalidBiome; - return std.enums.fromInt(BiomeId, byte) orelse CacheError.InvalidBiome; -} - -fn writeMaterialLayers(buf: []u8, layers: LODMaterialLayers) void { - writeBlock(buf[0..1], layers.surface); - writeBlock(buf[1..2], layers.subsurface); - writeBlock(buf[2..3], layers.foundation); -} - -fn readMaterialLayers(buf: []const u8) !LODMaterialLayers { - return .{ - .surface = try readBlock(buf[0]), - .subsurface = try readBlock(buf[1]), - .foundation = try readBlock(buf[2]), - }; -} - -fn writeWater(buf: []u8, water: LODWaterState) void { - buf[0] = if (water.is_surface) 1 else 0; - writeF32(buf[1..][0..4], water.surface_height); - writeF32(buf[5..][0..4], water.depth); - writeF32(buf[9..][0..4], water.coverage); -} - -fn readWater(buf: []const u8) LODWaterState { - return .{ - .is_surface = buf[0] != 0, - .surface_height = readF32(buf[1..][0..4]), - .depth = readF32(buf[5..][0..4]), - .coverage = readF32(buf[9..][0..4]), - }; -} - -fn writeLighting(buf: []u8, lighting: LODLightingHint) void { - buf[0] = lighting.sky_light; - buf[1] = lighting.block_light; - writeF32(buf[2..][0..4], lighting.ambient_occlusion); -} - -fn readLighting(buf: []const u8) LODLightingHint { - return .{ - .sky_light = buf[0], - .block_light = buf[1], - .ambient_occlusion = readF32(buf[2..][0..4]), - }; -} - -fn writeVegetation(buf: []u8, vegetation: LODVegetationHint) void { - writeF32(buf[0..][0..4], vegetation.tree_coverage); - writeF32(buf[4..][0..4], vegetation.avg_tree_height); - writeF32(buf[8..][0..4], vegetation.offset_x); - writeF32(buf[12..][0..4], vegetation.offset_z); - writeBlock(buf[16..17], vegetation.trunk); - writeBlock(buf[17..18], vegetation.leaves); -} - -fn readVegetation(buf: []const u8) !LODVegetationHint { - return .{ - .tree_coverage = readF32(buf[0..][0..4]), - .avg_tree_height = readF32(buf[4..][0..4]), - .offset_x = readF32(buf[8..][0..4]), - .offset_z = readF32(buf[12..][0..4]), - .trunk = try readBlock(buf[16]), - .leaves = try readBlock(buf[17]), - }; -} - -fn writeSpan(buf: []u8, span: LODVerticalSpan) void { - var off: usize = 0; - writeF32(buf[off..][0..4], span.min_height); - off += 4; - writeF32(buf[off..][0..4], span.max_height); - off += 4; - buf[off] = @intFromEnum(span.biome); - off += 1; - writeMaterialLayers(buf[off..][0..3], span.material_layers); - off += 3; - std.mem.writeInt(u32, buf[off..][0..4], span.color, .little); - off += 4; - writeWater(buf[off..][0..WATER_WIRE_SIZE], span.water); - off += WATER_WIRE_SIZE; - writeLighting(buf[off..][0..LIGHTING_WIRE_SIZE], span.lighting); - off += LIGHTING_WIRE_SIZE; - writeVegetation(buf[off..][0..VEGETATION_WIRE_SIZE], span.vegetation); -} - -fn readSpan(buf: []const u8) !LODVerticalSpan { - var off: usize = 0; - const min_height = readF32(buf[off..][0..4]); - off += 4; - const max_height = readF32(buf[off..][0..4]); - off += 4; - const biome = try readBiome(buf[off]); - off += 1; - const material_layers = try readMaterialLayers(buf[off..][0..3]); - off += 3; - const color = std.mem.readInt(u32, buf[off..][0..4], .little); - off += 4; - const water = readWater(buf[off..][0..WATER_WIRE_SIZE]); - off += WATER_WIRE_SIZE; - const lighting = readLighting(buf[off..][0..LIGHTING_WIRE_SIZE]); - off += LIGHTING_WIRE_SIZE; - const vegetation = try readVegetation(buf[off..][0..VEGETATION_WIRE_SIZE]); - return .{ .min_height = min_height, .max_height = max_height, .biome = biome, .material_layers = material_layers, .color = color, .water = water, .lighting = lighting, .vegetation = vegetation }; -} - -fn computeCrc(bytes: []const u8) u32 { - var crc = std.hash.Crc32.init(); - crc.update(bytes[0..6]); - crc.update(&.{ 0, 0, 0, 0 }); - crc.update(bytes[10..]); - return crc.final(); -} - -pub fn serialize(data: *const LODSimplifiedData, key: Key, allocator: std.mem.Allocator) ![]u8 { - const width_usize = @as(usize, @intCast(data.width)); - const count = width_usize * width_usize; - const total_size = serializedSize(data); - const buf = try allocator.alloc(u8, total_size); - errdefer allocator.free(buf); - - var off: usize = 0; - std.mem.writeInt(u32, buf[off..][0..4], MAGIC, .little); - off += 4; - buf[off] = CACHE_VERSION; - off += 1; - buf[off] = @intFromEnum(key.lod); - off += 1; - std.mem.writeInt(u32, buf[off..][0..4], 0, .little); - off += 4; - std.mem.writeInt(u64, buf[off..][0..8], key.seed, .little); - off += 8; - std.mem.writeInt(u64, buf[off..][0..8], key.generator_identity_hash, .little); - off += 8; - std.mem.writeInt(u32, buf[off..][0..4], key.generator_version, .little); - off += 4; - std.mem.writeInt(i32, buf[off..][0..4], key.rx, .little); - off += 4; - std.mem.writeInt(i32, buf[off..][0..4], key.rz, .little); - off += 4; - std.mem.writeInt(u32, buf[off..][0..4], data.width, .little); - off += 4; - - for (data.heightmap) |height| { - writeF32(buf[off..][0..4], height); - off += 4; - } - for (data.biomes) |biome| { - buf[off] = @intFromEnum(biome); - off += 1; - } - for (data.top_blocks) |block| { - writeBlock(buf[off..][0..1], block); - off += 1; - } - for (data.colors) |color| { - std.mem.writeInt(u32, buf[off..][0..4], color, .little); - off += 4; - } - for (data.material_layers) |layers| { - writeMaterialLayers(buf[off..][0..3], layers); - off += 3; - } - for (data.water) |water| { - writeWater(buf[off..][0..WATER_WIRE_SIZE], water); - off += WATER_WIRE_SIZE; - } - for (data.lighting) |lighting| { - writeLighting(buf[off..][0..LIGHTING_WIRE_SIZE], lighting); - off += LIGHTING_WIRE_SIZE; - } - for (data.vegetation) |vegetation| { - writeVegetation(buf[off..][0..VEGETATION_WIRE_SIZE], vegetation); - off += VEGETATION_WIRE_SIZE; - } - - const has_spans = data.hasVerticalSpans(); - buf[off] = if (has_spans) 1 else 0; - off += 1; - for (data.provenance) |provenance| { - buf[off] = @intFromEnum(provenance); - off += 1; - } - if (has_spans) { - const counts = data.vertical_span_counts.?; - const spans = data.vertical_spans.?; - @memcpy(buf[off..][0..count], counts); - off += count; - for (spans) |span| { - writeSpan(buf[off..][0..SPAN_WIRE_SIZE], span); - off += SPAN_WIRE_SIZE; - } - } - - std.debug.assert(off == total_size); - const crc = computeCrc(buf); - std.mem.writeInt(u32, buf[6..][0..4], crc, .little); - return buf; -} - -pub fn deserialize(bytes: []const u8, key: Key, allocator: std.mem.Allocator) !LODSimplifiedData { - if (bytes.len < HEADER_SIZE) return CacheError.DataTooShort; - - var off: usize = 0; - if (std.mem.readInt(u32, bytes[off..][0..4], .little) != MAGIC) return CacheError.InvalidMagic; - off += 4; - - const version = bytes[off]; - if (version != CACHE_VERSION and version != CACHE_VERSION_V10 and version != CACHE_VERSION_V1) return CacheError.UnsupportedVersion; - off += 1; - const lod_byte = bytes[off]; - off += 1; - if (lod_byte != @intFromEnum(key.lod)) return CacheError.InvalidKey; - - const stored_crc = std.mem.readInt(u32, bytes[off..][0..4], .little); - off += 4; - if (computeCrc(bytes) != stored_crc) return CacheError.ChecksumMismatch; - - const seed = std.mem.readInt(u64, bytes[off..][0..8], .little); - off += 8; - const generator_identity_hash = std.mem.readInt(u64, bytes[off..][0..8], .little); - off += 8; - const generator_version = std.mem.readInt(u32, bytes[off..][0..4], .little); - off += 4; - const rx = std.mem.readInt(i32, bytes[off..][0..4], .little); - off += 4; - const rz = std.mem.readInt(i32, bytes[off..][0..4], .little); - off += 4; - const width = std.mem.readInt(u32, bytes[off..][0..4], .little); - off += 4; - - if (seed != key.seed or generator_identity_hash != key.generator_identity_hash or generator_version != key.generator_version or rx != key.rx or rz != key.rz) return CacheError.InvalidKey; - if (!LODSimplifiedData.isSupportedGridSize(key.lod, width)) return CacheError.InvalidWidth; - - const count = @as(usize, @intCast(width)) * @as(usize, @intCast(width)); - const v1_expected = HEADER_SIZE + payloadSize(count); - if (bytes.len < v1_expected) return CacheError.DataTooShort; - - var has_spans = false; - var expected = v1_expected; - if (version == CACHE_VERSION or version == CACHE_VERSION_V10) { - if (bytes.len < v1_expected + SPAN_FLAGS_WIRE_SIZE) return CacheError.DataTooShort; - has_spans = bytes[v1_expected] != 0; - expected = v1_expected + SPAN_FLAGS_WIRE_SIZE; - if (version == CACHE_VERSION) expected += count; - if (has_spans) { - if (version == CACHE_VERSION_V10) expected += count; - expected += count + count * world_core.MAX_LOD_VERTICAL_SPANS * SPAN_WIRE_SIZE; - } - } - if (bytes.len < expected) return CacheError.DataTooShort; - - // Older span-less payloads never recorded source authority. Treat them as - // stale instead of silently converting edited/chunk-derived columns back - // to worldgen data; the cache pipeline will regenerate them safely. - if ((version == CACHE_VERSION_V1) or (version == CACHE_VERSION_V10 and !has_spans)) return CacheError.MissingProvenance; - - var data = if (has_spans) - try LODSimplifiedData.initWithVerticalSpansGridSize(allocator, key.lod, width) - else - try LODSimplifiedData.initWithGridSize(allocator, key.lod, width); - errdefer data.deinit(); - - for (data.heightmap) |*height| { - height.* = readF32(bytes[off..][0..4]); - off += 4; - } - for (data.biomes) |*biome| { - biome.* = try readBiome(bytes[off]); - off += 1; - } - for (data.top_blocks) |*block| { - block.* = try readBlock(bytes[off]); - off += 1; - } - for (data.colors) |*color| { - color.* = std.mem.readInt(u32, bytes[off..][0..4], .little); - off += 4; - } - for (data.material_layers) |*layers| { - layers.* = try readMaterialLayers(bytes[off..][0..3]); - off += 3; - } - for (data.water) |*water| { - water.* = readWater(bytes[off..][0..WATER_WIRE_SIZE]); - off += 13; - } - for (data.lighting) |*lighting| { - lighting.* = readLighting(bytes[off..][0..LIGHTING_WIRE_SIZE]); - off += 6; - } - for (data.vegetation) |*vegetation| { - vegetation.* = try readVegetation(bytes[off..][0..VEGETATION_WIRE_SIZE]); - off += 18; - } - - if (version == CACHE_VERSION or version == CACHE_VERSION_V10) { - const flags = bytes[off]; - off += 1; - if ((flags & ~@as(u8, 1)) != 0) return CacheError.InvalidSpanCount; - if (version == CACHE_VERSION or has_spans) { - for (data.provenance) |*provenance| { - const byte = bytes[off]; - provenance.* = switch (byte) { - 0 => .worldgen, - 1 => .chunk_derived, - 2 => .edited, - else => return CacheError.InvalidProvenance, - }; - off += 1; - } - } - if (has_spans) { - const counts = data.vertical_span_counts.?; - @memcpy(counts, bytes[off..][0..count]); - off += count; - for (counts) |span_count| { - if (span_count > world_core.MAX_LOD_VERTICAL_SPANS) return CacheError.InvalidSpanCount; - } - const spans = data.vertical_spans.?; - for (spans) |*span| { - span.* = try readSpan(bytes[off..][0..SPAN_WIRE_SIZE]); - off += SPAN_WIRE_SIZE; - } - } - } - - std.debug.assert(off == expected); - return data; -} - -const testing = std.testing; - -test "LOD cache round-trip preserves source data" { - var data = try LODSimplifiedData.init(testing.allocator, .lod2); - defer data.deinit(); - - data.setColumn(1, 2, 77.5, .forest, .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }, 0xFF336699, .{ - .is_surface = true, - .surface_height = 63.0, - .depth = 3.5, - .coverage = 0.75, - }, .{ - .sky_light = 12, - .block_light = 2, - .ambient_occlusion = 0.8, - }, .{ - .tree_coverage = 0.4, - .avg_tree_height = 8.0, - .offset_x = 0.2, - .offset_z = -0.3, - .trunk = .wood, - .leaves = .leaves, - }); - - const key = Key{ .seed = 1234, .generator_identity_hash = 99, .generator_version = 7, .rx = -2, .rz = 3, .lod = .lod2 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - - var decoded = try deserialize(bytes, key, testing.allocator); - defer decoded.deinit(); - - const idx = 1 + 2 * data.width; - try testing.expectEqual(data.heightmap[idx], decoded.heightmap[idx]); - try testing.expectEqual(data.biomes[idx], decoded.biomes[idx]); - try testing.expectEqual(data.top_blocks[idx], decoded.top_blocks[idx]); - try testing.expectEqual(data.colors[idx], decoded.colors[idx]); - try testing.expectEqual(data.material_layers[idx].subsurface, decoded.material_layers[idx].subsurface); - try testing.expectEqual(data.water[idx].depth, decoded.water[idx].depth); - try testing.expectEqual(data.lighting[idx].sky_light, decoded.lighting[idx].sky_light); - try testing.expectEqual(data.vegetation[idx].leaves, decoded.vegetation[idx].leaves); -} - -test "LOD cache round-trips reduced far density widths" { - inline for ([_]struct { lod: LODLevel, density: f32, width: u32 }{ - .{ .lod = .lod3, .density = 0.5, .width = 65 }, - .{ .lod = .lod4, .density = 0.25, .width = 17 }, - }) |case| { - var data = try LODSimplifiedData.initWithSampleDensity(testing.allocator, case.lod, case.density); - defer data.deinit(); - data.setHeight(0, 0, 83.0); - const key = Key{ .seed = 73, .generator_identity_hash = 9, .generator_version = 2, .rx = 1, .rz = -1, .lod = case.lod }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - var decoded = try deserialize(bytes, key, testing.allocator); - defer decoded.deinit(); - try testing.expectEqual(case.width, decoded.width); - try testing.expectEqual(@as(f32, 83.0), decoded.getHeight(0, 0)); - } -} - -test "LOD cache round-trip preserves vertical spans" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer data.deinit(); - - const lower = LODVerticalSpan{ - .min_height = 12.0, - .max_height = 48.0, - .biome = .forest, - .material_layers = .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, - .color = 0xFF112233, - .water = .empty, - .lighting = .{ .sky_light = 9, .block_light = 1, .ambient_occlusion = 0.7 }, - .vegetation = .{ .tree_coverage = 0.5, .avg_tree_height = 7.0, .offset_x = 0.25, .offset_z = -0.25, .trunk = .wood, .leaves = .leaves }, - }; - const upper = LODVerticalSpan{ - .min_height = 49.0, - .max_height = 92.0, - .biome = .mountains, - .material_layers = .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, - .color = 0xFF8899AA, - .water = .{ .is_surface = true, .surface_height = 64.0, .depth = 2.0, .coverage = 0.25 }, - .lighting = .daylight, - .vegetation = .empty, - }; - try testing.expect(data.setVerticalSpan(2, 3, 0, lower)); - try testing.expect(data.setVerticalSpan(2, 3, 1, upper)); - - const key = Key{ .seed = 1234, .generator_identity_hash = 99, .generator_version = 7, .rx = -2, .rz = 3, .lod = .lod1 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - - var decoded = try deserialize(bytes, key, testing.allocator); - defer decoded.deinit(); - - try testing.expect(decoded.hasVerticalSpans()); - try testing.expectEqual(data.vertical_span_counts.?.len, decoded.vertical_span_counts.?.len); - try testing.expectEqualSlices(u8, data.vertical_span_counts.?, decoded.vertical_span_counts.?); - try testing.expectEqual(lower, decoded.getVerticalSpan(2, 3, 0).?); - try testing.expectEqual(upper, decoded.getVerticalSpan(2, 3, 1).?); -} - -test "LOD cache round-trip preserves span-less column provenance" { - var data = try LODSimplifiedData.init(testing.allocator, .lod3); - defer data.deinit(); - - data.setColumnProvenance(2, 3, .chunk_derived); - data.setColumnProvenance(4, 5, .edited); - - const key = Key{ .seed = 1234, .generator_identity_hash = 99, .generator_version = 7, .rx = -2, .rz = 3, .lod = .lod3 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - - var decoded = try deserialize(bytes, key, testing.allocator); - defer decoded.deinit(); - - try testing.expectEqual(ColumnProvenance.chunk_derived, decoded.getColumnProvenance(2, 3)); - try testing.expectEqual(ColumnProvenance.edited, decoded.getColumnProvenance(4, 5)); - try testing.expectEqual(ColumnProvenance.worldgen, decoded.getColumnProvenance(0, 0)); -} - -test "LOD cache rejects legacy span-less payload without provenance" { - var data = try LODSimplifiedData.init(testing.allocator, .lod2); - defer data.deinit(); - data.setColumn(1, 1, 91.0, .mountains, .{ .surface = .stone, .subsurface = .dirt, .foundation = .stone }, 0xFF445566, .empty, .daylight, .empty); - - const key = Key{ .seed = 4, .generator_identity_hash = 5, .generator_version = 6, .rx = -1, .rz = -2, .lod = .lod2 }; - const v2_bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(v2_bytes); - const count = @as(usize, @intCast(data.width)) * @as(usize, @intCast(data.width)); - const v1_size = HEADER_SIZE + payloadSize(count); - const v1_bytes = try testing.allocator.dupe(u8, v2_bytes[0..v1_size]); - defer testing.allocator.free(v1_bytes); - v1_bytes[4] = CACHE_VERSION_V1; - std.mem.writeInt(u32, v1_bytes[6..][0..4], 0, .little); - std.mem.writeInt(u32, v1_bytes[6..][0..4], computeCrc(v1_bytes), .little); - - try testing.expectError(CacheError.MissingProvenance, deserialize(v1_bytes, key, testing.allocator)); -} - -test "LOD cache rejects v10 span-less payload without provenance" { - var data = try LODSimplifiedData.init(testing.allocator, .lod3); - defer data.deinit(); - - const key = Key{ .seed = 4, .generator_identity_hash = 5, .generator_version = 6, .rx = 1, .rz = -2, .lod = .lod3 }; - const current_bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(current_bytes); - const count = @as(usize, @intCast(data.width)) * @as(usize, @intCast(data.width)); - const legacy_size = current_bytes.len - count; - const legacy_bytes = try testing.allocator.alloc(u8, legacy_size); - defer testing.allocator.free(legacy_bytes); - const provenance_offset = HEADER_SIZE + payloadSize(count) + SPAN_FLAGS_WIRE_SIZE; - @memcpy(legacy_bytes[0..provenance_offset], current_bytes[0..provenance_offset]); - legacy_bytes[4] = CACHE_VERSION_V10; - std.mem.writeInt(u32, legacy_bytes[6..][0..4], 0, .little); - std.mem.writeInt(u32, legacy_bytes[6..][0..4], computeCrc(legacy_bytes), .little); - - try testing.expectError(CacheError.MissingProvenance, deserialize(legacy_bytes, key, testing.allocator)); -} - -test "LOD cache rejects checksum mismatch" { - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - - const key = Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod1 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - bytes[bytes.len - 1] ^= 0x01; - - try testing.expectError(CacheError.ChecksumMismatch, deserialize(bytes, key, testing.allocator)); -} - -test "LOD cache reports invalid provenance" { - var data = try LODSimplifiedData.init(testing.allocator, .lod2); - defer data.deinit(); - - const key = Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod2 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - const count = @as(usize, @intCast(data.width)) * @as(usize, @intCast(data.width)); - const provenance_offset = HEADER_SIZE + payloadSize(count) + SPAN_FLAGS_WIRE_SIZE; - bytes[provenance_offset] = 3; - std.mem.writeInt(u32, bytes[6..][0..4], 0, .little); - std.mem.writeInt(u32, bytes[6..][0..4], computeCrc(bytes), .little); - - try testing.expectError(CacheError.InvalidProvenance, deserialize(bytes, key, testing.allocator)); -} - -test "LOD cache rejects mismatched cache key" { - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - - const key = Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod1 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - - const wrong_key = Key{ .seed = 1, .generator_identity_hash = 3, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod1 }; - try testing.expectError(CacheError.InvalidKey, deserialize(bytes, wrong_key, testing.allocator)); -} - -test "LOD cache checksum covers key header fields" { - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - - const key = Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod1 }; - const bytes = try serialize(&data, key, testing.allocator); - defer testing.allocator.free(bytes); - - bytes[10] ^= 0x01; - try testing.expectError(CacheError.ChecksumMismatch, deserialize(bytes, key, testing.allocator)); -} - -test "LOD cache payload size follows named wire fields" { - try testing.expectEqual(@as(usize, 50), payloadSize(1)); - try testing.expectEqual(@as(usize, HEADER_SIZE + 50 * 4), HEADER_SIZE + payloadSize(4)); - - var data = try LODSimplifiedData.init(testing.allocator, .lod0); - defer data.deinit(); - const count = @as(usize, @intCast(data.width)) * @as(usize, @intCast(data.width)); - try testing.expectEqual(HEADER_SIZE + payloadSize(count) + SPAN_FLAGS_WIRE_SIZE + count, serializedSize(&data)); -} diff --git a/modules/world-lod/src/lod_cache_io.zig b/modules/world-lod/src/lod_cache_io.zig deleted file mode 100644 index 3e11a420..00000000 --- a/modules/world-lod/src/lod_cache_io.zig +++ /dev/null @@ -1,296 +0,0 @@ -//! Dedicated bounded cache I/O worker for LOD source data. -//! -//! This worker is deliberately separate from generation workers. The update -//! thread only snapshots/enqueues work and applies completions; all filesystem -//! access and cache serialization/deserialization happens on this one thread. - -const std = @import("std"); -const fs = @import("fs"); -const sync = @import("sync"); -const log = @import("engine-core").log; -const LODRegionKey = @import("lod_chunk.zig").LODRegionKey; -const LODSimplifiedData = @import("lod_chunk.zig").LODSimplifiedData; -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const LODProfilingCollector = @import("lod_stats.zig").LODProfilingCollector; - -pub const MAX_PENDING_TASKS: usize = 16; -const MAX_COMPLETIONS: usize = MAX_PENDING_TASKS + 1; - -pub const ReadResult = union(enum) { - hit: LODSimplifiedData, - miss: void, -}; - -pub const Completion = union(enum) { - read: struct { - key: LODRegionKey, - token: u32, - result: ReadResult, - used_legacy: bool, - }, - write: struct { - key: LODRegionKey, - revision: u32, - success: bool, - size_limited: bool, - store_size_cap_mb: u32, - }, -}; - -const Task = union(enum) { - read: struct { - path: []u8, - region_key: LODRegionKey, - cache_key: lod_cache.Key, - token: u32, - }, - write: struct { - path: []u8, - region_key: LODRegionKey, - cache_key: lod_cache.Key, - revision: u32, - data: LODSimplifiedData, - store_size_cap_mb: u32, - }, -}; - -/// A single worker and two bounded queues. Completion ownership transfers to -/// the update thread, which must deinit hit data after applying or rejecting it. -pub const CacheIoPipeline = struct { - allocator: std.mem.Allocator, - profiling: *LODProfilingCollector, - mutex: sync.Mutex = .{}, - work_ready: sync.Condition = .{}, - idle: sync.Condition = .{}, - tasks: std.ArrayListUnmanaged(Task) = .empty, - completions: std.ArrayListUnmanaged(Completion) = .empty, - stopping: bool = false, - active: bool = false, - thread: std.Thread, - - pub fn init(allocator: std.mem.Allocator, profiling: *LODProfilingCollector) !*CacheIoPipeline { - const result = try allocator.create(CacheIoPipeline); - errdefer allocator.destroy(result); - result.* = .{ - .allocator = allocator, - .profiling = profiling, - .thread = undefined, - }; - try result.tasks.ensureTotalCapacity(allocator, MAX_PENDING_TASKS); - errdefer result.tasks.deinit(allocator); - try result.completions.ensureTotalCapacity(allocator, MAX_COMPLETIONS); - errdefer result.completions.deinit(allocator); - result.thread = try std.Thread.spawn(.{}, workerMain, .{result}); - return result; - } - - pub fn enqueueRead(self: *CacheIoPipeline, path: []const u8, region_key: LODRegionKey, cache_key: lod_cache.Key, token: u32) !bool { - const path_copy = try self.allocator.dupe(u8, path); - errdefer self.allocator.free(path_copy); - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.stopping or self.tasks.items.len >= MAX_PENDING_TASKS) { - self.allocator.free(path_copy); - return false; - } - self.tasks.appendAssumeCapacity(.{ .read = .{ .path = path_copy, .region_key = region_key, .cache_key = cache_key, .token = token } }); - self.work_ready.signal(); - return true; - } - - /// Transfers ownership of `data` when accepted. Rejected snapshots remain - /// owned by the caller so it can release them or retry later. - pub fn enqueueWrite(self: *CacheIoPipeline, path: []const u8, region_key: LODRegionKey, cache_key: lod_cache.Key, revision: u32, data: LODSimplifiedData, store_size_cap_mb: u32) !bool { - const path_copy = try self.allocator.dupe(u8, path); - errdefer self.allocator.free(path_copy); - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.stopping or self.tasks.items.len >= MAX_PENDING_TASKS) { - self.allocator.free(path_copy); - return false; - } - self.tasks.appendAssumeCapacity(.{ .write = .{ - .path = path_copy, - .region_key = region_key, - .cache_key = cache_key, - .revision = revision, - .data = data, - .store_size_cap_mb = store_size_cap_mb, - } }); - self.work_ready.signal(); - return true; - } - - /// Moves all available completions into `out`. The caller becomes owner of - /// every read-hit payload in the returned list. - pub fn drainCompletions(self: *CacheIoPipeline, out: *std.ArrayListUnmanaged(Completion)) void { - self.mutex.lock(); - defer self.mutex.unlock(); - while (self.completions.items.len > 0) { - const completion = self.completions.items[0]; - out.append(self.allocator, completion) catch { - // Allocation failure cannot strand a read request. Keep the - // completion for a later update rather than dropping it. - return; - }; - _ = self.completions.orderedRemove(0); - } - } - - /// Explicit lifecycle operation only; never call from the frame path. - pub fn waitUntilIdle(self: *CacheIoPipeline) void { - self.mutex.lock(); - defer self.mutex.unlock(); - while (self.tasks.items.len != 0 or self.active) self.idle.wait(&self.mutex); - } - - /// Stops accepting work, drains accepted tasks, joins the sole worker, and - /// releases queued completion payloads. This may block by design. - pub fn deinit(self: *CacheIoPipeline) void { - self.mutex.lock(); - self.stopping = true; - self.work_ready.broadcast(); - self.mutex.unlock(); - self.thread.join(); - - for (self.tasks.items) |*task| deinitTask(self.allocator, task); - self.tasks.deinit(self.allocator); - for (self.completions.items) |*completion| deinitCompletion(completion); - self.completions.deinit(self.allocator); - self.allocator.destroy(self); - } - - fn workerMain(self: *CacheIoPipeline) void { - while (true) { - self.mutex.lock(); - while (self.tasks.items.len == 0 and !self.stopping) self.work_ready.wait(&self.mutex); - if (self.tasks.items.len == 0 and self.stopping) { - self.idle.broadcast(); - self.mutex.unlock(); - return; - } - const task = self.tasks.orderedRemove(0); - self.active = true; - self.mutex.unlock(); - - const completion = self.runTask(task); - - self.mutex.lock(); - // Capacity is guaranteed by the bounded work queue: at most one - // active task plus MAX_PENDING_TASKS accepted tasks can complete. - self.completions.appendAssumeCapacity(completion); - self.active = false; - if (self.tasks.items.len == 0) self.idle.broadcast(); - self.mutex.unlock(); - } - } - - fn runTask(self: *CacheIoPipeline, task: Task) Completion { - const timer = self.profiling.begin(); - defer self.profiling.end(.cache, timer); - return switch (task) { - .read => |read| blk: { - defer self.allocator.free(read.path); - const outcome = readData(self.allocator, read.path, read.cache_key); - break :blk .{ .read = .{ - .key = read.region_key, - .token = read.token, - .result = outcome.result, - .used_legacy = outcome.used_legacy, - } }; - }, - .write => |write| blk: { - defer self.allocator.free(write.path); - var data = write.data; - defer data.deinit(); - const bytes = lod_cache.serialize(&data, write.cache_key, self.allocator) catch |err| { - log.log.warn("Failed to serialize LOD{} cache ({}, {}): {}", .{ @intFromEnum(write.region_key.lod), write.region_key.rx, write.region_key.rz, err }); - break :blk .{ .write = .{ .key = write.region_key, .revision = write.revision, .success = false, .size_limited = false, .store_size_cap_mb = write.store_size_cap_mb } }; - }; - defer self.allocator.free(bytes); - var size_limited = false; - const success = blk_success: { - lod_store.writePayload(self.allocator, write.path, write.cache_key, bytes, write.store_size_cap_mb) catch |err| { - log.log.warn("Failed to write LOD{} store ({}, {}): {}", .{ @intFromEnum(write.region_key.lod), write.region_key.rx, write.region_key.rz, err }); - size_limited = err == lod_store.StoreError.StoreSizeLimit; - break :blk_success false; - }; - break :blk_success true; - }; - break :blk .{ .write = .{ .key = write.region_key, .revision = write.revision, .success = success, .size_limited = size_limited, .store_size_cap_mb = write.store_size_cap_mb } }; - }, - }; - } -}; - -const ReadDataOutcome = struct { - result: ReadResult, - used_legacy: bool = false, -}; - -fn readData(allocator: std.mem.Allocator, path: []const u8, key: lod_cache.Key) ReadDataOutcome { - const payload = lod_store.readPayload(allocator, path, key) catch |err| switch (err) { - lod_store.StoreError.CorruptContainer => { - const container_path = lod_store.containerPath(allocator, path, key) catch return .{ .result = .miss }; - defer allocator.free(container_path); - log.log.warn("Discarding corrupt LOD store container '{s}'", .{container_path}); - fs.cwd().deleteFile(container_path) catch |delete_err| { - if (delete_err != error.FileNotFound) log.log.warn("Failed to delete corrupt LOD store container '{s}': {}", .{ container_path, delete_err }); - }; - return .{ .result = .miss }; - }, - else => { - log.log.warn("Failed to read LOD store for LOD{} ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - return .{ .result = .miss }; - }, - }; - if (payload) |bytes| { - defer allocator.free(bytes); - const data = lod_cache.deserialize(bytes, key, allocator) catch |err| { - log.log.warn("Discarding corrupt LOD store payload LOD{} ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - lod_store.deletePayload(allocator, path, key); - return .{ .result = .miss }; - }; - return .{ .result = .{ .hit = data } }; - } - - const legacy_path = std.fmt.allocPrint(allocator, "{s}/lod_cache/lod_{}_{}_{}_{}_{}_{}.dat", .{ path, key.seed, key.generator_identity_hash, key.generator_version, key.rx, key.rz, @intFromEnum(key.lod) }) catch return .{ .result = .miss }; - defer allocator.free(legacy_path); - const bytes = fs.cwd().readFileAlloc(legacy_path, allocator, 16 * 1024 * 1024) catch |err| switch (err) { - error.FileNotFound => return .{ .result = .miss }, - else => { - log.log.warn("Failed to read legacy LOD cache '{s}': {}", .{ legacy_path, err }); - return .{ .result = .miss }; - }, - }; - defer allocator.free(bytes); - const data = lod_cache.deserialize(bytes, key, allocator) catch |err| { - log.log.warn("Discarding corrupt legacy LOD cache '{s}': {}", .{ legacy_path, err }); - fs.cwd().deleteFile(legacy_path) catch |delete_err| { - if (delete_err != error.FileNotFound) log.log.warn("Failed to delete corrupt legacy LOD cache '{s}': {}", .{ legacy_path, delete_err }); - }; - return .{ .result = .miss, .used_legacy = true }; - }; - return .{ .result = .{ .hit = data }, .used_legacy = true }; -} - -fn deinitTask(allocator: std.mem.Allocator, task: *Task) void { - switch (task.*) { - .read => |read| allocator.free(read.path), - .write => |*write| { - allocator.free(write.path); - write.data.deinit(); - }, - } -} - -fn deinitCompletion(completion: *Completion) void { - switch (completion.*) { - .read => |*read| switch (read.result) { - .hit => |*data| data.deinit(), - .miss => {}, - }, - .write => {}, - } -} diff --git a/modules/world-lod/src/lod_chunk.zig b/modules/world-lod/src/lod_chunk.zig deleted file mode 100644 index 3c8fc5ab..00000000 --- a/modules/world-lod/src/lod_chunk.zig +++ /dev/null @@ -1,1126 +0,0 @@ -//! LOD Chunk data structures for Distant Horizons-style rendering. -//! -//! LOD levels merge progressively larger chunk regions. Runtime settings choose -//! each level's grid detail and mesh path. - -const std = @import("std"); -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; -pub const LODMeshPath = @import("engine-rhi").LODMeshPath; - -/// LOD level enum - higher values = more simplified -pub const LODLevel = @import("lod_types.zig").LODLevel; -pub const regionSizeBlocks = world_core.regionSizeBlocks; -pub const TRANSITION_FADE_FRAMES: u8 = 30; - -/// State for LOD chunks/regions -pub const LODState = @import("lod_types.zig").LODState; - -/// Simplified data for distant LOD levels (LOD1+). -/// Only stores essential data needed for rendering distant terrain. -pub const LODSimplifiedData = world_core.LODSimplifiedData; - -/// LOD region key - identifies a region at a specific LOD level -pub const LODRegionKey = struct { - /// Region X coordinate (in region units, not chunks) - rx: i32, - /// Region Z coordinate - rz: i32, - /// LOD level - lod: LODLevel, - - /// Converts full-detail chunk coordinates to the containing LOD region key. - /// Uses floor division so negative world coordinates map to the correct parent region. - pub fn fromChunkCoords(chunk_x: i32, chunk_z: i32, lod: LODLevel) LODRegionKey { - const scale: i32 = @intCast(lod.chunksPerSide()); - return .{ - .rx = @divFloor(chunk_x, scale), - .rz = @divFloor(chunk_z, scale), - .lod = lod, - }; - } - - /// Hashes the region coordinates and LOD level for use in region maps. - /// The hash is deterministic and does not inspect mutable chunk state. - pub fn hash(self: LODRegionKey) u64 { - const ux: u64 = @bitCast(@as(i64, self.rx)); - const uz: u64 = @bitCast(@as(i64, self.rz)); - const ul: u64 = @intFromEnum(self.lod); - return ux ^ (uz *% 0x9e3779b97f4a7c15) ^ (ul *% 0x517cc1b727220a95); - } - - /// Compares two region keys by coordinates and LOD level. - /// Intended for hash-map key equality and has no side effects. - pub fn eql(a: LODRegionKey, b: LODRegionKey) bool { - return a.rx == b.rx and a.rz == b.rz and a.lod == b.lod; - } - - /// Returns the coarser parent region that spatially contains this region. - /// Returns `null` for the coarsest LOD because no parent level exists. - pub fn parentKey(self: LODRegionKey) ?LODRegionKey { - const lod_idx = @intFromEnum(self.lod); - if (lod_idx + 1 >= LODLevel.count) return null; - return .{ - .rx = @divFloor(self.rx, 2), - .rz = @divFloor(self.rz, 2), - .lod = @enumFromInt(@as(u3, @intCast(lod_idx + 1))), - }; - } - - /// Returns the four direct finer child regions covered by this region. - /// Returns `null` for LOD0 because it has no finer LOD children. - pub fn childKeys(self: LODRegionKey) ?[4]LODRegionKey { - const lod_idx = @intFromEnum(self.lod); - if (lod_idx == 0) return null; - const child_lod: LODLevel = @enumFromInt(@as(u3, @intCast(lod_idx - 1))); - const base_x = self.rx * 2; - const base_z = self.rz * 2; - return .{ - .{ .rx = base_x, .rz = base_z, .lod = child_lod }, - .{ .rx = base_x + 1, .rz = base_z, .lod = child_lod }, - .{ .rx = base_x, .rz = base_z + 1, .lod = child_lod }, - .{ .rx = base_x + 1, .rz = base_z + 1, .lod = child_lod }, - }; - } - - /// Get the chunk coordinates that this region covers - pub fn chunkBounds(self: LODRegionKey) ChunkBounds { - const scale: i32 = @intCast(self.lod.chunksPerSide()); - return .{ - .min_x = self.rx * scale, - .min_z = self.rz * scale, - .max_x = self.rx * scale + scale - 1, - .max_z = self.rz * scale + scale - 1, - }; - } -}; - -pub const ChunkBounds = struct { - min_x: i32, - min_z: i32, - max_x: i32, - max_z: i32, - - /// Computes squared distance from a chunk-coordinate point to this bounds rectangle. - /// Points inside the rectangle return zero; used to avoid square roots in range tests. - pub fn distanceSquaredToPoint(self: ChunkBounds, point_x: i32, point_z: i32) i64 { - const dx = axisDistance(point_x, self.min_x, self.max_x); - const dz = axisDistance(point_z, self.min_z, self.max_z); - const distance_sq = @as(i128, dx) * dx + @as(i128, dz) * dz; - return @intCast(@min(distance_sq, std.math.maxInt(i64))); - } - - /// Tests whether this chunk bounds intersects a radius around a chunk-coordinate center. - /// The radius is expressed in chunks and is evaluated radially, not as an axis-aligned square. - pub fn intersectsRadius(self: ChunkBounds, center_x: i32, center_z: i32, radius_chunks: i32) bool { - const radius_sq: i64 = @as(i64, radius_chunks) * @as(i64, radius_chunks); - return self.distanceSquaredToPoint(center_x, center_z) <= radius_sq; - } - - fn axisDistance(point: i32, min_value: i32, max_value: i32) i64 { - if (point < min_value) return @as(i64, min_value) - @as(i64, point); - if (point > max_value) return @as(i64, point) - @as(i64, max_value); - return 0; - } -}; - -/// Context for LODRegionKey HashMap -pub const LODRegionKeyContext = struct { - /// Hashes a region key for `std.HashMap` storage. - /// The context has no state; hashing delegates to the key's deterministic hash. - pub fn hash(self: @This(), key: LODRegionKey) u64 { - _ = self; - return key.hash(); - } - - /// Compares two region keys for `std.HashMap` lookup. - /// The context has no state and the comparison is side-effect-free. - pub fn eql(self: @This(), a: LODRegionKey, b: LODRegionKey) bool { - _ = self; - return a.eql(b); - } -}; - -/// LOD Chunk - represents terrain data at a specific LOD level -pub const LODChunk = struct { - /// Region position - region_x: i32, - region_z: i32, - - /// LOD level - lod_level: LODLevel, - - /// Current state - state: LODState, - - /// Job token for tracking async work - job_token: u32, - /// Encoded scheduling priority retained across cache lookup and lifecycle - /// transitions so bootstrap horizon seeds keep their spatial ordering. - job_priority: i32, - preserve_job_priority: bool, - - /// Pin count for preventing unload during async work - pin_count: std.atomic.Value(u32), - - /// Per-region cancellation signal for worker jobs invalidated by pause, - /// teleport, or horizon changes. Unlike the manager teardown flag, this - /// remains set until a new job is explicitly dispatched for the region. - cancel_requested: std.atomic.Value(bool), - - /// Chunk data - either full detail or simplified - data: union(enum) { - /// LOD0: Full chunk data (pointer to existing Chunk) - full: *Chunk, - /// LOD1+: Simplified heightmap-based data - simplified: LODSimplifiedData, - /// Not yet generated - empty: void, - }, - - /// Mesh handle (0 = no mesh) - mesh_handle: u32, - - /// Actual source-data height bounds in world block coordinates. - min_height: f32, - max_height: f32, - - /// Number of direct 2x2 finer child regions currently renderable. - ready_children: u8, - - /// Remaining render ticks for child fade-in or parent fade-out after a - /// hierarchy coverage transition. - transition_frames_remaining: u8, - - /// Dirty flag for re-meshing - dirty: bool, - - /// Source data changed since the last store flush (chunk-derived ingestion - /// or edit). The manager writes the region container to disk lazily. - store_dirty: bool, - - /// Monotonic source revision used to reject stale cache write completions. - source_revision: u32, - - /// Cache pipeline state. These flags are owned by the manager update - /// thread and prevent duplicate requests without pinning region memory. - cache_read_queued: bool, - store_write_queued: bool, - /// Suppresses retries for a snapshot that cannot fit the current store - /// cap. A source revision or cap increase makes it eligible again. - store_size_limited: bool, - store_size_limit_cap_mb: u32, - /// Sticky for the current source revision after a compact runtime - /// submission failure, preventing an auto/force rebuild loop on this - /// device session. Source changes clear it and may retry compact safely. - compact_disabled: bool, - - /// Creates an empty LOD region record in the missing state. - /// Source data, mesh handles, readiness counts, and transition state are initialized to safe defaults. - pub fn init(rx: i32, rz: i32, lod: LODLevel) LODChunk { - return .{ - .region_x = rx, - .region_z = rz, - .lod_level = lod, - .state = .missing, - .job_token = 0, - .job_priority = 0, - .preserve_job_priority = false, - .pin_count = std.atomic.Value(u32).init(0), - .cancel_requested = std.atomic.Value(bool).init(false), - .data = .{ .empty = {} }, - .mesh_handle = 0, - .min_height = 0.0, - .max_height = @floatFromInt(CHUNK_SIZE_Y), - .ready_children = 0, - .transition_frames_remaining = 0, - .dirty = false, - .store_dirty = false, - .source_revision = 0, - .cache_read_queued = false, - .store_write_queued = false, - .store_size_limited = false, - .store_size_limit_cap_mb = 0, - .compact_disabled = false, - }; - } - - /// Releases heap-owned simplified data and mesh references held by the LOD chunk. - /// Pinned chunks must not be deinitialized while worker jobs still reference them. - pub fn deinit(self: *LODChunk, allocator: std.mem.Allocator) void { - _ = allocator; - switch (self.data) { - .simplified => |*s| s.deinit(), - .full => {}, // Full chunks are managed elsewhere - .empty => {}, - } - self.* = undefined; - } - - /// Increments the async-work pin count for this LOD chunk. - /// Managers must not unload or deinitialize pinned chunks while worker jobs may still reference them. - pub fn pin(self: *LODChunk) void { - _ = self.pin_count.fetchAdd(1, .monotonic); - } - - /// Decrements the async-work pin count for this LOD chunk. - /// Must be paired with a previous `pin`; callers are responsible for avoiding underflow. - pub fn unpin(self: *LODChunk) void { - _ = self.pin_count.fetchSub(1, .monotonic); - } - - /// Reports whether any async job currently pins this LOD chunk. - /// This is an atomic read suitable for eviction checks. - pub fn isPinned(self: *const LODChunk) bool { - return self.pin_count.load(.monotonic) > 0; - } - - pub fn requestCancellation(self: *LODChunk) void { - self.cancel_requested.store(true, .release); - } - - pub fn resetCancellation(self: *LODChunk) void { - self.cancel_requested.store(false, .release); - } - - pub fn cancellationRequested(self: *const LODChunk) bool { - return self.cancel_requested.load(.acquire); - } - - /// Returns the immutable map key corresponding to this chunk's region coordinates and LOD level. - /// The key can be used to look up the same chunk in manager maps. - pub fn key(self: *const LODChunk) LODRegionKey { - return .{ .rx = self.region_x, .rz = self.region_z, .lod = self.lod_level }; - } - - /// Returns the current lifecycle state of this LOD chunk. - /// The value drives scheduler decisions such as generation, meshing, upload, and rendering. - pub fn getState(self: *const LODChunk) LODState { - return self.state; - } - - /// Sets the lifecycle state used by LOD scheduling and rendering. - /// Call from the manager/update thread that owns the chunk maps. - pub fn setState(self: *LODChunk, state: LODState) void { - self.state = state; - } - - /// Reports whether generation, meshing, or upload work is currently in flight for this chunk. - /// Eviction code should keep in-flight chunks until work completes or is cancelled. - pub fn isInFlight(self: *const LODChunk) bool { - return self.state == .generating or self.state == .meshing or self.state == .uploading; - } - - /// Reports whether this chunk has uploaded mesh data ready for rendering. - /// This is a state query only; it does not validate the mesh handle. - pub fn isRenderable(self: *const LODChunk) bool { - return self.state == .renderable; - } - - /// Returns the LOD level represented by this region. - /// Higher levels cover larger areas with more simplified geometry. - pub fn lodLevel(self: *const LODChunk) LODLevel { - return self.lod_level; - } - - /// Returns how many direct finer children are currently renderable. - /// Parent fallback rendering uses this count to decide coverage and fade behavior. - pub fn readyChildren(self: *const LODChunk) u8 { - return self.ready_children; - } - - /// Returns the normalized fade progress for parent/child LOD hierarchy transitions. - /// Child regions fade in while fully covered parents fade out according to remaining transition frames. - pub fn transitionFadeProgress(self: *const LODChunk) f32 { - if (self.transition_frames_remaining == 0) return 1.0; - const remaining = @as(f32, @floatFromInt(self.transition_frames_remaining)); - const total = @as(f32, @floatFromInt(TRANSITION_FADE_FRAMES)); - const t = @min(remaining / total, 1.0); - if (self.lod_level != .lod1 and self.ready_children >= 4) return t; - return 1.0 - t; - } - - /// Adjusts the count of renderable direct child regions, clamped to `[0, 4]`. - /// Crossing into full child coverage starts a transition fade; losing coverage cancels it. - pub fn adjustReadyChildren(self: *LODChunk, delta: i8) void { - const before = self.ready_children; - if (delta > 0) { - self.ready_children = @min(self.ready_children + @as(u8, @intCast(delta)), 4); - } else if (delta < 0) { - const amount: u8 = @intCast(-delta); - self.ready_children = if (amount >= self.ready_children) 0 else self.ready_children - amount; - } - if (before < 4 and self.ready_children >= 4) { - self.transition_frames_remaining = TRANSITION_FADE_FRAMES; - } else if (self.ready_children < 4) { - self.transition_frames_remaining = 0; - } - } - - /// Marks this chunk renderable after mesh upload completes. - /// Sets child readiness, starts a transition fade, and moves lifecycle state to `.renderable`. - pub fn markRenderable(self: *LODChunk, ready_children: u8) void { - self.ready_children = @min(ready_children, 4); - self.transition_frames_remaining = TRANSITION_FADE_FRAMES; - self.state = .renderable; - } - - /// Advances transition fading by one render/update tick. - /// Does nothing once the transition counter has reached zero. - pub fn tickTransition(self: *LODChunk) void { - if (self.transition_frames_remaining > 0) self.transition_frames_remaining -= 1; - } - - /// Reports whether all four direct finer children cover this region. - /// Partial child coverage must never hide the parent: quality thresholds - /// may tune transitions, but cannot create a terrain hole. - pub fn isCoveredByFinerLOD(self: *const LODChunk, fallback_missing_child_threshold: f32) bool { - _ = fallback_missing_child_threshold; - if (self.lod_level == .lod0) return false; - return self.ready_children >= 4 and self.transition_frames_remaining == 0; - } - - /// Marks source data dirty after chunk-derived ingestion or edits. - /// Also marks persistent cache storage dirty so the manager can flush updated source data. - pub fn markSourceDirty(self: *LODChunk) void { - self.dirty = true; - self.store_dirty = true; - self.store_size_limited = false; - self.bumpSourceRevision(); - } - - /// Advances the immutable source identity. A fresh source revision is - /// allowed to retry compact rendering after a device-local failure. - pub fn bumpSourceRevision(self: *LODChunk) void { - self.source_revision +%= 1; - self.compact_disabled = false; - } - - /// Sets the ready-child count directly, clamped to four direct children. - /// Used when reconstructing or synchronizing hierarchy state from manager bookkeeping. - pub fn setReadyChildren(self: *LODChunk, ready_children: u8) void { - self.ready_children = @min(ready_children, 4); - } - - /// World-space bounds structure for LOD regions - pub const WorldBounds = struct { - min_x: i32, - min_z: i32, - max_x: i32, - max_z: i32, - min_y: f32, - max_y: f32, - }; - - /// Get the world-space bounds of this LOD region - pub fn worldBounds(self: *const LODChunk) WorldBounds { - const scale: i32 = @intCast(self.lod_level.chunksPerSide()); - const size: i32 = scale * CHUNK_SIZE_X; - return .{ - .min_x = self.region_x * size, - .min_z = self.region_z * size, - .max_x = self.region_x * size + size, - .max_z = self.region_z * size + size, - .min_y = self.min_height, - .max_y = self.max_height, - }; - } - - /// Recomputes world-space min/max height bounds from simplified source data. - /// Full or empty chunks leave existing bounds unchanged; call after replacing simplified data. - pub fn updateHeightBoundsFromData(self: *LODChunk) void { - switch (self.data) { - .simplified => |*data| { - var min_height: f32 = std.math.floatMax(f32); - var max_height: f32 = -std.math.floatMax(f32); - if (data.hasVerticalSpans()) { - const counts = data.vertical_span_counts.?; - const spans = data.vertical_spans.?; - for (counts, 0..) |span_count, column_idx| { - var span_idx: usize = 0; - while (span_idx < span_count) : (span_idx += 1) { - const span = spans[column_idx * world_core.MAX_LOD_VERTICAL_SPANS + span_idx]; - min_height = @min(min_height, span.min_height); - max_height = @max(max_height, span.max_height); - } - } - } - for (data.heightmap) |height| { - min_height = @min(min_height, height); - max_height = @max(max_height, height); - } - if (min_height <= max_height) { - self.min_height = min_height; - self.max_height = max_height; - } - }, - else => {}, - } - } - - /// Returns the full-detail chunk-coordinate bounds covered by this LOD region. - /// Bounds are inclusive and derived from region coordinates plus chunks-per-side for the LOD level. - pub fn chunkBounds(self: *const LODChunk) ChunkBounds { - return .{ - .min_x = self.region_x * @as(i32, @intCast(self.lod_level.chunksPerSide())), - .min_z = self.region_z * @as(i32, @intCast(self.lod_level.chunksPerSide())), - .max_x = (self.region_x + 1) * @as(i32, @intCast(self.lod_level.chunksPerSide())) - 1, - .max_z = (self.region_z + 1) * @as(i32, @intCast(self.lod_level.chunksPerSide())) - 1, - }; - } -}; - -/// Configuration interface for LOD system to decouple settings from logic. -pub const ILODConfig = struct { - ptr: *anyopaque, - vtable: *const VTable, - - pub const VTable = struct { - getRadii: *const fn (ptr: *anyopaque) [LODLevel.count]i32, - getChunkRenderRadius: *const fn (ptr: *anyopaque) i32, - getActiveLODCount: *const fn (ptr: *anyopaque) u32, - setActiveLODCount: *const fn (ptr: *anyopaque, count: u32) void, - setChunkRenderRadius: *const fn (ptr: *anyopaque, radius: i32) void, - setLOD0Radius: *const fn (ptr: *anyopaque, radius: i32) void, - setRadii: *const fn (ptr: *anyopaque, radii: [LODLevel.count]i32) void, - getLODForDistance: *const fn (ptr: *anyopaque, dist_chunks: i32) LODLevel, - isInRange: *const fn (ptr: *anyopaque, dist_chunks: i32) bool, - getMaxUploadsPerFrame: *const fn (ptr: *anyopaque) u32, - calculateMaskRadius: *const fn (ptr: *anyopaque) f32, - getQEMTarget: *const fn (ptr: *anyopaque, lod: LODLevel) u32, - getQEMMinInputTriangles: *const fn (ptr: *anyopaque) u32, - getHorizontalDetail: *const fn (ptr: *anyopaque, lod: LODLevel) u32, - getSampleDensity: *const fn (ptr: *anyopaque, lod: LODLevel) f32, - getCompactTilesEnabled: *const fn (ptr: *anyopaque) bool, - getVerticalSpanBudget: *const fn (ptr: *anyopaque) u8, - getMeshPath: *const fn (ptr: *anyopaque) LODMeshPath, - getFogStartPercent: *const fn (ptr: *anyopaque, lod: LODLevel) f32, - getFallbackMissingChildThreshold: *const fn (ptr: *anyopaque) f32, - getMemoryBudgetMB: *const fn (ptr: *anyopaque) u32, - getLODStoreSizeCapMB: *const fn (ptr: *anyopaque) u32, - }; - - /// Returns configured outer radii for every LOD level in chunks. - /// Callers should use `getActiveLODCount` to know how many entries are currently active. - pub fn getRadii(self: ILODConfig) [LODLevel.count]i32 { - return self.vtable.getRadii(self.ptr); - } - /// Returns the high-detail chunk render radius that LOD terrain must avoid overlapping. - /// Shader masking and eviction use this to preserve the full-detail chunk ring. - pub fn getChunkRenderRadius(self: ILODConfig) i32 { - return self.vtable.getChunkRenderRadius(self.ptr); - } - /// Returns the number of LOD levels currently enabled by configuration. - /// Implementations clamp the value to at least one and at most `LODLevel.count`. - pub fn getActiveLODCount(self: ILODConfig) u32 { - return self.vtable.getActiveLODCount(self.ptr); - } - /// Sets how many LOD levels should participate in scheduling and rendering. - /// Implementations should clamp out-of-range values to the supported LOD count. - pub fn setActiveLODCount(self: ILODConfig, count: u32) void { - self.vtable.setActiveLODCount(self.ptr, count); - } - /// Sets the full-detail chunk render radius used by LOD masking and radius expansion. - /// Implementations should clamp invalid radii to a positive value. - pub fn setChunkRenderRadius(self: ILODConfig, radius: i32) void { - self.vtable.setChunkRenderRadius(self.ptr, radius); - } - /// Sets the outer radius for the LOD0 ring. - /// This affects near-distance LOD scheduling without changing the full-detail chunk radius. - pub fn setLOD0Radius(self: ILODConfig, radius: i32) void { - self.vtable.setLOD0Radius(self.ptr, radius); - } - /// Replaces all configured LOD outer radii at once. - /// Callers must provide radii in increasing order if they want monotonic distance selection. - pub fn setRadii(self: ILODConfig, radii: [LODLevel.count]i32) void { - self.vtable.setRadii(self.ptr, radii); - } - /// Selects the LOD level for a distance from the player measured in chunks. - /// Distances beyond active radii resolve to the coarsest active LOD. - pub fn getLODForDistance(self: ILODConfig, dist_chunks: i32) LODLevel { - return self.vtable.getLODForDistance(self.ptr, dist_chunks); - } - /// Reports whether a chunk-distance lies inside the active LOD horizon. - /// Used before scheduling or retaining distant LOD regions. - pub fn isInRange(self: ILODConfig, dist_chunks: i32) bool { - return self.vtable.isInRange(self.ptr, dist_chunks); - } - /// Returns the maximum number of LOD uploads the manager should attempt per frame. - /// This is a scheduler throttle, separate from byte-budget throttling. - pub fn getMaxUploadsPerFrame(self: ILODConfig) u32 { - return self.vtable.getMaxUploadsPerFrame(self.ptr); - } - - /// Calculate the masking radius used by shaders to discard LOD pixels overlapping with high-detail chunks. - /// This is a pure function based on config state, extracted for testability. - pub fn calculateMaskRadius(self: ILODConfig) f32 { - return self.vtable.calculateMaskRadius(self.ptr); - } - - /// Returns the target triangle budget for QEM simplification at one LOD level. - /// A value of zero means that level should not use QEM reduction. - pub fn getQEMTarget(self: ILODConfig, lod: LODLevel) u32 { - return self.vtable.getQEMTarget(self.ptr, lod); - } - - /// Returns the minimum input triangle count required before QEM simplification is attempted. - /// Smaller meshes use the cheaper non-QEM path to avoid unnecessary work. - pub fn getQEMMinInputTriangles(self: ILODConfig) u32 { - return self.vtable.getQEMMinInputTriangles(self.ptr); - } - - /// Returns the horizontal sample resolution used to generate a mesh for one LOD level. - /// Higher values preserve more terrain detail at higher CPU/GPU cost. - pub fn getHorizontalDetail(self: ILODConfig, lod: LODLevel) u32 { - return self.vtable.getHorizontalDetail(self.ptr, lod); - } - /// Returns the source-sample density used before meshing. Far LODs may - /// reduce this independently of their render radius. - pub fn getSampleDensity(self: ILODConfig, lod: LODLevel) f32 { - return self.vtable.getSampleDensity(self.ptr, lod); - } - /// Returns whether far LODs may use compact GPU tiles. Callers can disable - /// them for retained-overlay scenes that require the expanded fallback. - pub fn getCompactTilesEnabled(self: ILODConfig) bool { - return self.vtable.getCompactTilesEnabled(self.ptr); - } - - /// Returns the maximum number of vertical spans retained per LOD column. - /// Implementations clamp this to the storage capacity of `LODSimplifiedData`. - pub fn getVerticalSpanBudget(self: ILODConfig) u8 { - return self.vtable.getVerticalSpanBudget(self.ptr); - } - - /// Returns which mesh-generation algorithm the LOD manager should use. - /// The value selects between column-span, QEM, or other backend-supported paths. - pub fn getMeshPath(self: ILODConfig) LODMeshPath { - return self.vtable.getMeshPath(self.ptr); - } - - /// Returns where fog should begin within an LOD level's radius, as a fraction in `[0, 1]`. - /// Renderers use this to hide distant LOD transitions and horizon cutoffs. - pub fn getFogStartPercent(self: ILODConfig, lod: LODLevel) f32 { - return self.vtable.getFogStartPercent(self.ptr, lod); - } - - /// Returns the fraction of missing finer children allowed before a parent fallback remains visible. - /// Values are clamped by concrete implementations to `[0, 1]`. - pub fn getFallbackMissingChildThreshold(self: ILODConfig) f32 { - return self.vtable.getFallbackMissingChildThreshold(self.ptr); - } - - /// Returns the approximate memory budget for LOD meshes and source data in MiB. - /// The manager uses this to decide when to shrink radii or evict regions. - pub fn getMemoryBudgetMB(self: ILODConfig) u32 { - return self.vtable.getMemoryBudgetMB(self.ptr); - } - - /// Returns the persistent LOD cache store size cap in MiB. - /// Cache maintenance uses this to bound disk usage for source-data containers. - pub fn getLODStoreSizeCapMB(self: ILODConfig) u32 { - return self.vtable.getLODStoreSizeCapMB(self.ptr); - } -}; - -/// Returns the active LOD count as a `usize`, clamped to the supported range. -/// Use this when indexing fixed arrays whose length is `LODLevel.count`. -pub fn activeLODCount(config: ILODConfig) usize { - return @intCast(std.math.clamp(config.getActiveLODCount(), 1, LODLevel.count)); -} - -/// Concrete implementation of LOD system configuration. -pub const LODConfig = struct { - pub const default_chunk_render_radius: i32 = 16; - pub const default_horizon_radius: i32 = 512; - pub const minimum_horizon_radius: i32 = 256; - /// Production user setting limit. Larger horizons remain available only to - /// explicit benchmark/diagnostic configurations until additional coarse - /// levels remove the current logical-memory and compact-pool pressure. - pub const maximum_user_horizon_radius: i32 = 512; - pub const target_lod1_radius: i32 = 96; // keep 2-block cells visible farther out. - pub const target_lod2_radius: i32 = 256; - pub const target_lod3_radius: i32 = 512; - /// Keep enough horizon beyond full detail for the complete LOD ladder to - /// remain useful as users raise render distance. - pub const horizon_render_distance_scale: i64 = 32; - - /// Radius of real full-detail chunks. LOD0 is a separate 1-block-column - /// LOD ring that extends beyond this radius. - chunk_render_radius: i32 = default_chunk_render_radius, - - radii: [LODLevel.count]i32 = .{ 32, target_lod1_radius, target_lod2_radius, target_lod3_radius, default_horizon_radius }, - - memory_budget_mb: u32 = 256, - - lod_store_size_cap_mb: u32 = @import("lod_store.zig").DEFAULT_STORE_SIZE_CAP_MB, - - max_uploads_per_frame: u32 = 32, - - fog_transitions: bool = true, - - /// Fog start position as percentage of LOD radius (0.0-1.0) where fog begins. - /// Values closer to 0.0 start fog near the player; 1.0 disables fog for that level. - fog_start_percent: [LODLevel.count]f32 = .{ 0.55, 0.48, 0.38, 0.28, 0.22 }, - - horizontal_detail: [LODLevel.count]u32 = .{ 33, 65, 65, 129, 129 }, - - /// LOD3 reduces worker input where four-block cells remain visually stable. - /// Keep the outer horizon at its full 65-sample grid: reducing it to 17 - /// samples creates 32-block plateaus and visibly detached height seams. - sample_density: [LODLevel.count]f32 = .{ 1.0, 1.0, 1.0, 0.5, 1.0 }, - - compact_tiles_enabled: bool = true, - - vertical_span_budget: u8 = 4, - - mesh_path: LODMeshPath = .column_spans, - - qem_triangle_targets: [LODLevel.count]u32 = .{ 0, 2000, 800, 200, 64 }, - - qem_min_input_triangles: u32 = 50, - - skip_cutout_lod2: bool = false, - - skip_lighting_lod3: bool = false, - - // The coarsest level remains active even when it shares the horizon radius - // with its child: it is the fast, large-area fallback while that child loads. - active_lod_count: u32 = LODLevel.count, - - /// Maximum fraction of direct finer child regions that may be missing before - /// a coarser parent must remain visible as fallback terrain. - fallback_missing_child_threshold: f32 = 0.2, - - /// Returns the configured QEM triangle target for one LOD level. - /// Used by mesh builders to choose simplification aggressiveness. - pub fn getQEMTarget(self: *const LODConfig, lod: LODLevel) u32 { - return self.qem_triangle_targets[@intFromEnum(lod)]; - } - - /// Expands a full-detail render distance into the default LOD radius ladder. - /// Ordinary settings use the qualified user horizon; benchmarks and - /// diagnostics request larger ladders explicitly through radiiForDistances. - pub fn radiiForRenderDistance(distance: i32) [LODLevel.count]i32 { - return radiiForDistances(distance, normalizeUserHorizonDistance(distance, default_horizon_radius)); - } - - /// Returns the minimum useful distant-LOD horizon for a full-detail radius. - /// Arithmetic saturates at the coordinate representation limit rather than - /// introducing an arbitrary settings cap. - pub fn recommendedHorizonDistance(distance: i32) i32 { - const requested = @as(i64, @max(distance, 1)); - const scaled = @min(requested * horizon_render_distance_scale, @as(i64, std.math.maxInt(i32))); - return @intCast(@max(@as(i64, minimum_horizon_radius), scaled)); - } - - /// Normalizes the explicit outer LOD limit without silently expanding it - /// to the recommended long-distance profile. - pub fn normalizeHorizonDistance(render_distance: i32, horizon_distance: i32) i32 { - return @max(horizon_distance, @max(render_distance, minimum_horizon_radius)); - } - - /// Clamps the normal user-facing distant terrain control to the currently - /// qualified production range. Benchmark configs use the uncapped helper. - pub fn normalizeUserHorizonDistance(render_distance: i32, horizon_distance: i32) i32 { - return @min(normalizeHorizonDistance(render_distance, horizon_distance), @max(render_distance, maximum_user_horizon_radius)); - } - - /// Steps the explicit outer LOD limit geometrically. The recommendation is - /// a preset default, not a mandatory minimum, so users can trade reach for - /// contiguous fill and lower generation pressure. - pub fn stepHorizonDistance(render_distance: i32, horizon_distance: i32, increase: bool) i32 { - const minimum = @max(render_distance, minimum_horizon_radius); - const maximum = @max(render_distance, maximum_user_horizon_radius); - if (horizon_distance > maximum) return maximum; - const current = std.math.clamp(horizon_distance, minimum, maximum); - if (!increase) return @max(minimum, @divFloor(current, 2)); - return @intCast(@min(@as(i64, current) * 2, @as(i64, maximum))); - } - - /// Expands full-detail and horizon distances into monotonically increasing LOD radii. - /// Radii are expressed in chunks and are clamped so they do not exceed the horizon. - pub fn radiiForDistances(distance: i32, horizon_distance: i32) [LODLevel.count]i32 { - const requested = @max(distance, 1); - const lod0_target = @max(@as(i64, requested) * 3, @as(i64, requested) + 16); - const lod0 = @as(i32, @intCast(@min(lod0_target, @as(i64, @max(horizon_distance, requested))))); - const horizon = @max(horizon_distance, lod0); - const max_radius_i64 = @as(i64, horizon); - const lod1_target = @max(@as(i64, lod0) * 2, @as(i64, target_lod1_radius)); - const lod1 = @as(i32, @intCast(@min(lod1_target, max_radius_i64))); - const lod2_target = @max(@as(i64, lod1) * 2, @as(i64, target_lod2_radius)); - const lod2 = @as(i32, @intCast(@min(lod2_target, max_radius_i64))); - const lod3_target = @max(@as(i64, lod2) * 2, @as(i64, target_lod3_radius)); - const lod3 = @as(i32, @intCast(@min(lod3_target, max_radius_i64))); - return .{ lod0, lod1, lod2, lod3, horizon }; - } - - /// Returns how many LOD levels should be active for a render-distance setting. - pub fn activeCountForRenderDistance(distance: i32) u32 { - _ = distance; - return LODLevel.count; - } - - /// Returns the coarsest supported LOD level. - /// Use as a fallback when a distance exceeds all configured active radii. - pub fn coarsestLOD() LODLevel { - return @enumFromInt(@as(u3, @intCast(LODLevel.count - 1))); - } - - /// Selects the concrete LOD level for a chunk distance using this config's active radii. - /// Distances beyond active radii resolve to the coarsest active LOD. - pub fn getLODForDistance(self: *const LODConfig, dist_chunks: i32) LODLevel { - const active_lod_count = activeLODCount(self.interfaceConst()); - for (0..active_lod_count) |i| { - if (dist_chunks <= self.radii[i]) return @enumFromInt(@as(u3, @intCast(i))); - } - return @enumFromInt(@as(u3, @intCast(active_lod_count - 1))); - } - - /// Reports whether a chunk distance lies inside this config's active LOD horizon. - /// The horizon is the outer radius of the coarsest active LOD. - pub fn isInRange(self: *const LODConfig, dist_chunks: i32) bool { - return dist_chunks <= self.radii[activeLODCount(self.interfaceConst()) - 1]; - } - - /// Returns the interface for this concrete config. - pub fn interface(self: *LODConfig) ILODConfig { - return .{ - .ptr = self, - .vtable = &VTABLE, - }; - } - - fn interfaceConst(self: *const LODConfig) ILODConfig { - return .{ - .ptr = @constCast(self), - .vtable = &VTABLE, - }; - } - - const VTABLE = ILODConfig.VTable{ - .getRadii = getRadiiWrapper, - .getChunkRenderRadius = getChunkRenderRadiusWrapper, - .getActiveLODCount = getActiveLODCountWrapper, - .setActiveLODCount = setActiveLODCountWrapper, - .setChunkRenderRadius = setChunkRenderRadiusWrapper, - .setLOD0Radius = setLOD0RadiusWrapper, - .setRadii = setRadiiWrapper, - .getLODForDistance = getLODForDistanceWrapper, - .isInRange = isInRangeWrapper, - .getMaxUploadsPerFrame = getMaxUploadsPerFrameWrapper, - .calculateMaskRadius = calculateMaskRadiusWrapper, - .getQEMTarget = getQEMTargetWrapper, - .getQEMMinInputTriangles = getQEMMinInputTrianglesWrapper, - .getHorizontalDetail = getHorizontalDetailWrapper, - .getSampleDensity = getSampleDensityWrapper, - .getCompactTilesEnabled = getCompactTilesEnabledWrapper, - .getVerticalSpanBudget = getVerticalSpanBudgetWrapper, - .getMeshPath = getMeshPathWrapper, - .getFogStartPercent = getFogStartPercentWrapper, - .getFallbackMissingChildThreshold = getFallbackMissingChildThresholdWrapper, - .getMemoryBudgetMB = getMemoryBudgetMBWrapper, - .getLODStoreSizeCapMB = getLODStoreSizeCapMBWrapper, - }; - - fn getRadiiWrapper(ptr: *anyopaque) [LODLevel.count]i32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.radii; - } - fn getChunkRenderRadiusWrapper(ptr: *anyopaque) i32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.chunk_render_radius; - } - fn getActiveLODCountWrapper(ptr: *anyopaque) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return std.math.clamp(self.active_lod_count, 1, LODLevel.count); - } - fn setActiveLODCountWrapper(ptr: *anyopaque, count: u32) void { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - self.active_lod_count = std.math.clamp(count, 1, LODLevel.count); - } - fn setChunkRenderRadiusWrapper(ptr: *anyopaque, radius: i32) void { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - self.chunk_render_radius = @max(radius, 1); - } - fn setLOD0RadiusWrapper(ptr: *anyopaque, radius: i32) void { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - self.radii[0] = radius; - } - fn setRadiiWrapper(ptr: *anyopaque, radii: [LODLevel.count]i32) void { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - self.radii = radii; - } - fn getLODForDistanceWrapper(ptr: *anyopaque, dist_chunks: i32) LODLevel { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.getLODForDistance(dist_chunks); - } - fn isInRangeWrapper(ptr: *anyopaque, dist_chunks: i32) bool { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.isInRange(dist_chunks); - } - fn getMaxUploadsPerFrameWrapper(ptr: *anyopaque) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.max_uploads_per_frame; - } - fn calculateMaskRadiusWrapper(ptr: *anyopaque) f32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - // Keep a small overlap so the chunk ring and LOD ring blend instead of - // leaving a camera-centered dead zone between them. - const overlap_chunks = @max(self.chunk_render_radius - 2, 0); - return @as(f32, @floatFromInt(overlap_chunks)) * @as(f32, @floatFromInt(CHUNK_SIZE_X)); - } - fn getQEMTargetWrapper(ptr: *anyopaque, lod: LODLevel) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.getQEMTarget(lod); - } - fn getQEMMinInputTrianglesWrapper(ptr: *anyopaque) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.qem_min_input_triangles; - } - fn getHorizontalDetailWrapper(ptr: *anyopaque, lod: LODLevel) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.horizontal_detail[@intFromEnum(lod)]; - } - fn getSampleDensityWrapper(ptr: *anyopaque, lod: LODLevel) f32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return std.math.clamp(self.sample_density[@intFromEnum(lod)], 0.0625, 1.0); - } - fn getCompactTilesEnabledWrapper(ptr: *anyopaque) bool { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.compact_tiles_enabled; - } - fn getVerticalSpanBudgetWrapper(ptr: *anyopaque) u8 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return @min(self.vertical_span_budget, @as(u8, @intCast(world_core.MAX_LOD_VERTICAL_SPANS))); - } - fn getMeshPathWrapper(ptr: *anyopaque) LODMeshPath { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.mesh_path; - } - fn getFogStartPercentWrapper(ptr: *anyopaque, lod: LODLevel) f32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.fog_start_percent[@intFromEnum(lod)]; - } - fn getFallbackMissingChildThresholdWrapper(ptr: *anyopaque) f32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return std.math.clamp(self.fallback_missing_child_threshold, 0.0, 1.0); - } - fn getMemoryBudgetMBWrapper(ptr: *anyopaque) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.memory_budget_mb; - } - fn getLODStoreSizeCapMBWrapper(ptr: *anyopaque) u32 { - const self: *LODConfig = @ptrCast(@alignCast(ptr)); - return self.lod_store_size_cap_mb; - } -}; - -// Tests -test "LODLevel scale calculations" { - try std.testing.expectEqual(@as(u32, 1), LODLevel.lod0.scale()); - try std.testing.expectEqual(@as(u32, 2), LODLevel.lod1.scale()); - try std.testing.expectEqual(@as(u32, 4), LODLevel.lod2.scale()); - try std.testing.expectEqual(@as(u32, 8), LODLevel.lod3.scale()); - try std.testing.expectEqual(@as(u32, 16), LODLevel.lod4.scale()); - - try std.testing.expectEqual(@as(u32, 4), LODLevel.lod0.totalChunks()); - try std.testing.expectEqual(@as(u32, 16), LODLevel.lod1.totalChunks()); - try std.testing.expectEqual(@as(u32, 64), LODLevel.lod2.totalChunks()); - try std.testing.expectEqual(@as(u32, 256), LODLevel.lod3.totalChunks()); - try std.testing.expectEqual(@as(u32, 1024), LODLevel.lod4.totalChunks()); -} - -test "LODRegionKey from chunk coords" { - const key1 = LODRegionKey.fromChunkCoords(5, 7, .lod1); - try std.testing.expectEqual(@as(i32, 1), key1.rx); // 5 / 4 = 1 - try std.testing.expectEqual(@as(i32, 1), key1.rz); // 7 / 4 = 1 - - const key2 = LODRegionKey.fromChunkCoords(-3, -5, .lod2); - try std.testing.expectEqual(@as(i32, -1), key2.rx); // -3 / 8 = -1 - try std.testing.expectEqual(@as(i32, -1), key2.rz); // -5 / 8 = -1 -} - -test "LODRegionKey parent and child keys handle negative coordinates" { - const child = LODRegionKey{ .rx = -1, .rz = -3, .lod = .lod1 }; - const parent = child.parentKey().?; - try std.testing.expectEqual(LODRegionKey{ .rx = -1, .rz = -2, .lod = .lod2 }, parent); - - const children = parent.childKeys().?; - try std.testing.expectEqual(LODRegionKey{ .rx = -2, .rz = -4, .lod = .lod1 }, children[0]); - try std.testing.expectEqual(LODRegionKey{ .rx = -1, .rz = -4, .lod = .lod1 }, children[1]); - try std.testing.expectEqual(LODRegionKey{ .rx = -2, .rz = -3, .lod = .lod1 }, children[2]); - try std.testing.expectEqual(LODRegionKey{ .rx = -1, .rz = -3, .lod = .lod1 }, children[3]); -} - -test "LODConfig distance calculation" { - const config = LODConfig{}; - try std.testing.expectEqual(LODLevel.lod0, config.getLODForDistance(10)); - try std.testing.expectEqual(LODLevel.lod0, config.getLODForDistance(20)); - try std.testing.expectEqual(LODLevel.lod1, config.getLODForDistance(50)); - try std.testing.expectEqual(LODLevel.lod2, config.getLODForDistance(100)); -} - -test "LODConfig keeps outer horizon cells below detached-plateau scale" { - var config = LODConfig{}; - const interface = config.interface(); - const width = world_core.LODSimplifiedData.getGridSizeForDensity(.lod4, interface.getSampleDensity(.lod4)); - const cell_size = regionSizeBlocks(.lod4) / (width - 1); - - try std.testing.expectEqual(@as(u32, 65), width); - try std.testing.expectEqual(@as(u32, 8), cell_size); -} - -test "LODConfig distance calculation respects active LOD count" { - const config = LODConfig{ - .radii = .{ 16, 32, 64, 128, 256 }, - .active_lod_count = 2, - }; - - try std.testing.expectEqual(LODLevel.lod0, config.getLODForDistance(10)); - try std.testing.expectEqual(LODLevel.lod1, config.getLODForDistance(20)); - try std.testing.expectEqual(LODLevel.lod1, config.getLODForDistance(100)); - try std.testing.expect(!config.isInRange(100)); -} - -test "ILODConfig exposes clamped active LOD count" { - var config = LODConfig{ .active_lod_count = 2 }; - var interface = config.interface(); - try std.testing.expectEqual(@as(u32, 2), interface.getActiveLODCount()); - - config.active_lod_count = 0; - interface = config.interface(); - try std.testing.expectEqual(@as(u32, 1), interface.getActiveLODCount()); - - config.active_lod_count = LODLevel.count + 10; - interface = config.interface(); - try std.testing.expectEqual(@as(u32, LODLevel.count), interface.getActiveLODCount()); -} - -test "LODConfig expands render distance into distant LOD horizon" { - try std.testing.expectEqual(@as(u32, LODLevel.count), LODConfig.activeCountForRenderDistance(8)); - try std.testing.expectEqual(@as(u32, LODLevel.count), LODConfig.activeCountForRenderDistance(32)); - - const low_radii = LODConfig.radiiForRenderDistance(8); - try std.testing.expectEqual(@as(i32, 24), low_radii[0]); - try std.testing.expectEqual(@as(i32, 96), low_radii[1]); - try std.testing.expectEqual(@as(i32, 256), low_radii[2]); - try std.testing.expectEqual(@as(i32, 512), low_radii[3]); - try std.testing.expectEqual(@as(i32, 512), low_radii[4]); - - const radii = LODConfig.radiiForRenderDistance(32); - try std.testing.expectEqual(@as(i32, 96), radii[0]); - try std.testing.expectEqual(@as(i32, 192), radii[1]); - try std.testing.expectEqual(@as(i32, 384), radii[2]); - try std.testing.expectEqual(@as(i32, 512), radii[3]); - try std.testing.expectEqual(@as(i32, 512), radii[4]); - - try std.testing.expectEqual(@as(i32, 256), LODConfig.recommendedHorizonDistance(8)); - try std.testing.expectEqual(@as(i32, 131_072), LODConfig.recommendedHorizonDistance(4096)); - try std.testing.expectEqual(std.math.maxInt(i32), LODConfig.recommendedHorizonDistance(std.math.maxInt(i32))); - try std.testing.expectEqual(@as(i32, 4096), LODConfig.normalizeHorizonDistance(4096, 2048)); - try std.testing.expectEqual(@as(i32, 512), LODConfig.stepHorizonDistance(32, 1024, false)); - try std.testing.expectEqual(@as(i32, 256), LODConfig.stepHorizonDistance(32, 512, false)); - try std.testing.expectEqual(@as(i32, 256), LODConfig.stepHorizonDistance(32, 256, false)); - try std.testing.expectEqual(@as(i32, 512), LODConfig.stepHorizonDistance(32, 512, true)); - try std.testing.expectEqual(@as(i32, 256), LODConfig.normalizeUserHorizonDistance(32, 128)); - try std.testing.expectEqual(@as(i32, 512), LODConfig.normalizeUserHorizonDistance(32, 1024)); - try std.testing.expectEqual(@as(i32, 600), LODConfig.normalizeUserHorizonDistance(600, 512)); - - const custom_horizon = LODConfig.radiiForDistances(12, 1024); - try std.testing.expectEqual(@as(i32, 36), custom_horizon[0]); - try std.testing.expectEqual(@as(i32, 96), custom_horizon[1]); - try std.testing.expectEqual(@as(i32, 256), custom_horizon[2]); - try std.testing.expectEqual(@as(i32, 512), custom_horizon[3]); - try std.testing.expectEqual(@as(i32, 1024), custom_horizon[4]); - - const beyond_horizon = LODConfig.radiiForDistances(4096, 2048); - try std.testing.expectEqual([_]i32{4096} ** LODLevel.count, beyond_horizon); - - const integer_limit = LODConfig.radiiForDistances(std.math.maxInt(i32), 2048); - try std.testing.expectEqual([_]i32{std.math.maxInt(i32)} ** LODLevel.count, integer_limit); -} - -test "LODConfig keeps the coarse fallback when tail radii match" { - var config = LODConfig{ .active_lod_count = LODLevel.count }; - const interface = config.interface(); - - interface.setRadii(.{ 96, 192, 256, 256, 256 }); - try std.testing.expectEqual(@as(u32, LODLevel.count), interface.getActiveLODCount()); - try std.testing.expectEqual(@as(i32, 256), interface.getRadii()[@intFromEnum(LODLevel.lod4)]); - - interface.setRadii(.{ 30, 96, 256, 512, 512 }); - try std.testing.expectEqual(@as(u32, LODLevel.count), interface.getActiveLODCount()); - - interface.setRadii(.{ 36, 96, 256, 512, 1024 }); - try std.testing.expectEqual(@as(u32, LODLevel.count), interface.getActiveLODCount()); -} - -test "ChunkBounds intersects radius radially" { - const axis_region = ChunkBounds{ .min_x = 16, .min_z = 0, .max_x = 31, .max_z = 15 }; - try std.testing.expect(axis_region.intersectsRadius(0, 0, 16)); - - const diagonal_region = ChunkBounds{ .min_x = 16, .min_z = 16, .max_x = 31, .max_z = 31 }; - try std.testing.expect(!diagonal_region.intersectsRadius(0, 0, 16)); - try std.testing.expectEqual(@as(i64, 16 * 16 + 16 * 16), diagonal_region.distanceSquaredToPoint(0, 0)); - - const extreme_region = ChunkBounds{ - .min_x = std.math.maxInt(i32), - .min_z = std.math.maxInt(i32), - .max_x = std.math.maxInt(i32), - .max_z = std.math.maxInt(i32), - }; - try std.testing.expectEqual(std.math.maxInt(i64), extreme_region.distanceSquaredToPoint(std.math.minInt(i32), std.math.minInt(i32))); -} - -test "ILODConfig.calculateMaskRadius" { - var config = LODConfig{ - .chunk_render_radius = 16, - .radii = .{ 16, 40, 80, 160, 512 }, - }; - const interface = config.interface(); - try std.testing.expectEqual(@as(f32, 224.0), interface.calculateMaskRadius()); - - config.chunk_render_radius = 32; - try std.testing.expectEqual(@as(f32, 480.0), interface.calculateMaskRadius()); -} - -test "ILODConfig exposes fallback missing child threshold" { - var config = LODConfig{ .fallback_missing_child_threshold = 0.2 }; - var interface = config.interface(); - try std.testing.expectEqual(@as(f32, 0.2), interface.getFallbackMissingChildThreshold()); - - config.fallback_missing_child_threshold = -1.0; - try std.testing.expectEqual(@as(f32, 0.0), interface.getFallbackMissingChildThreshold()); - - config.fallback_missing_child_threshold = 2.0; - try std.testing.expectEqual(@as(f32, 1.0), interface.getFallbackMissingChildThreshold()); -} - -test "LOD parent remains visible until all direct children are ready" { - var parent = LODChunk.init(0, 0, .lod4); - parent.state = .renderable; - parent.ready_children = 3; - parent.transition_frames_remaining = 0; - - try std.testing.expect(!parent.isCoveredByFinerLOD(1.0)); - parent.ready_children = 4; - try std.testing.expect(parent.isCoveredByFinerLOD(0.0)); -} - -test "ILODConfig exposes LOD quality tuning controls" { - var config = LODConfig{ - .horizontal_detail = .{ 16, 24, 32, 40, 24 }, - .vertical_span_budget = 99, - .mesh_path = .qem, - .compact_tiles_enabled = false, - }; - const interface = config.interface(); - - try std.testing.expectEqual(@as(u32, 32), interface.getHorizontalDetail(.lod2)); - try std.testing.expectEqual(@as(u8, world_core.MAX_LOD_VERTICAL_SPANS), interface.getVerticalSpanBudget()); - try std.testing.expectEqual(LODMeshPath.qem, interface.getMeshPath()); - try std.testing.expect(!interface.getCompactTilesEnabled()); -} diff --git a/modules/world-lod/src/lod_compact_pool.zig b/modules/world-lod/src/lod_compact_pool.zig deleted file mode 100644 index 2b8fe546..00000000 --- a/modules/world-lod/src/lod_compact_pool.zig +++ /dev/null @@ -1,290 +0,0 @@ -//! Shared storage for compact LOD3/4 samples. Freed ranges are not reusable -//! until the same completed frame slot is observed at a later frame serial. -const std = @import("std"); -const sync = @import("sync"); -const rhi = @import("engine-rhi"); -const LODMesh = @import("lod_mesh.zig").LODMesh; -const LODMeshResources = @import("lod_mesh_resources.zig").LODMeshResources; - -pub const CompactLODPool = struct { - pub const CAPACITY_BYTES = 64 * 1024 * 1024; - const Range = struct { offset: usize, size: usize }; - const Retired = struct { range: Range, serial: u64, frame_slot: usize }; - - pub const MemoryStats = struct { - capacity_bytes: usize = 0, - allocated_bytes: usize = 0, - free_bytes: usize = 0, - retired_bytes: usize = 0, - }; - - allocator: std.mem.Allocator, - mutex: sync.Mutex = .{}, - buffer_handle: rhi.BufferHandle = 0, - capacity_bytes: usize, - /// Live bytes. Retired bytes remain unavailable but are not live mesh data. - allocated_bytes: usize = 0, - free_bytes: usize = 0, - retired_bytes: usize = 0, - free_ranges: std.ArrayListUnmanaged(Range) = .empty, - retired: std.ArrayListUnmanaged(Retired) = .empty, - last_completed_serial: ?u64 = null, - - pub fn init(allocator: std.mem.Allocator) CompactLODPool { - return .{ .allocator = allocator, .capacity_bytes = CAPACITY_BYTES }; - } - - fn initForTest(allocator: std.mem.Allocator, capacity_bytes: usize) CompactLODPool { - return .{ .allocator = allocator, .capacity_bytes = capacity_bytes }; - } - - pub fn deinit(self: *CompactLODPool, resources: LODMeshResources) void { - // Renderer shutdown is exclusive; locking a mutex immediately before - // invalidating its storage would make a deferred unlock impossible. - if (self.buffer_handle != 0) resources.destroyBuffer(self.buffer_handle); - self.free_ranges.deinit(self.allocator); - self.retired.deinit(self.allocator); - self.* = undefined; - } - - pub fn memoryStats(self: *CompactLODPool) MemoryStats { - self.mutex.lock(); - defer self.mutex.unlock(); - // A configured capacity is not GPU memory until the backing buffer is - // successfully created. This prevents accounting a failed allocation. - return .{ - .capacity_bytes = if (self.buffer_handle != 0) self.capacity_bytes else 0, - .allocated_bytes = self.allocated_bytes, - .free_bytes = self.free_bytes, - .retired_bytes = self.retired_bytes, - }; - } - - pub fn upload(self: *CompactLODPool, mesh: *LODMesh, resources: LODMeshResources) rhi.RhiError!void { - self.mutex.lock(); - defer self.mutex.unlock(); - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - const tile = mesh.compact_tile orelse return error.InvalidState; - const bytes = std.mem.sliceAsBytes(tile.samples); - if (bytes.len == 0 or bytes.len % 16 != 0) return error.InvalidState; - try self.ensureBuffer(resources); - const range = try self.allocate(bytes.len); - errdefer self.releaseRange(range) catch {}; - try resources.updateBuffer(self.buffer_handle, range.offset, bytes); - - // A range must never be overwritten in place. Callers retire a compact - // mesh before remeshing, so seeing one here proves a lifecycle bug. - if (mesh.compact_sample_bytes != 0) return error.InvalidState; - mesh.compact_sample_offset = @intCast(range.offset / 16); - mesh.compact_sample_bytes = range.size; - mesh.buffer_handle = self.buffer_handle; - mesh.vertex_count = mesh.compact_index_count; - mesh.opaque_vertex_count = mesh.compact_index_count; - mesh.water_vertex_count = if (mesh.compact_has_water) mesh.compact_index_count else 0; - mesh.ready = true; - // Preserve the edge-validity decision after freeing the CPU payload. - // A later arrival cannot safely overwrite this resident range. - mesh.compact_neighbor_apron_mask = tile.neighbor_apron_mask; - var mutable_tile = mesh.compact_tile.?; - mutable_tile.deinit(); - mesh.compact_tile = null; - } - - /// Retire a compact range at the serial/frame-slot that last submitted a - /// draw. `collectRetired` only returns it after that slot is completed and - /// reused at a strictly later serial. - pub fn retireMesh(self: *CompactLODPool, mesh: *LODMesh, serial: u64, frame_slot: usize) void { - self.mutex.lock(); - defer self.mutex.unlock(); - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - if (!mesh.compact) return; - if (mesh.compact_sample_bytes != 0) { - const range = Range{ .offset = @as(usize, mesh.compact_sample_offset) * 16, .size = mesh.compact_sample_bytes }; - self.retired.append(self.allocator, .{ .range = range, .serial = serial, .frame_slot = frame_slot }) catch { - // Never reuse on bookkeeping failure. The mesh state is still - // cleared below so a CPU fallback cannot render stale offsets. - self.allocated_bytes -= range.size; - self.retired_bytes += range.size; - self.clearMeshUnlocked(mesh); - return; - }; - self.allocated_bytes -= range.size; - self.retired_bytes += range.size; - } - self.clearMeshUnlocked(mesh); - } - - /// `completed_serial` comes from the monotonic WorldRenderer frame serial; - /// the completed slot is supplied only after its frame fence has made it - /// reusable. Older or duplicate serials cannot reclaim anything. - pub fn collectRetired(self: *CompactLODPool, completed_serial: u64, completed_frame_slot: usize) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.last_completed_serial) |last| { - if (completed_serial <= last) return; - } - self.last_completed_serial = completed_serial; - var i: usize = 0; - while (i < self.retired.items.len) { - const retired = self.retired.items[i]; - if (retired.frame_slot == completed_frame_slot and completed_serial > retired.serial) { - self.releaseRange(retired.range) catch { - // Keep the range retired on allocation failure; it remains - // safe and will be retried at a later completed boundary. - i += 1; - continue; - }; - self.retired_bytes -= retired.range.size; - _ = self.retired.swapRemove(i); - } else i += 1; - } - } - - fn ensureBuffer(self: *CompactLODPool, resources: LODMeshResources) rhi.RhiError!void { - if (self.buffer_handle != 0) return; - // The fixed pool can contain at most a bounded number of minimum-size - // tiles. Reserve retirement metadata up front so pressure cannot turn - // a fence-safe retirement into a permanently leaked range. - try self.retired.ensureTotalCapacity(self.allocator, 16_384); - const handle = try resources.createBuffer(self.capacity_bytes, .storage); - errdefer resources.destroyBuffer(handle); - try self.free_ranges.append(self.allocator, .{ .offset = 0, .size = self.capacity_bytes }); - self.buffer_handle = handle; - self.free_bytes = self.capacity_bytes; - } - - fn allocate(self: *CompactLODPool, size: usize) rhi.RhiError!Range { - if (size == 0 or size % 16 != 0) return error.InvalidState; - for (self.free_ranges.items, 0..) |*range, i| if (range.size >= size) { - const result = Range{ .offset = range.offset, .size = size }; - range.offset += size; - range.size -= size; - if (range.size == 0) _ = self.free_ranges.swapRemove(i); - self.free_bytes -= size; - self.allocated_bytes += size; - return result; - }; - return error.OutOfMemory; - } - - fn releaseRange(self: *CompactLODPool, range: Range) !void { - var insert_at: usize = 0; - while (insert_at < self.free_ranges.items.len and self.free_ranges.items[insert_at].offset < range.offset) : (insert_at += 1) {} - try self.free_ranges.insert(self.allocator, insert_at, range); - self.free_bytes += range.size; - - var i = if (insert_at > 0) insert_at - 1 else insert_at; - while (i + 1 < self.free_ranges.items.len) { - const current = self.free_ranges.items[i]; - const next = self.free_ranges.items[i + 1]; - if (current.offset + current.size != next.offset) { - i += 1; - continue; - } - self.free_ranges.items[i].size += next.size; - _ = self.free_ranges.orderedRemove(i + 1); - } - } - - fn clearMeshUnlocked(_: *CompactLODPool, mesh: *LODMesh) void { - if (mesh.compact_tile) |*tile| tile.deinit(); - mesh.compact_tile = null; - mesh.compact_neighbor_apron_mask = 0; - mesh.compact = false; - mesh.compact_sample_offset = 0; - mesh.compact_sample_bytes = 0; - mesh.compact_index_count = 0; - mesh.compact_tile_width = 0; - mesh.compact_has_water = false; - mesh.buffer_handle = 0; - mesh.vertex_count = 0; - mesh.opaque_vertex_count = 0; - mesh.water_vertex_offset = 0; - mesh.water_vertex_count = 0; - mesh.vertex_offset = 0; - mesh.capacity = 0; - mesh.pooled = false; - mesh.ready = false; - } -}; - -test "compact pool capacity is sample aligned" { - try std.testing.expectEqual(@as(usize, 0), CompactLODPool.CAPACITY_BYTES % 16); -} - -test "compact pool retires only at a later completed frame-slot boundary" { - var pool = CompactLODPool.initForTest(std.testing.allocator, 64); - defer { - pool.free_ranges.deinit(std.testing.allocator); - pool.retired.deinit(std.testing.allocator); - } - try pool.free_ranges.append(std.testing.allocator, .{ .offset = 0, .size = 64 }); - pool.free_bytes = 64; - // This test exercises bookkeeping without creating a GPU backing buffer. - const range = try pool.allocate(16); - try pool.retired.append(std.testing.allocator, .{ .range = range, .serial = 10, .frame_slot = 1 }); - pool.allocated_bytes -= 16; - pool.retired_bytes += 16; - - pool.collectRetired(10, 1); - try std.testing.expectEqual(@as(usize, 48), pool.free_bytes); - pool.collectRetired(11, 0); - try std.testing.expectEqual(@as(usize, 48), pool.free_bytes); - pool.collectRetired(11, 1); // duplicate serial cannot reclaim. - try std.testing.expectEqual(@as(usize, 48), pool.free_bytes); - pool.collectRetired(12, 1); - try std.testing.expectEqual(@as(usize, 64), pool.free_bytes); - try std.testing.expectEqual(@as(usize, 0), pool.retired_bytes); -} - -test "compact pool reports exhaustion without reusing retired bytes" { - var pool = CompactLODPool.initForTest(std.testing.allocator, 32); - defer { - pool.free_ranges.deinit(std.testing.allocator); - pool.retired.deinit(std.testing.allocator); - } - try pool.free_ranges.append(std.testing.allocator, .{ .offset = 0, .size = 32 }); - pool.free_bytes = 32; - const a = try pool.allocate(16); - const b = try pool.allocate(16); - try std.testing.expectError(error.OutOfMemory, pool.allocate(16)); - try pool.retired.append(std.testing.allocator, .{ .range = a, .serial = 4, .frame_slot = 0 }); - pool.allocated_bytes -= 16; - pool.retired_bytes += 16; - try std.testing.expectError(error.OutOfMemory, pool.allocate(16)); - pool.collectRetired(5, 0); - _ = try pool.allocate(16); - try std.testing.expectEqual(@as(usize, 16), b.size); -} - -test "Phase 5 stress compact pool retirement and reuse remains bounded" { - const cycles = @min(@import("engine-core").envInt("ZIGCRAFT_PHASE5_STRESS_ITERATIONS", 64), 4096); - var pool = CompactLODPool.initForTest(std.testing.allocator, 64); - defer { - pool.free_ranges.deinit(std.testing.allocator); - pool.retired.deinit(std.testing.allocator); - } - try pool.free_ranges.append(std.testing.allocator, .{ .offset = 0, .size = 64 }); - pool.free_bytes = 64; - - var serial: u64 = 1; - for (0..cycles) |cycle| { - const frame_slot = cycle % 2; - const range = try pool.allocate(16); - try pool.retired.append(std.testing.allocator, .{ .range = range, .serial = serial, .frame_slot = frame_slot }); - pool.allocated_bytes -= range.size; - pool.retired_bytes += range.size; - - // Completing the same submission must not make the range reusable. - pool.collectRetired(serial, frame_slot); - try std.testing.expectEqual(@as(usize, 16), pool.retired_bytes); - pool.collectRetired(serial + 1, frame_slot); - try std.testing.expectEqual(@as(usize, 0), pool.retired_bytes); - try std.testing.expectEqual(@as(usize, 64), pool.free_bytes); - try std.testing.expectEqual(@as(usize, 0), pool.allocated_bytes); - serial += 2; - } -} diff --git a/modules/world-lod/src/lod_generator.zig b/modules/world-lod/src/lod_generator.zig deleted file mode 100644 index c6ab0392..00000000 --- a/modules/world-lod/src/lod_generator.zig +++ /dev/null @@ -1,20 +0,0 @@ -const std = @import("std"); -const LODLevel = @import("lod_types.zig").LODLevel; -const LODSimplifiedData = @import("lod_chunk.zig").LODSimplifiedData; - -pub const LODGenerator = struct { - ptr: *anyopaque, - generate_heightmap_only: *const fn (ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void, - maybe_recenter_cache: *const fn (ptr: *anyopaque, player_x: i32, player_z: i32) bool, - seed: u64, - identity_hash: u64, - version: u32, - - pub fn generateHeightmapOnly(self: LODGenerator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - self.generate_heightmap_only(self.ptr, data, region_x, region_z, lod_level, stop_flag); - } - - pub fn maybeRecenterCache(self: LODGenerator, player_x: i32, player_z: i32) bool { - return self.maybe_recenter_cache(self.ptr, player_x, player_z); - } -}; diff --git a/modules/world-lod/src/lod_geometry.zig b/modules/world-lod/src/lod_geometry.zig deleted file mode 100644 index 8e0e9624..00000000 --- a/modules/world-lod/src/lod_geometry.zig +++ /dev/null @@ -1,1674 +0,0 @@ -//! Geometry, material selection, vegetation impostors, and color helpers for LOD mesh generation. - -const std = @import("std"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const world_core = @import("world-core"); -const BiomeId = world_core.BiomeId; -const biome_mod = @import("biome_color_provider.zig"); -const BlockType = world_core.BlockType; -const TextureAtlas = @import("engine-assets").TextureAtlas; -const rhi_types = @import("engine-rhi"); -const Vertex = rhi_types.Vertex; -const encodeColor = rhi_types.encodeColor; -const encodeNormal = rhi_types.encodeNormal; -const encodeMeta = rhi_types.encodeMeta; - -pub const FullDetailMesh = struct { - vertices: []Vertex, - indices: []u32, -}; - -/// Appends a four-vertex quad and six triangle indices to the caller-owned mesh buffers. -/// The quad vertices are copied in order and indexed as two triangles `(0,1,2)` and `(0,2,3)`. -pub fn appendIndexedQuad( - vertices: *std.ArrayListUnmanaged(Vertex), - indices: *std.ArrayListUnmanaged(u32), - allocator: std.mem.Allocator, - quad: *const [4]Vertex, -) !void { - const base: u32 = @intCast(vertices.items.len); - try vertices.appendSlice(allocator, quad); - try indices.appendSlice(allocator, &.{ - base, base + 1, base + 2, - base, base + 2, base + 3, - }); -} - -pub const SkirtParams = struct { - x: f32, - z: f32, - size: f32, - avg_h: f32, - avg_c: u32, - brightness: f32, - dir: SkirtDir, -}; - -pub const SkirtDir = enum { north, south, east, west }; - -/// Builds the vertical boundary skirt quad used to hide cracks at the edge of an LOD heightfield. -/// `params.x/z/size` choose the edge segment, `avg_h` is the top edge, and the bottom edge drops below it to cover neighbor gaps. -pub fn makeSkirtQuad(params: SkirtParams, tile_id: u16, world_x: i32, world_z: i32) [4]Vertex { - const p = params; - const cr = unpackR(p.avg_c) * p.brightness; - const cg = unpackG(p.avg_c) * p.brightness; - const cb = unpackB(p.avg_c) * p.brightness; - const skirt_bottom = p.avg_h - p.size * 4.0; - const normal: [3]f32 = switch (p.dir) { - .north => .{ 0, 0, -1 }, - .south => .{ 0, 0, 1 }, - .west => .{ -1, 0, 0 }, - .east => .{ 1, 0, 0 }, - }; - const col = [3]f32{ cr, cg, cb }; - const face_dir = skirtDirToFaceDir(p.dir); - return switch (p.dir) { - .north => .{ - makeLODVertex(.{ p.x + p.size, skirt_bottom, p.z }, col, normal, sideFaceUV(.{ p.x + p.size, skirt_bottom, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, skirt_bottom, p.z }, col, normal, sideFaceUV(.{ p.x, skirt_bottom, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, p.avg_h, p.z }, col, normal, sideFaceUV(.{ p.x, p.avg_h, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, p.avg_h, p.z }, col, normal, sideFaceUV(.{ p.x + p.size, p.avg_h, p.z }, face_dir, world_x, world_z), tile_id), - }, - .south => .{ - makeLODVertex(.{ p.x, skirt_bottom, p.z + p.size }, col, normal, sideFaceUV(.{ p.x, skirt_bottom, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, skirt_bottom, p.z + p.size }, col, normal, sideFaceUV(.{ p.x + p.size, skirt_bottom, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, p.avg_h, p.z + p.size }, col, normal, sideFaceUV(.{ p.x + p.size, p.avg_h, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, p.avg_h, p.z + p.size }, col, normal, sideFaceUV(.{ p.x, p.avg_h, p.z + p.size }, face_dir, world_x, world_z), tile_id), - }, - .west => .{ - makeLODVertex(.{ p.x, skirt_bottom, p.z }, col, normal, sideFaceUV(.{ p.x, skirt_bottom, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, skirt_bottom, p.z + p.size }, col, normal, sideFaceUV(.{ p.x, skirt_bottom, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, p.avg_h, p.z + p.size }, col, normal, sideFaceUV(.{ p.x, p.avg_h, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x, p.avg_h, p.z }, col, normal, sideFaceUV(.{ p.x, p.avg_h, p.z }, face_dir, world_x, world_z), tile_id), - }, - .east => .{ - makeLODVertex(.{ p.x + p.size, skirt_bottom, p.z + p.size }, col, normal, sideFaceUV(.{ p.x + p.size, skirt_bottom, p.z + p.size }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, skirt_bottom, p.z }, col, normal, sideFaceUV(.{ p.x + p.size, skirt_bottom, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, p.avg_h, p.z }, col, normal, sideFaceUV(.{ p.x + p.size, p.avg_h, p.z }, face_dir, world_x, world_z), tile_id), - makeLODVertex(.{ p.x + p.size, p.avg_h, p.z + p.size }, col, normal, sideFaceUV(.{ p.x + p.size, p.avg_h, p.z + p.size }, face_dir, world_x, world_z), tile_id), - }, - }; -} - -/// Builds an indexed full-detail heightfield mesh from simplified LOD samples. -/// Each heightmap cell emits a textured top quad, and boundary cells also emit skirts to cover holes before QEM simplification. -pub fn buildFullDetailHeightmapMesh( - allocator: std.mem.Allocator, - lod_level: LODLevel, - data: *const LODSimplifiedData, - world_x: i32, - world_z: i32, - atlas: *const TextureAtlas, -) !FullDetailMesh { - const w = data.width; - const grid_total = w * w; - if (grid_total == 0) return error.EmptyData; - const grid_count: usize = @intCast(grid_total); - std.debug.assert(grid_count <= data.heightmap.len and - grid_count <= data.biomes.len and - grid_count <= data.top_blocks.len and - grid_count <= data.colors.len and - grid_count <= data.material_layers.len and - grid_count <= data.water.len and - grid_count <= data.lighting.len and - grid_count <= data.vegetation.len); - - if (w < 2) return error.EmptyData; - const cell_size: f32 = @as(f32, @floatFromInt(lod_chunk.regionSizeBlocks(lod_level))) / @as(f32, @floatFromInt(w - 1)); - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - errdefer vertices.deinit(allocator); - var indices = std.ArrayListUnmanaged(u32).empty; - errdefer indices.deinit(allocator); - - var gz: u32 = 0; - while (gz + 1 < w) : (gz += 1) { - var gx: u32 = 0; - while (gx + 1 < w) : (gx += 1) { - const h00 = data.heightmap[gx + gz * w]; - const h10 = data.heightmap[(gx + 1) + gz * w]; - const h01 = data.heightmap[gx + (gz + 1) * w]; - const h11 = data.heightmap[(gx + 1) + (gz + 1) * w]; - - const c00 = data.colors[gx + gz * w]; - const c10 = data.colors[(gx + 1) + gz * w]; - const c01 = data.colors[gx + (gz + 1) * w]; - const c11 = data.colors[(gx + 1) + (gz + 1) * w]; - const wx = @as(f32, @floatFromInt(gx)) * cell_size; - const wz = @as(f32, @floatFromInt(gz)) * cell_size; - const size = cell_size; - const material = selectCellMaterial(data, atlas, gx, gz); - - const top_block = blockForLODQuad(data, gx, gz); - const top_tile_id = getLodTopTile(top_block, atlas); - const tc00 = applyTextureLuminance(getLodTopColor(top_block, top_tile_id, applyColorBrightness(c00, data.lighting[gx + gz * w].ambient_occlusion)), top_block, .top, atlas); - const tc10 = applyTextureLuminance(getLodTopColor(top_block, top_tile_id, applyColorBrightness(c10, data.lighting[(gx + 1) + gz * w].ambient_occlusion)), top_block, .top, atlas); - const tc01 = applyTextureLuminance(getLodTopColor(top_block, top_tile_id, applyColorBrightness(c01, data.lighting[gx + (gz + 1) * w].ambient_occlusion)), top_block, .top, atlas); - const tc11 = applyTextureLuminance(getLodTopColor(top_block, top_tile_id, applyColorBrightness(c11, data.lighting[(gx + 1) + (gz + 1) * w].ambient_occlusion)), top_block, .top, atlas); - const top_quad = [4]Vertex{ - makeLODVertex(.{ wx, h00, wz }, .{ unpackR(tc00), unpackG(tc00), unpackB(tc00) }, .{ 0, 1, 0 }, topFaceUV(.{ wx, h00, wz }, world_x, world_z), top_tile_id), - makeLODVertex(.{ wx + size, h10, wz }, .{ unpackR(tc10), unpackG(tc10), unpackB(tc10) }, .{ 0, 1, 0 }, topFaceUV(.{ wx + size, h10, wz }, world_x, world_z), top_tile_id), - makeLODVertex(.{ wx + size, h11, wz + size }, .{ unpackR(tc11), unpackG(tc11), unpackB(tc11) }, .{ 0, 1, 0 }, topFaceUV(.{ wx + size, h11, wz + size }, world_x, world_z), top_tile_id), - makeLODVertex(.{ wx, h01, wz + size }, .{ unpackR(tc01), unpackG(tc01), unpackB(tc01) }, .{ 0, 1, 0 }, topFaceUV(.{ wx, h01, wz + size }, world_x, world_z), top_tile_id), - }; - try appendIndexedQuad(&vertices, &indices, allocator, &top_quad); - - if (gz == 0) try appendIndexedQuad(&vertices, &indices, allocator, &makeSkirtQuad(.{ - .x = wx, - .z = wz, - .size = size, - .avg_h = (h00 + h10) * 0.5, - .avg_c = averageColor(c00, c10, c00, c10), - .brightness = 0.7, - .dir = .north, - }, material.side, world_x, world_z)); - if (gz == w - 2) try appendIndexedQuad(&vertices, &indices, allocator, &makeSkirtQuad(.{ - .x = wx, - .z = wz, - .size = size, - .avg_h = (h01 + h11) * 0.5, - .avg_c = averageColor(c01, c11, c01, c11), - .brightness = 0.7, - .dir = .south, - }, material.side, world_x, world_z)); - if (gx == 0) try appendIndexedQuad(&vertices, &indices, allocator, &makeSkirtQuad(.{ - .x = wx, - .z = wz, - .size = size, - .avg_h = (h00 + h01) * 0.5, - .avg_c = averageColor(c00, c01, c00, c01), - .brightness = 0.8, - .dir = .west, - }, material.side, world_x, world_z)); - if (gx == w - 2) try appendIndexedQuad(&vertices, &indices, allocator, &makeSkirtQuad(.{ - .x = wx, - .z = wz, - .size = size, - .avg_h = (h10 + h11) * 0.5, - .avg_c = averageColor(c10, c11, c10, c11), - .brightness = 0.8, - .dir = .east, - }, material.side, world_x, world_z)); - } - } - - return .{ - .vertices = try vertices.toOwnedSlice(allocator), - .indices = try indices.toOwnedSlice(allocator), - }; -} - -pub const FaceDir = enum { north, south, east, west }; -pub const WORLDGEN_SEA_LEVEL: f32 = 64.0; -pub const SEA_LEVEL_WATER_EPSILON: f32 = 2.0; -pub const SYNTHETIC_SEAFLOOR_SKIRT: f32 = 8.0; -pub const LOD_TREE_COVERAGE_THRESHOLD: f32 = 0.08; -/// At the horizon a coverage below this cannot form a stable canopy pixel. -/// Aggregate it into the terrain material instead of emitting noisy slivers. -pub const LOD_FAR_TREE_SUBPIXEL_THRESHOLD: f32 = 0.16; - -/// Classifies a coarse LOD cell as water using averaged coverage, wet-sample count, and representative depth. -/// Used for coarse material selection where a single fine water sample should not flood the whole cell. -pub fn isLODWaterCell(data: *const LODSimplifiedData, gx: u32, gz: u32) bool { - const stats = waterCoverageStats(data, gx, gz); - const water_coverage = stats.average_coverage; - if (water_coverage >= 0.35) return true; - return stats.wet_samples >= 2 and water_coverage >= 0.25 and stats.representative_depth >= 1.5; -} - -/// Reports whether an LOD level should sample one grid cell directly instead of blending a 2x2 neighborhood. -/// LOD0 and LOD1 preserve per-cell detail; coarser levels average neighboring samples for stability. -pub fn isFineSampleLOD(lod_level: LODLevel) bool { - return @intFromEnum(lod_level) <= @intFromEnum(LODLevel.lod1); -} - -/// Returns the clamped flat index for a sample-grid coordinate. -/// `gx` and `gz` may point at a neighbor cell; values are clamped to the valid `data.width` range. -pub fn cellIndex(data: *const LODSimplifiedData, gx: u32, gz: u32) u32 { - return @min(gx, data.width - 1) + @min(gz, data.width - 1) * data.width; -} - -/// Returns the representative packed RGB color for an LOD cell. -/// Fine LODs use the direct sample; coarse LODs average the surrounding 2x2 colors to avoid abrupt material changes. -pub fn cellColorForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) u32 { - if (isFineSampleLOD(lod_level)) return data.colors[cellIndex(data, gx, gz)]; - const c00 = data.colors[cellIndex(data, gx, gz)]; - const c10 = data.colors[cellIndex(data, gx + 1, gz)]; - const c01 = data.colors[cellIndex(data, gx, gz + 1)]; - const c11 = data.colors[cellIndex(data, gx + 1, gz + 1)]; - return averageColor(c00, c10, c01, c11); -} - -/// Returns representative ambient occlusion for an LOD cell. -/// Fine LODs use the direct lighting sample; coarse LODs average neighboring lighting to smooth distant terrain. -pub fn ambientOcclusionForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) f32 { - if (isFineSampleLOD(lod_level)) return data.lighting[cellIndex(data, gx, gz)].ambient_occlusion; - return averageAmbientOcclusion(data, gx, gz); -} - -/// Classifies water for a cell using LOD-specific sampling rules. -/// Fine levels require the direct sample to be a shallow-or-deeper water surface; coarse levels use coverage statistics. -pub fn isLODWaterCellForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) bool { - if (isFineSampleLOD(lod_level)) { - const water = data.water[cellIndex(data, gx, gz)]; - return water.is_surface and water.coverage > 0.0 and water.depth >= 0.25; - } - return isLODWaterCell(data, gx, gz); -} - -/// Rounds a height to the nearest block unit for mesh seam stability. -/// Quantization keeps adjacent LOD cells from producing tiny floating-point cracks. -pub fn quantizedHeight(height: f32) f32 { - return @round(height); -} - -/// Returns the solid terrain height used below water surfaces. -/// For water cells, this clamps the visible terrain floor below the water surface instead of returning the water plane. -pub fn terrainHeightForPoint(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - const clamped_x = @min(gx, data.width - 1); - const clamped_z = @min(gz, data.width - 1); - const idx = clamped_x + clamped_z * data.width; - const height = terrainSurfaceHeightForPoint(data, clamped_x, clamped_z); - const water = data.water[idx]; - if (!water.is_surface or water.coverage <= 0.0) return height; - - const floor_height = if (water.depth > 0.0) - @max(0.0, water.surface_height - water.depth) - else - water.surface_height; - return @min(height, floor_height); -} - -/// Returns the authoritative terrain surface height at a sample-grid point. -/// Cross-region stitching requires real neighbor samples; blending against -/// unrelated samples inside this region creates visible edge distortion. -pub fn terrainSurfaceHeightForPoint(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - const clamped_x = @min(gx, data.width - 1); - const clamped_z = @min(gz, data.width - 1); - return data.getHeight(clamped_x, clamped_z); -} - -/// Returns the quantized solid terrain height for a sample-grid point. -/// This is the terrain-floor height, not necessarily the visible water surface. -pub fn quantizedTerrainHeightForPoint(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - return quantizedHeight(terrainHeightForPoint(data, gx, gz)); -} - -/// Returns a quantized solid terrain height for an LOD cell. -/// Used by material and span generation when terrain should remain below water. -pub fn quantizedCellTerrainHeight(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - return quantizedHeight(terrainHeightForPoint(data, gx, gz)); -} - -/// Returns a quantized surface height for an LOD cell. -/// Unlike terrain-floor height, this keeps dry columns at the visible terrain surface. -pub fn quantizedCellSurfaceHeight(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - return quantizedHeight(terrainSurfaceHeightForPoint(data, gx, gz)); -} - -/// Returns terrain height using fine or coarse sampling appropriate to the LOD level. -/// Fine levels sample the exact point; coarser levels use the representative cell terrain height. -pub fn quantizedCellTerrainHeightForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) f32 { - if (isFineSampleLOD(lod_level)) return quantizedTerrainHeightForPoint(data, gx, gz); - return quantizedCellTerrainHeight(data, gx, gz); -} - -/// Returns the height that should be visible for a terrain column at the requested LOD. -/// Water cells use terrain-floor height, while dry fine cells use the stitched surface height. -pub fn quantizedCellVisualTerrainHeightForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, is_water_cell: bool) f32 { - if (is_water_cell) return quantizedCellTerrainHeightForLOD(data, gx, gz, lod_level); - if (isFineSampleLOD(lod_level)) return quantizedHeight(terrainSurfaceHeightForPoint(data, gx, gz)); - return quantizedCellSurfaceHeight(data, gx, gz); -} - -/// Returns the representative water surface height for a cell when water exists. -/// Currently delegates to the representative surface picker so water quads share the same height policy. -pub fn maxWaterSurfaceHeightForCell(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) ?f32 { - return representativeWaterSurfaceHeightForCell(data, gx, gz, lod_level); -} - -/// Finds the water surface height to use for a fine or coarse LOD cell. -/// Fine cells inspect one sample; coarse cells scan the 2x2 neighborhood and return the first valid water surface. -pub fn representativeWaterSurfaceHeightForCell(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) ?f32 { - if (isFineSampleLOD(lod_level)) { - const idx = cellIndex(data, gx, gz); - const water = data.water[idx]; - if (!water.is_surface or water.coverage <= 0.0) return null; - return normalizedWaterSurfaceHeight(data, idx, water); - } - - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - for (indices) |idx| { - const water = data.water[idx]; - if (!water.is_surface or water.coverage <= 0.0) continue; - return normalizedWaterSurfaceHeight(data, idx, water); - } - - return null; -} - -/// Normalizes ocean water surfaces near sea level to the canonical sea-level height. -/// This removes tiny generator deviations that would otherwise create visible ocean seams. -pub fn normalizedWaterSurfaceHeight(data: *const LODSimplifiedData, idx: u32, water: world_core.LODWaterState) f32 { - if (isOceanBiome(data.biomes[idx]) and @abs(water.surface_height - WORLDGEN_SEA_LEVEL) <= SEA_LEVEL_WATER_EPSILON) { - return WORLDGEN_SEA_LEVEL; - } - return water.surface_height; -} - -/// Reports whether a biome should use ocean water normalization and ocean-floor material fallback. -/// Only ocean-family biomes return true. -pub fn isOceanBiome(biome: BiomeId) bool { - return switch (biome) { - .deep_ocean, .ocean, .warm_ocean, .frozen_ocean, .cold_ocean => true, - else => false, - }; -} - -/// Returns the quantized water plane for a cell, falling back to terrain height when no water is present. -/// Used when constructing water spans and side faces that need a stable top height. -pub fn quantizedWaterSurfaceHeightForCell(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) f32 { - if (maxWaterSurfaceHeightForCell(data, gx, gz, lod_level)) |height| return quantizedHeight(height); - return quantizedCellTerrainHeightForLOD(data, gx, gz, lod_level); -} - -/// Returns a quantized water height for a vertical span. -/// When the cell has no representative water surface, `fallback_height` supplies the span top. -pub fn quantizedWaterSurfaceHeightForSpan(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, fallback_height: f32) f32 { - if (maxWaterSurfaceHeightForCell(data, gx, gz, lod_level)) |height| return quantizedHeight(height); - return quantizedHeight(fallback_height); -} - -pub const FoldedCanopyColumn = struct { - height: f32, - block: BlockType, - color: u32, -}; - -/// Reports whether vegetation canopy should be merged into terrain height for this LOD level. -/// The current policy leaves canopy as separate geometry for every level. -pub fn shouldFoldCanopyIntoTerrain(lod_level: LODLevel) bool { - _ = lod_level; - return false; -} - -/// Reports whether LOD tree trunks are still detailed enough to render at this level. -/// Very coarse levels skip trunks to avoid noisy distant vertical slivers. -pub fn shouldRenderLODTreeTrunk(lod_level: LODLevel) bool { - return @intFromEnum(lod_level) <= @intFromEnum(LODLevel.lod3); -} - -/// Builds a synthetic canopy column when tree coverage should raise the visible LOD terrain. -/// Returns null for water, non-tree terrain, low coverage, or LOD policies that keep canopy separate. -pub fn foldedCanopyColumnForLOD( - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - base_height: f32, - terrain_block: BlockType, - is_water_cell: bool, -) ?FoldedCanopyColumn { - if (!shouldFoldCanopyIntoTerrain(lod_level) or is_water_cell or !shouldRenderLODTree(terrain_block)) return null; - - const vegetation = representativeVegetationForLOD(data, gx, gz, lod_level); - if (vegetation.tree_coverage < LOD_TREE_COVERAGE_THRESHOLD or vegetation.avg_tree_height < 2.0) return null; - - const leaves = if (vegetation.leaves == .air) BlockType.leaves else vegetation.leaves; - const coverage = std.math.clamp(vegetation.tree_coverage, 0.0, 1.0); - const height_boost = @max(2.0, @max(3.0, vegetation.avg_tree_height) * std.math.clamp(coverage, 0.35, 1.0)); - return .{ - .height = quantizedHeight(base_height + height_boost), - .block = leaves, - .color = packBlockDefaultColor(leaves, 0x2F7D2A), - }; -} - -/// Returns the final visible column height after water and optional canopy folding are considered. -/// This is the height used by column-span mesh generation for the top of visible terrain. -pub fn quantizedVisualColumnHeightForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) f32 { - const is_water_cell = isLODWaterCellForLOD(data, gx, gz, lod_level); - const terrain_block = terrainBlockForLODQuadForLOD(data, gx, gz, is_water_cell, lod_level); - const base_height = quantizedCellVisualTerrainHeightForLOD(data, gx, gz, lod_level, is_water_cell); - if (foldedCanopyColumnForLOD(data, gx, gz, lod_level, base_height, terrain_block, is_water_cell)) |folded| { - return folded.height; - } - return base_height; -} - -/// Chooses the side/top terrain block for a possibly-water coarse LOD quad. -/// Water cells prefer a solid side/subsurface material, then representative surface material, then ocean-floor fallback. -pub fn terrainBlockForLODQuad(data: *const LODSimplifiedData, gx: u32, gz: u32, is_water_cell: bool) BlockType { - if (!is_water_cell) return blockForLODQuad(data, gx, gz); - - const side_block = sideBlockForLODQuad(data, gx, gz, .water); - if (side_block != .air and side_block != .water) return side_block; - - const representative = representativeSurfaceBlock(data, gx, gz); - if (representative != .air and representative != .water) return representative; - - const idx = @min(gx, data.width - 1) + @min(gz, data.width - 1) * data.width; - return data.biomes[idx].getOceanFloorBlock(representativeWaterDepth(data, gx, gz)); -} - -/// Chooses the representative terrain material for a quad using LOD-specific sampling rules. -/// Fine water cells use direct material layers; coarser cells use the coarse representative material logic. -pub fn terrainBlockForLODQuadForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, is_water_cell: bool, lod_level: LODLevel) BlockType { - if (!isFineSampleLOD(lod_level)) return terrainBlockForLODQuad(data, gx, gz, is_water_cell); - if (!is_water_cell) return blockForLODCell(data, gx, gz); - - const idx = cellIndex(data, gx, gz); - const subsurface = data.material_layers[idx].subsurface; - if (subsurface != .air and subsurface != .water) return subsurface; - const surface = data.material_layers[idx].surface; - if (surface != .air and surface != .water) return surface; - return data.biomes[idx].getOceanFloorBlock(data.water[idx].depth); -} - -/// Returns the atlas tile id used for LOD side faces of a block. -/// Air falls back to the stone side tile so missing side material does not sample an invalid atlas entry. -pub fn getLodSideTile(block: BlockType, atlas: *const TextureAtlas) u16 { - if (block == .air) return Vertex.LOD_TILE_ID; - if (isLeafBlock(block)) return Vertex.LOD_TILE_ID; - const tiles = atlas.getTilesForBlock(@intFromEnum(block)); - if (tiles.side == 0) return Vertex.LOD_TILE_ID; - return tiles.side; -} - -/// Computes how far boundary skirts should drop below a column top. -/// Water-adjacent skirts drop to a synthetic seafloor so ocean edges do not expose holes. -pub fn boundarySkirtDepth(size: f32) f32 { - return std.math.clamp(size, 16.0, 32.0); -} - -/// Returns directional brightness for vertical heightfield side faces. -/// North/south/east/west sides use different factors to preserve block-like directional shading. -pub fn heightfieldSideBrightness(dir: FaceDir) f32 { - return switch (dir) { - .west, .east => 0.8, - .north, .south => 0.7, - }; -} - -/// Adds vertical side geometry around a heightfield cell wherever neighboring cells are lower. -/// The function compares center height against four neighbors and emits side faces down to terrain or seafloor depth. -pub fn addSteppedHeightfieldSides( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - wx: f32, - wz: f32, - size: f32, - column_height: f32, - color: u32, - side_tile: u16, - world_x: i32, - world_z: i32, -) !void { - try addHeightfieldSide(allocator, vertices, wx, wz, size, column_height, if (gx == 0) null else quantizedVisualColumnHeightForLOD(data, gx - 1, gz, lod_level), color, side_tile, .west, world_x, world_z); - try addHeightfieldSide(allocator, vertices, wx, wz, size, column_height, if (gx + 1 >= data.width - 1) null else quantizedVisualColumnHeightForLOD(data, gx + 1, gz, lod_level), color, side_tile, .east, world_x, world_z); - try addHeightfieldSide(allocator, vertices, wx, wz, size, column_height, if (gz == 0) null else quantizedVisualColumnHeightForLOD(data, gx, gz - 1, lod_level), color, side_tile, .north, world_x, world_z); - try addHeightfieldSide(allocator, vertices, wx, wz, size, column_height, if (gz + 1 >= data.width - 1) null else quantizedVisualColumnHeightForLOD(data, gx, gz + 1, lod_level), color, side_tile, .south, world_x, world_z); -} - -/// Adds one vertical side face for a heightfield column edge. -/// The face uses the provided direction, material tile, color, and world offsets for stable UVs. -pub fn addHeightfieldSide( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - wx: f32, - wz: f32, - size: f32, - column_height: f32, - neighbor_height: ?f32, - color: u32, - side_tile: u16, - dir: FaceDir, - world_x: i32, - world_z: i32, -) !void { - const bottom = neighbor_height orelse (column_height - boundarySkirtDepth(size)); - if (column_height <= bottom + 0.01) return; - const brightness = heightfieldSideBrightness(dir); - try addSideFaceQuad(allocator, vertices, wx, column_height, wz, size, bottom, unpackR(color) * brightness, unpackG(color) * brightness, unpackB(color) * brightness, dir, side_tile, world_x, world_z); -} - -pub const LODColumnSpan = struct { - min_height: f32, - max_height: f32, - block: BlockType, - color: u32, - ambient_occlusion: f32, -}; - -pub const HeightInterval = struct { - min_height: f32, - max_height: f32, -}; - -/// Collects the vertical material spans that should be meshed for one LOD column. -/// It combines terrain, water, canopy folding, and material-layer data into a bounded span list. -pub fn collectColumnSpans(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, out: *[world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan) usize { - var count: usize = 0; - var has_water_span = false; - var has_solid_span = false; - var has_canopy_span = false; - const is_water_cell = isLODWaterCellForLOD(data, gx, gz, lod_level); - var i: u8 = 0; - while (i < data.verticalSpanCount(gx, gz)) : (i += 1) { - const raw = data.getVerticalSpan(gx, gz, i) orelse continue; - const block = representativeSpanBlock(raw.material_layers); - if (block == .air) continue; - const min_height = @min(raw.min_height, raw.max_height); - const max_height = @max(raw.min_height, raw.max_height); - if (max_height <= min_height + 0.01) continue; - if (block == .water) { - if (!shouldEmitWaterSpanForLOD(data, gx, gz, lod_level, raw.water)) continue; - has_water_span = true; - } else { - if (is_water_cell and isLeafBlock(block)) continue; - has_solid_span = true; - const terrain_height = data.getHeight(gx, gz); - if (raw.vegetation.tree_coverage > 0.0 and max_height > terrain_height + 1.0) { - has_canopy_span = true; - } - } - insertColumnSpan(out, &count, .{ - .min_height = min_height, - .max_height = max_height, - .block = block, - .color = raw.color, - .ambient_occlusion = raw.lighting.ambient_occlusion, - }); - } - - if (gx < data.width and gz < data.width) { - const idx = gx + gz * data.width; - const vegetation = data.vegetation[idx]; - if (!is_water_cell and !has_canopy_span and vegetation.tree_coverage >= LOD_TREE_COVERAGE_THRESHOLD and vegetation.avg_tree_height >= 2.0 and count < out.len) { - const leaves = if (vegetation.leaves == .air) BlockType.leaves else vegetation.leaves; - const canopy = treeCanopyInterval(quantizedTerrainHeightForPoint(data, gx, gz), vegetation); - insertColumnSpan(out, &count, .{ - .min_height = canopy.min_height, - .max_height = canopy.max_height, - .block = leaves, - .color = packBlockDefaultColor(leaves, data.colors[idx]), - .ambient_occlusion = data.lighting[idx].ambient_occlusion, - }); - } - - const representative_water = representativeWaterStateForLOD(data, gx, gz, lod_level); - if (!has_water_span and representative_water != null and count < out.len) { - const water = representative_water.?; - has_water_span = true; - insertColumnSpan(out, &count, .{ - .min_height = water.surface_height - water.depth, - .max_height = water.surface_height, - .block = .water, - .color = data.colors[idx], - .ambient_occlusion = data.lighting[idx].ambient_occlusion, - }); - } - - if (has_water_span and !has_solid_span and count < out.len) { - const terrain_height = quantizedTerrainHeightForPoint(data, gx, gz); - if (terrain_height > 0.01) { - const seafloor_block = terrainBlockForLODQuad(data, gx, gz, true); - insertColumnSpan(out, &count, .{ - .min_height = syntheticSeafloorMinHeight(data, gx, gz), - .max_height = terrain_height, - .block = seafloor_block, - .color = packBlockDefaultColor(seafloor_block, data.colors[idx]), - .ambient_occlusion = data.lighting[idx].ambient_occlusion, - }); - } - } - } - foldCanopyIntoSpansForLOD(data, gx, gz, lod_level, out, &count); - return count; -} - -/// Computes a fallback seafloor height below water for skirt and side geometry. -/// The result is clamped below the water surface so ocean boundaries hide missing neighboring terrain. -pub fn syntheticSeafloorMinHeight(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - const x_min = if (gx == 0) gx else gx - 1; - const z_min = if (gz == 0) gz else gz - 1; - const x_max = @min(gx + 1, data.width - 1); - const z_max = @min(gz + 1, data.width - 1); - - var min_height: f32 = std.math.floatMax(f32); - var z = z_min; - while (z <= z_max) : (z += 1) { - var x = x_min; - while (x <= x_max) : (x += 1) { - min_height = @min(min_height, quantizedTerrainHeightForPoint(data, x, z)); - } - } - if (min_height == std.math.floatMax(f32)) return 0.0; - return @max(0.0, min_height - SYNTHETIC_SEAFLOOR_SKIRT); -} - -/// Optionally merges vegetation canopy information into column spans for distant rendering. -/// When enabled, it raises or inserts a leaf span based on tree coverage and average height. -pub fn foldCanopyIntoSpansForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, spans: *[world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan, count: *usize) void { - if (!shouldFoldCanopyIntoTerrain(lod_level) or count.* == 0) return; - - const is_water_cell = isLODWaterCellForLOD(data, gx, gz, lod_level); - const terrain_block = terrainBlockForLODQuadForLOD(data, gx, gz, is_water_cell, lod_level); - const base_height = quantizedCellVisualTerrainHeightForLOD(data, gx, gz, lod_level, is_water_cell); - const folded = foldedCanopyColumnForLOD(data, gx, gz, lod_level, base_height, terrain_block, is_water_cell) orelse return; - - var i: usize = 0; - while (i < count.*) { - if (isDetachedCanopySpan(spans[i], base_height)) { - var j = i; - while (j + 1 < count.*) : (j += 1) spans[j] = spans[j + 1]; - count.* -= 1; - continue; - } - i += 1; - } - - var terrain_idx: ?usize = null; - i = 0; - while (i < count.*) : (i += 1) { - if (spans[i].block == .water) continue; - if (spans[i].min_height <= base_height + 0.01) terrain_idx = i; - } - - if (terrain_idx) |idx| { - spans[idx].max_height = @max(spans[idx].max_height, folded.height); - spans[idx].block = folded.block; - spans[idx].color = folded.color; - return; - } - - insertColumnSpan(spans, count, .{ - .min_height = 0.0, - .max_height = folded.height, - .block = folded.block, - .color = folded.color, - .ambient_occlusion = ambientOcclusionForLOD(data, gx, gz, lod_level), - }); -} - -/// Reports whether a span represents canopy detached above the terrain surface. -/// Detached canopy is treated differently from solid ground spans when selecting visible surfaces. -pub fn isDetachedCanopySpan(span: LODColumnSpan, base_height: f32) bool { - return isLeafBlock(span.block) and span.min_height > base_height + 0.5; -} - -/// Returns the block type that best represents a vertical material span. -/// Air-like spans use fallback material so emitted faces sample a visible tile. -pub fn representativeSpanBlock(layers: world_core.LODMaterialLayers) BlockType { - if (layers.surface != .air) return layers.surface; - if (layers.subsurface != .air) return layers.subsurface; - return layers.foundation; -} - -/// Inserts a bounded vertical span into the per-column span list. -/// The list is kept ordered and capped so mesh generation respects `MAX_LOD_VERTICAL_SPANS`. -pub fn insertColumnSpan(out: *[world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan, count: *usize, span: LODColumnSpan) void { - if (count.* >= out.len) return; - var dst = count.*; - count.* += 1; - while (dst > 0 and out[dst - 1].min_height > span.min_height) : (dst -= 1) { - out[dst] = out[dst - 1]; - } - out[dst] = span; -} - -/// Finds the highest span that should behave as solid terrain. -/// Returns null when a column contains only air, water, or detached canopy spans. -pub fn highestSolidSpanIndex(spans: []const LODColumnSpan) ?usize { - var i = spans.len; - while (i > 0) { - i -= 1; - if (spans[i].block != .water) return i; - } - return null; -} - -/// Emits faces for portions of a vertical span that are visible from a neighbor direction. -/// Neighbor intervals are subtracted so covered span ranges do not produce hidden geometry. -pub fn addExposedSpanFaces( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - span: LODColumnSpan, - wx: f32, - wz: f32, - size: f32, - color: u32, - tile_id: u16, - world_x: i32, - world_z: i32, -) !void { - try addExposedSpanFace(allocator, vertices, data, gx, gz, if (gx == 0) null else gx - 1, gz, lod_level, span, wx, wz, size, color, tile_id, .west, world_x, world_z); - try addExposedSpanFace(allocator, vertices, data, gx, gz, if (gx + 1 >= data.width - 1) null else gx + 1, gz, lod_level, span, wx, wz, size, color, tile_id, .east, world_x, world_z); - try addExposedSpanFace(allocator, vertices, data, gx, gz, gx, if (gz == 0) null else gz - 1, lod_level, span, wx, wz, size, color, tile_id, .north, world_x, world_z); - try addExposedSpanFace(allocator, vertices, data, gx, gz, gx, if (gz + 1 >= data.width - 1) null else gz + 1, lod_level, span, wx, wz, size, color, tile_id, .south, world_x, world_z); -} - -/// Emits one vertical quad for an exposed interval of a span side. -/// The interval bounds become the face bottom/top heights and share the span material color. -pub fn addExposedSpanFace( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - neighbor_gx: ?u32, - neighbor_gz: ?u32, - lod_level: LODLevel, - span: LODColumnSpan, - wx: f32, - wz: f32, - size: f32, - color: u32, - tile_id: u16, - dir: FaceDir, - world_x: i32, - world_z: i32, -) !void { - _ = gx; - _ = gz; - var exposed: [world_core.MAX_LOD_VERTICAL_SPANS + 1]HeightInterval = undefined; - var exposed_count: usize = 1; - exposed[0] = .{ .min_height = span.min_height, .max_height = span.max_height }; - - if (neighbor_gx == null or neighbor_gz == null) { - // Without a border apron we do not know the adjacent region column. - // Treat it as matching this span so region borders do not emit walls. - subtractCoveredInterval(&exposed, &exposed_count, span.min_height, span.max_height); - } else { - const nx = neighbor_gx.?; - const nz = neighbor_gz.?; - var neighbor_spans: [world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan = undefined; - const neighbor_count = collectColumnSpans(data, nx, nz, lod_level, &neighbor_spans); - var ni: usize = 0; - while (ni < neighbor_count) : (ni += 1) { - subtractCoveredInterval(&exposed, &exposed_count, neighbor_spans[ni].min_height, neighbor_spans[ni].max_height); - } - } - - var i: usize = 0; - while (i < exposed_count) : (i += 1) { - const interval = exposed[i]; - if (interval.max_height <= interval.min_height + 0.01) continue; - const brightness: f32 = switch (dir) { - .west, .east => 0.8, - .north, .south => 0.7, - }; - try addSideFaceQuad(allocator, vertices, wx, interval.max_height, wz, size, interval.min_height, unpackR(color) * brightness, unpackG(color) * brightness, unpackB(color) * brightness, dir, tile_id, world_x, world_z); - } -} - -/// Subtracts an occluding height interval from a set of visible height intervals. -/// Used to split side faces around neighboring spans that cover part of the same vertical range. -pub fn subtractCoveredInterval(intervals: *[world_core.MAX_LOD_VERTICAL_SPANS + 1]HeightInterval, count: *usize, cover_min: f32, cover_max: f32) void { - var i: usize = 0; - while (i < count.*) { - const current = intervals[i]; - const overlap_min = @max(current.min_height, cover_min); - const overlap_max = @min(current.max_height, cover_max); - if (overlap_max <= overlap_min + 0.01) { - i += 1; - continue; - } - - if (overlap_min <= current.min_height + 0.01 and overlap_max >= current.max_height - 0.01) { - intervals[i] = intervals[count.* - 1]; - count.* -= 1; - continue; - } - - if (overlap_min <= current.min_height + 0.01) { - intervals[i].min_height = overlap_max; - i += 1; - continue; - } - - if (overlap_max >= current.max_height - 0.01) { - intervals[i].max_height = overlap_min; - i += 1; - continue; - } - - if (count.* < intervals.len) { - intervals[i].max_height = overlap_min; - intervals[count.*] = .{ .min_height = overlap_max, .max_height = current.max_height }; - count.* += 1; - } - i += 1; - } -} - -pub const LOD_UV_BLOCK_SCALE: f32 = 1.0; -pub const LOD_UV_WRAP_BLOCKS: i32 = 256; - -/// Computes a repeatable atlas UV offset from world block coordinates. -/// The offset keeps tiled LOD texture sampling stable as neighboring regions stream in. -pub fn lodUVOffset(coord: i32) f32 { - return @floatFromInt(@mod(coord, LOD_UV_WRAP_BLOCKS)); -} - -/// Computes atlas UVs for a top face using world-space X/Z position. -/// World offsets make top textures line up across LOD region boundaries. -pub fn topFaceUV(pos: [3]f32, world_x: i32, world_z: i32) [2]f32 { - return .{ - (lodUVOffset(world_x) + pos[0]) * LOD_UV_BLOCK_SCALE, - (lodUVOffset(world_z) + pos[2]) * LOD_UV_BLOCK_SCALE, - }; -} - -/// Computes atlas UVs for a vertical side face using face direction and world position. -/// Horizontal coordinate follows the face axis while vertical coordinate follows height. -pub fn sideFaceUV(pos: [3]f32, dir: FaceDir, world_x: i32, world_z: i32) [2]f32 { - const horizontal = switch (dir) { - .north, .south => lodUVOffset(world_x) + pos[0], - .east, .west => lodUVOffset(world_z) + pos[2], - }; - return .{ horizontal * LOD_UV_BLOCK_SCALE, pos[1] * LOD_UV_BLOCK_SCALE }; -} - -/// Maps a skirt edge direction to the corresponding material face direction. -/// The result selects the correct side-face UV and texture slot for boundary skirts. -pub fn skirtDirToFaceDir(dir: SkirtDir) FaceDir { - return switch (dir) { - .north => .north, - .south => .south, - .east => .east, - .west => .west, - }; -} - -/// Returns the direct top block for a fine LOD cell. -/// Coordinates are clamped through `cellIndex`, so edge callers can safely request neighbor samples. -pub fn blockForLODCell(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { - const clamped_x = @min(gx, data.width - 1); - const clamped_z = @min(gz, data.width - 1); - const idx = clamped_x + clamped_z * data.width; - const block = data.material_layers[idx].surface; - if (block != .air) return block; - if (data.top_blocks[idx] != .air) return data.top_blocks[idx]; - return data.biomes[idx].getSurfaceBlock(); -} - -/// Chooses a representative top block for a coarse LOD quad. -/// The function samples the 2x2 neighborhood and prefers stable surface material over isolated outliers. -pub fn blockForLODQuad(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { - if (isLODWaterCell(data, gx, gz)) return .water; - return representativeSurfaceBlock(data, gx, gz); -} - -/// Returns the best visible surface block from a coarse 2x2 cell neighborhood. -/// Air and water are ignored when a solid representative material is available. -pub fn representativeSurfaceBlock(data: *const LODSimplifiedData, gx: u32, gz: u32) BlockType { - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - var best_block: BlockType = .air; - var best_count: u32 = 0; - for (indices) |idx| { - const block = if (data.material_layers[idx].surface != .air) data.material_layers[idx].surface else if (data.top_blocks[idx] != .air) data.top_blocks[idx] else data.biomes[idx].getSurfaceBlock(); - if (block == .air or block == .water) continue; - - var count: u32 = 0; - for (indices) |other_idx| { - const other = if (data.material_layers[other_idx].surface != .air) data.material_layers[other_idx].surface else if (data.top_blocks[other_idx] != .air) data.top_blocks[other_idx] else data.biomes[other_idx].getSurfaceBlock(); - if (other == block) count += 1; - } - if (count > best_count) { - best_block = block; - best_count = count; - } - } - - if (best_block != .air) return best_block; - return blockForLODCell(data, gx, gz); -} - -/// Computes a representative water depth for a coarse cell. -/// Depth is weighted by water coverage so thin or partial water samples have less influence. -pub fn representativeWaterDepth(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - var weighted_depth: f32 = 0.0; - var coverage: f32 = 0.0; - for (indices) |idx| { - const water = data.water[idx]; - if (!water.is_surface) continue; - weighted_depth += water.depth * water.coverage; - coverage += water.coverage; - } - if (coverage <= 0.001) return 0.0; - return weighted_depth / coverage; -} - -/// Chooses top and side atlas materials for an LOD mesh cell. -/// Water cells get water top material and terrain side fallback; dry cells use terrain block material. -pub fn selectCellMaterial(data: *const LODSimplifiedData, atlas: *const TextureAtlas, gx: u32, gz: u32) TextureAtlas.BlockTiles { - const top_block = blockForLODQuad(data, gx, gz); - const side_block = sideBlockForLODQuad(data, gx, gz, top_block); - const top_tiles = atlas.getTilesForBlock(@intFromEnum(top_block)); - const side_tiles = atlas.getTilesForBlock(@intFromEnum(side_block)); - return .{ - .top = getLodTopTile(top_block, atlas), - .bottom = if (top_tiles.bottom == 0) Vertex.LOD_TILE_ID else top_tiles.bottom, - .side = if (side_tiles.side == 0) Vertex.LOD_TILE_ID else side_tiles.side, - }; -} - -/// Chooses the block material to use for vertical sides of a coarse LOD quad. -/// It prefers subsurface or neighboring solid material so water and air do not create invisible side walls. -pub fn sideBlockForLODQuad(data: *const LODSimplifiedData, gx: u32, gz: u32, top_block: BlockType) BlockType { - if (top_block != .water) return top_block; - - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - for (indices) |idx| { - if (data.water[idx].is_surface and data.material_layers[idx].subsurface != .air and data.material_layers[idx].subsurface != .water) { - return data.material_layers[idx].subsurface; - } - } - - return blockForLODCell(data, gx, gz); -} - -/// Reports whether a block type should produce LOD vegetation geometry. -/// Only leaf-like/tree surface materials participate in distant tree impostors. -pub fn shouldRenderLODTree(top_block: BlockType) bool { - return top_block != .water and top_block != .air; -} - -/// Reports whether a block type is one of the leaf materials handled by LOD vegetation code. -/// Used to decide tinting and canopy representation. -pub fn isLeafBlock(block: BlockType) bool { - return switch (block) { - .leaves, - .mangrove_leaves, - .jungle_leaves, - .acacia_leaves, - .birch_leaves, - .spruce_leaves, - => true, - else => false, - }; -} - -/// Returns vegetation hints using direct or coarse sampling for the requested LOD level. -/// Fine levels use one sample; coarser levels combine the local 2x2 vegetation hints. -pub fn representativeVegetationForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) world_core.LODVegetationHint { - if (isFineSampleLOD(lod_level)) return data.vegetation[cellIndex(data, gx, gz)]; - const vegetation = representativeVegetation(data, gx, gz); - if (lod_level == .lod4 and vegetation.tree_coverage < LOD_FAR_TREE_SUBPIXEL_THRESHOLD) { - return world_core.LODVegetationHint.empty; - } - return vegetation; -} - -/// Combines a 2x2 neighborhood of vegetation hints into one representative hint. -/// Coverage is area-averaged while height is averaged across non-empty tree -/// samples. This aggregates far vegetation rather than multiplying sparse -/// one-column impostors across a coarse mesh. -pub fn representativeVegetation(data: *const LODSimplifiedData, gx: u32, gz: u32) world_core.LODVegetationHint { - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - var height_sum: f32 = 0.0; - var height_count: u32 = 0; - var coverage_sum: f32 = 0.0; - var best = world_core.LODVegetationHint.empty; - for (indices) |idx| { - const hint = data.vegetation[idx]; - coverage_sum += hint.tree_coverage; - if (hint.avg_tree_height > 0.0) { - height_sum += hint.avg_tree_height; - height_count += 1; - } - if (hint.tree_coverage > best.tree_coverage) best = hint; - } - - best.avg_tree_height = if (height_count == 0) 0.0 else height_sum / @as(f32, @floatFromInt(height_count)); - best.tree_coverage = coverage_sum * 0.25; - return best; -} - -/// Returns average water coverage for the 2x2 neighborhood around a coarse cell. -/// Dry or invalid water samples contribute zero coverage. -pub fn averageWaterCoverage(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - return waterCoverageStats(data, gx, gz).average_coverage; -} - -pub const WaterCoverageStats = struct { - average_coverage: f32, - wet_samples: u32, - representative_depth: f32, -}; - -/// Computes water coverage, wet-sample count, and coverage-weighted representative depth for a coarse cell. -/// Only positive-coverage water surfaces with meaningful depth count as wet samples. -pub fn waterCoverageStats(data: *const LODSimplifiedData, gx: u32, gz: u32) WaterCoverageStats { - if (data.width == 0) return .{ .average_coverage = 0.0, .wet_samples = 0, .representative_depth = 0.0 }; - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const indices = [_]u32{ - x0 + z0 * data.width, - x1 + z0 * data.width, - x0 + z1 * data.width, - x1 + z1 * data.width, - }; - - var coverage_sum: f32 = 0.0; - var weighted_depth: f32 = 0.0; - var coverage_weight: f32 = 0.0; - var wet_samples: u32 = 0; - for (indices) |idx| { - const water = data.water[idx]; - if (!water.is_surface or water.coverage <= 0.0 or water.depth <= 0.01) continue; - coverage_sum += water.coverage; - weighted_depth += water.depth * water.coverage; - coverage_weight += water.coverage; - wet_samples += 1; - } - - return .{ - .average_coverage = coverage_sum * 0.25, - .wet_samples = wet_samples, - .representative_depth = if (coverage_weight <= 0.001) 0.0 else weighted_depth / coverage_weight, - }; -} - -/// Reports whether water should produce a visible span for the requested LOD cell. -/// Fine LODs always emit water when present; coarse LODs require enough coverage or wet samples. -pub fn shouldEmitWaterSpanForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, water: world_core.LODWaterState) bool { - if (!water.is_surface or water.coverage <= 0.0 or water.depth <= 0.01) return false; - if (isFineSampleLOD(lod_level)) return true; - return isLODWaterCellForLOD(data, gx, gz, lod_level); -} - -/// Returns the canonical water surface for a rendered LOD cell. Coarse cells -/// use the same 2x2 coverage decision as terrain meshing, preventing one wet -/// corner from creating a full-cell water span over otherwise dry terrain. -pub fn representativeWaterStateForLOD(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel) ?world_core.LODWaterState { - if (isFineSampleLOD(lod_level)) { - const idx = cellIndex(data, gx, gz); - const water = data.water[idx]; - if (!water.is_surface or water.coverage <= 0.0 or water.depth <= 0.01) return null; - var result = water; - result.surface_height = normalizedWaterSurfaceHeight(data, idx, water); - return result; - } - if (!isLODWaterCellForLOD(data, gx, gz, lod_level)) return null; - - const surface_height = representativeWaterSurfaceHeightForCell(data, gx, gz, lod_level) orelse return null; - const stats = waterCoverageStats(data, gx, gz); - return .{ - .is_surface = true, - .surface_height = surface_height, - .depth = stats.representative_depth, - .coverage = stats.average_coverage, - }; -} - -test "coarse representative water ignores one fully wet corner" { - var data = try LODSimplifiedData.init(std.testing.allocator, .lod2); - defer data.deinit(); - - data.water[0] = .{ - .is_surface = true, - .surface_height = 63.0, - .depth = 8.0, - .coverage = 1.0, - }; - - try std.testing.expect(representativeWaterStateForLOD(&data, 0, 0, .lod2) == null); -} - -// Helper functions for unpacking colors -/// Extracts the red channel from a packed 0xRRGGBB color as a normalized float. -/// The result is in `[0, 1]` for direct use in vertex color data. -pub fn unpackR(color: u32) f32 { - return @as(f32, @floatFromInt((color >> 16) & 0xFF)) / 255.0; -} - -/// Extracts the green channel from a packed 0xRRGGBB color as a normalized float. -/// The result is in `[0, 1]` for direct use in vertex color data. -pub fn unpackG(color: u32) f32 { - return @as(f32, @floatFromInt((color >> 8) & 0xFF)) / 255.0; -} - -/// Extracts the blue channel from a packed 0xRRGGBB color as a normalized float. -/// The result is in `[0, 1]` for direct use in vertex color data. -pub fn unpackB(color: u32) f32 { - return @as(f32, @floatFromInt(color & 0xFF)) / 255.0; -} - -/// Averages four packed RGB colors channel-by-channel. -/// The returned packed color is used for coarse-cell material and skirt shading. -pub fn averageColor(c00: u32, c10: u32, c01: u32, c11: u32) u32 { - const r = ((c00 >> 16) & 0xFF) + ((c10 >> 16) & 0xFF) + ((c01 >> 16) & 0xFF) + ((c11 >> 16) & 0xFF); - const g = ((c00 >> 8) & 0xFF) + ((c10 >> 8) & 0xFF) + ((c01 >> 8) & 0xFF) + ((c11 >> 8) & 0xFF); - const b = (c00 & 0xFF) + (c10 & 0xFF) + (c01 & 0xFF) + (c11 & 0xFF); - const r_avg: u32 = r / 4; - const g_avg: u32 = g / 4; - const b_avg: u32 = b / 4; - return (r_avg << 16) | (g_avg << 8) | b_avg; -} - -/// Averages ambient-occlusion values from a clamped 2x2 sample neighborhood. -/// Used by coarse LODs to avoid single-sample lighting artifacts. -pub fn averageAmbientOcclusion(data: *const LODSimplifiedData, gx: u32, gz: u32) f32 { - const x0 = @min(gx, data.width - 1); - const z0 = @min(gz, data.width - 1); - const x1 = @min(gx + 1, data.width - 1); - const z1 = @min(gz + 1, data.width - 1); - const a00 = data.lighting[x0 + z0 * data.width].ambient_occlusion; - const a10 = data.lighting[x1 + z0 * data.width].ambient_occlusion; - const a01 = data.lighting[x0 + z1 * data.width].ambient_occlusion; - const a11 = data.lighting[x1 + z1 * data.width].ambient_occlusion; - return (a00 + a10 + a01 + a11) * 0.25; -} - -/// Applies a scalar brightness factor to a packed RGB color. -/// Channels are clamped to 255 and repacked as 0xRRGGBB. -pub fn applyColorBrightness(color: u32, brightness: f32) u32 { - const clamped = std.math.clamp(brightness, 0.0, 1.0); - const r: u32 = @intFromFloat(@round(@as(f32, @floatFromInt((color >> 16) & 0xFF)) * clamped)); - const g: u32 = @intFromFloat(@round(@as(f32, @floatFromInt((color >> 8) & 0xFF)) * clamped)); - const b: u32 = @intFromFloat(@round(@as(f32, @floatFromInt(color & 0xFF)) * clamped)); - return (r << 16) | (g << 8) | b; -} - -pub const LODTextureFace = enum { top, side, bottom }; - -/// Chooses biome tinting for LOD top/side/bottom faces when a block type needs tint. -/// Grass tops, water, and leaves use averaged biome tint; other blocks keep the fallback color. -pub fn tintColorForLodFace(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, block: BlockType, face: LODTextureFace, fallback: u32) u32 { - if (block == .grass and face == .top) return averageBiomeBlockTint(data, gx, gz, lod_level, block); - if (block == .water) return averageBiomeBlockTint(data, gx, gz, lod_level, block); - if (isLeafBlock(block)) return averageBiomeBlockTint(data, gx, gz, lod_level, block); - return fallback; -} - -/// Averages biome tint colors for the requested block over the LOD sample neighborhood. -/// Fine LODs use one biome; coarse LODs blend four neighboring biome tints. -pub fn averageBiomeBlockTint(data: *const LODSimplifiedData, gx: u32, gz: u32, lod_level: LODLevel, block: BlockType) u32 { - if (isFineSampleLOD(lod_level)) { - return biome_mod.getBlockTintColor(data.biomes[cellIndex(data, gx, gz)], block); - } - - const c00 = biome_mod.getBlockTintColor(data.biomes[cellIndex(data, gx, gz)], block); - const c10 = biome_mod.getBlockTintColor(data.biomes[cellIndex(data, gx + 1, gz)], block); - const c01 = biome_mod.getBlockTintColor(data.biomes[cellIndex(data, gx, gz + 1)], block); - const c11 = biome_mod.getBlockTintColor(data.biomes[cellIndex(data, gx + 1, gz + 1)], block); - return averageColor(c00, c10, c01, c11); -} - -/// Modulates a material color by the average texture luminance for a block face. -/// This keeps LOD fallback colors closer to the atlas texture brightness. -pub fn applyTextureLuminance(color: u32, block: BlockType, face: LODTextureFace, atlas: *const TextureAtlas) u32 { - if (block == .air or block == .water) return color; - const texture_color = averageTextureColorForFace(block, face, atlas) orelse { - const luminance = atlas.getLuminanceForBlock(@intFromEnum(block)); - const factor = switch (face) { - .top => luminance.top, - .side => luminance.side, - .bottom => luminance.bottom, - }; - return scaleColor(color, std.math.clamp(factor, 0.18, 1.0)); - }; - - if (!shouldTintLodFace(block, face)) return texture_color; - return multiplyColors(texture_color, shaderLikeTintColor(color)); -} - -/// Returns the average atlas color for a block face when texture statistics are available. -/// The face selects top, side, or bottom color according to block texture metadata. -pub fn averageTextureColorForFace(block: BlockType, face: LODTextureFace, atlas: *const TextureAtlas) ?u32 { - const tiles = atlas.getTilesForBlock(@intFromEnum(block)); - const tile_id = switch (face) { - .top => tiles.top, - .bottom => tiles.bottom, - .side => tiles.side, - }; - if (tile_id == 0) return null; - - const colors = atlas.getAverageColorForBlock(@intFromEnum(block)); - return switch (face) { - .top => colors.top, - .bottom => colors.bottom, - .side => colors.side, - }; -} - -/// Reports whether a block face should receive biome tint in the LOD mesh. -/// Tinting is limited to materials whose in-game shader also applies biome coloration. -pub fn shouldTintLodFace(block: BlockType, face: LODTextureFace) bool { - if (block == .grass) return face == .top; - if (block == .water) return true; - return isLeafBlock(block); -} - -/// Multiplies two packed RGB colors channel-by-channel. -/// Used to combine biome tint and texture color in shader-like LOD material paths. -pub fn multiplyColors(base: u32, tint: u32) u32 { - const r: u32 = @intFromFloat(@round(unpackR(base) * unpackR(tint) * 255.0)); - const g: u32 = @intFromFloat(@round(unpackG(base) * unpackG(tint) * 255.0)); - const b: u32 = @intFromFloat(@round(unpackB(base) * unpackB(tint) * 255.0)); - const rr: u32 = @min(r, 255); - const gg: u32 = @min(g, 255); - const bb: u32 = @min(b, 255); - return (rr << 16) | (gg << 8) | bb; -} - -/// Approximates the runtime shader tint result for an LOD block face. -/// It blends biome tint, texture luminance, and fallback color without sampling shaders at runtime. -pub fn shaderLikeTintColor(color: u32) u32 { - const r: u32 = (color >> 16) & 0xFF; - const g: u32 = (color >> 8) & 0xFF; - const b: u32 = color & 0xFF; - const max_channel = @max(r, @max(g, b)); - if (max_channel == 0) return 0xFFFFFF; - - const min_channel = @min(r, @min(g, b)); - const chroma = @as(f32, @floatFromInt(max_channel - min_channel)) / @as(f32, @floatFromInt(max_channel)); - const tint_strength = smoothstep(0.05, 0.2, chroma); - - const max_f = @as(f32, @floatFromInt(max_channel)); - const nr = @as(f32, @floatFromInt(r)) / max_f; - const ng = @as(f32, @floatFromInt(g)) / max_f; - const nb = @as(f32, @floatFromInt(b)) / max_f; - const out_r: u32 = @intFromFloat(@round((1.0 + (nr - 1.0) * tint_strength) * 255.0)); - const out_g: u32 = @intFromFloat(@round((1.0 + (ng - 1.0) * tint_strength) * 255.0)); - const out_b: u32 = @intFromFloat(@round((1.0 + (nb - 1.0) * tint_strength) * 255.0)); - const rr: u32 = @min(out_r, 255); - const gg: u32 = @min(out_g, 255); - const bb: u32 = @min(out_b, 255); - return (rr << 16) | (gg << 8) | bb; -} - -/// Applies cubic Hermite interpolation between two edges. -/// Returns 0 below `edge0`, 1 above `edge1`, and a smooth transition between them. -pub fn smoothstep(edge0: f32, edge1: f32, x: f32) f32 { - const t = std.math.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); - return t * t * (3.0 - 2.0 * t); -} - -/// Scales a packed RGB color by a floating-point factor. -/// Each channel is clamped before repacking. -pub fn scaleColor(color: u32, factor: f32) u32 { - const clamped = std.math.clamp(factor, 0.0, 2.0); - const r: u32 = @intFromFloat(@round(std.math.clamp(@as(f32, @floatFromInt((color >> 16) & 0xFF)) * clamped, 0.0, 255.0))); - const g: u32 = @intFromFloat(@round(std.math.clamp(@as(f32, @floatFromInt((color >> 8) & 0xFF)) * clamped, 0.0, 255.0))); - const b: u32 = @intFromFloat(@round(std.math.clamp(@as(f32, @floatFromInt(color & 0xFF)) * clamped, 0.0, 255.0))); - return (r << 16) | (g << 8) | b; -} - -/// Returns a packed fallback color for a block type when atlas or biome data is unavailable. -/// `fallback` is used for blocks without a known default LOD color. -pub fn packBlockDefaultColor(block: BlockType, fallback: u32) u32 { - if (block == .air) return fallback; - const color = world_core.block_registry.getBlockDefinition(block).default_color; - const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); - const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); - const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); - return (r << 16) | (g << 8) | b; -} - -/// Returns the atlas tile id used for the top face of a block in LOD meshes. -/// Unknown or air blocks fall back to a safe terrain tile. -pub fn getLodTopTile(block: BlockType, atlas: *const TextureAtlas) u16 { - if (block == .air) return Vertex.LOD_TILE_ID; - if (isLeafBlock(block)) return Vertex.LOD_TILE_ID; - - const tiles = atlas.getTilesForBlock(@intFromEnum(block)); - if (tiles.top == 0) return Vertex.LOD_TILE_ID; - return tiles.top; -} - -/// Returns the packed fallback color used for a block top face. -/// The tile id can influence texture-derived colors when atlas statistics are available. -pub fn getLodTopColor(block: BlockType, tile_id: u16, fallback_color: u32) u32 { - _ = block; - if (tile_id == Vertex.LOD_TILE_ID) return fallback_color; - return fallback_color; -} - -/// Packs position, color, normal, UV, and tile id into the shared RHI vertex layout. -/// The returned vertex is ready for terrain, water, or skirt mesh buffers. -pub fn makeLODVertex(pos: [3]f32, col: [3]f32, norm: [3]f32, uv: [2]f32, tile_id: u16) Vertex { - return Vertex{ - .pos = pos, - .color = encodeColor(col), - .normal = encodeNormal(norm), - .uv = .{ @floatCast(uv[0]), @floatCast(uv[1]) }, - .packed_meta = encodeMeta(tile_id, 1.0, 1.0), - .blocklight = 0, - }; -} - -/// Appends the top face for a column or span to the mesh buffers. -/// The quad uses top-face UVs, upward normal, and material color for the selected block. -pub fn addTopFaceQuad(allocator: std.mem.Allocator, vertices: *std.ArrayListUnmanaged(Vertex), x: f32, y: f32, z: f32, size: f32, r: f32, g: f32, b: f32, tile_id: u16, world_x: i32, world_z: i32) !void { - const normal = [3]f32{ 0, 1, 0 }; - const color = [3]f32{ r, g, b }; - - try vertices.append(allocator, makeLODVertex(.{ x, y, z }, color, normal, topFaceUV(.{ x, y, z }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z + size }, color, normal, topFaceUV(.{ x + size, y, z + size }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x, y, z + size }, color, normal, topFaceUV(.{ x, y, z + size }, world_x, world_z), tile_id)); - - try vertices.append(allocator, makeLODVertex(.{ x, y, z }, color, normal, topFaceUV(.{ x, y, z }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z + size }, color, normal, topFaceUV(.{ x + size, y, z + size }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z }, color, normal, topFaceUV(.{ x + size, y, z }, world_x, world_z), tile_id)); -} - -/// Appends the bottom face for a vertical span when it is exposed. -/// The face uses a downward normal and the span material color. -pub fn addBottomFaceQuad(allocator: std.mem.Allocator, vertices: *std.ArrayListUnmanaged(Vertex), x: f32, y: f32, z: f32, size: f32, r: f32, g: f32, b: f32, tile_id: u16, world_x: i32, world_z: i32) !void { - const normal = [3]f32{ 0, -1, 0 }; - const color = [3]f32{ r, g, b }; - - try vertices.append(allocator, makeLODVertex(.{ x, y, z }, color, normal, topFaceUV(.{ x, y, z }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x, y, z + size }, color, normal, topFaceUV(.{ x, y, z + size }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z + size }, color, normal, topFaceUV(.{ x + size, y, z + size }, world_x, world_z), tile_id)); - - try vertices.append(allocator, makeLODVertex(.{ x, y, z }, color, normal, topFaceUV(.{ x, y, z }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z + size }, color, normal, topFaceUV(.{ x + size, y, z + size }, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(.{ x + size, y, z }, color, normal, topFaceUV(.{ x + size, y, z }, world_x, world_z), tile_id)); -} - -/// Appends one side face for a block/span column. -/// Face direction controls normal, winding, UV projection, and side brightness. -pub fn addSideFaceQuad(allocator: std.mem.Allocator, vertices: *std.ArrayListUnmanaged(Vertex), x: f32, y_top: f32, z: f32, size: f32, y_bottom: f32, r: f32, g: f32, b: f32, dir: FaceDir, tile_id: u16, world_x: i32, world_z: i32) !void { - const color = [3]f32{ r, g, b }; - - const normal: [3]f32 = switch (dir) { - .north => .{ 0, 0, -1 }, - .south => .{ 0, 0, 1 }, - .east => .{ 1, 0, 0 }, - .west => .{ -1, 0, 0 }, - }; - - // Calculate quad corners based on direction - const corners: [4][3]f32 = switch (dir) { - .west => .{ - .{ x, y_bottom, z }, - .{ x, y_bottom, z + size }, - .{ x, y_top, z + size }, - .{ x, y_top, z }, - }, - .east => .{ - .{ x + size, y_bottom, z + size }, - .{ x + size, y_bottom, z }, - .{ x + size, y_top, z }, - .{ x + size, y_top, z + size }, - }, - .north => .{ - .{ x + size, y_bottom, z }, - .{ x, y_bottom, z }, - .{ x, y_top, z }, - .{ x + size, y_top, z }, - }, - .south => .{ - .{ x, y_bottom, z + size }, - .{ x + size, y_bottom, z + size }, - .{ x + size, y_top, z + size }, - .{ x, y_top, z + size }, - }, - }; - - try vertices.append(allocator, makeLODVertex(corners[0], color, normal, sideFaceUV(corners[0], dir, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(corners[1], color, normal, sideFaceUV(corners[1], dir, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(corners[2], color, normal, sideFaceUV(corners[2], dir, world_x, world_z), tile_id)); - - try vertices.append(allocator, makeLODVertex(corners[0], color, normal, sideFaceUV(corners[0], dir, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(corners[2], color, normal, sideFaceUV(corners[2], dir, world_x, world_z), tile_id)); - try vertices.append(allocator, makeLODVertex(corners[3], color, normal, sideFaceUV(corners[3], dir, world_x, world_z), tile_id)); -} - -/// Adds simplified trunk and canopy geometry for a distant tree column. -/// The tree footprint is derived from vegetation hints and clipped against water/neighbor coverage. -pub fn addTreeColumn( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - wx: f32, - wz: f32, - cell_size: f32, - base_height: f32, - vegetation: world_core.LODVegetationHint, - atlas: *const TextureAtlas, - world_x: i32, - world_z: i32, -) !void { - if (vegetation.leaves == .air) return; - - const canopy = treeCanopyInterval(base_height, vegetation); - try addTreeCanopyColumn(allocator, vertices, data, gx, gz, lod_level, wx, wz, cell_size, base_height, canopy.min_height, canopy.max_height, vegetation, atlas, world_x, world_z); -} - -/// Adds box-like leaf canopy geometry for a distant tree impostor. -/// Canopy dimensions come from tree height and coverage and are emitted as colored block faces. -pub fn addTreeCanopyColumn( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - wx: f32, - wz: f32, - cell_size: f32, - base_height: f32, - canopy_min_height: f32, - canopy_max_height: f32, - vegetation: world_core.LODVegetationHint, - atlas: *const TextureAtlas, - world_x: i32, - world_z: i32, -) !void { - if (vegetation.leaves == .air) return; - if (canopy_max_height <= canopy_min_height + 0.01) return; - - const leaf_tile = Vertex.LOD_TILE_ID; - const leaf_base_color = tintColorForLodFace(data, gx, gz, lod_level, vegetation.leaves, .top, packBlockDefaultColor(vegetation.leaves, 0x2F7D2A)); - const leaf_top_color = applyTextureLuminance(leaf_base_color, vegetation.leaves, .top, atlas); - const footprint = treeCanopyFootprint(cell_size, vegetation); - const origin = treeFootprintOrigin(wx, wz, cell_size, footprint, vegetation); - try addBoxColumn(allocator, vertices, origin.x, origin.z, footprint, canopy_min_height, canopy_max_height, leaf_top_color, leaf_tile, world_x, world_z); - - if (shouldRenderLODTreeTrunk(lod_level) and vegetation.trunk != .air and canopy_min_height > base_height + 1.0) { - const trunk_tiles = atlas.getTilesForBlock(@intFromEnum(vegetation.trunk)); - const trunk_tile = if (trunk_tiles.side == 0) Vertex.LOD_TILE_ID else trunk_tiles.side; - const trunk_color = applyTextureLuminance(0xFFFFFF, vegetation.trunk, .side, atlas); - const trunk_size = @min(footprint * 0.35, @max(0.65, cell_size * 0.18)); - const trunk_inset = (footprint - trunk_size) * 0.5; - try addBoxColumn(allocator, vertices, origin.x + trunk_inset, origin.z + trunk_inset, trunk_size, base_height, canopy_min_height, trunk_color, trunk_tile, world_x, world_z); - } -} - -/// Computes the horizontal footprint size used for an LOD tree canopy. -/// Higher coverage and taller trees produce larger but bounded canopy boxes. -pub fn treeCanopyFootprint(cell_size: f32, vegetation: world_core.LODVegetationHint) f32 { - const coverage = std.math.clamp(vegetation.tree_coverage, LOD_TREE_COVERAGE_THRESHOLD, 1.0); - const desired = @max(1.0, vegetation.avg_tree_height * (0.30 + coverage * 0.18)); - const min_size = @min(cell_size, 1.35); - const max_size = @max(min_size, cell_size * 0.72); - return std.math.clamp(desired, min_size, max_size); -} - -pub const TreeFootprintOrigin = struct { x: f32, z: f32 }; - -/// Computes the local origin for centering a tree footprint inside a cell. -/// The origin keeps trunk/canopy geometry inside the LOD cell area. -pub fn treeFootprintOrigin(wx: f32, wz: f32, cell_size: f32, footprint: f32, vegetation: world_core.LODVegetationHint) TreeFootprintOrigin { - const max_offset = @max(0.0, (cell_size - footprint) * 0.5 - 0.01); - const offset_x = std.math.clamp(vegetation.offset_x, -max_offset, max_offset); - const offset_z = std.math.clamp(vegetation.offset_z, -max_offset, max_offset); - return .{ - .x = wx + (cell_size - footprint) * 0.5 + offset_x, - .z = wz + (cell_size - footprint) * 0.5 + offset_z, - }; -} - -/// Computes the vertical min/max interval occupied by a tree canopy. -/// The interval starts above terrain height and scales with average tree height. -pub fn treeCanopyInterval(base_height: f32, vegetation: world_core.LODVegetationHint) HeightInterval { - const canopy_height = @max(3.0, vegetation.avg_tree_height); - const top = base_height + canopy_height; - const depth = @max(2.0, canopy_height * 0.45); - return .{ - .min_height = @max(base_height + 1.0, top - depth), - .max_height = top, - }; -} - -/// Adds side faces for a tree trunk or canopy box. -/// Neighbor water information can suppress or adjust faces that would intersect water cells. -pub fn addTreeColumnSides( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - gx: u32, - gz: u32, - lod_level: LODLevel, - wx: f32, - wz: f32, - size: f32, - canopy: HeightInterval, - color: u32, - tile_id: u16, - world_x: i32, - world_z: i32, -) !void { - try addTreeColumnSide(allocator, vertices, data, if (gx == 0) null else gx - 1, gz, lod_level, wx, wz, size, canopy, color, tile_id, .west, world_x, world_z); - try addTreeColumnSide(allocator, vertices, data, if (gx + 1 >= data.width - 1) null else gx + 1, gz, lod_level, wx, wz, size, canopy, color, tile_id, .east, world_x, world_z); - try addTreeColumnSide(allocator, vertices, data, gx, if (gz == 0) null else gz - 1, lod_level, wx, wz, size, canopy, color, tile_id, .north, world_x, world_z); - try addTreeColumnSide(allocator, vertices, data, gx, if (gz + 1 >= data.width - 1) null else gz + 1, lod_level, wx, wz, size, canopy, color, tile_id, .south, world_x, world_z); -} - -/// Adds one side face for a tree box column. -/// The face uses directional normals and material color for a trunk or canopy side. -pub fn addTreeColumnSide( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - data: *const LODSimplifiedData, - neighbor_gx: ?u32, - neighbor_gz: ?u32, - lod_level: LODLevel, - wx: f32, - wz: f32, - size: f32, - canopy: HeightInterval, - color: u32, - tile_id: u16, - dir: FaceDir, - world_x: i32, - world_z: i32, -) !void { - var exposed: [world_core.MAX_LOD_VERTICAL_SPANS + 1]HeightInterval = undefined; - var exposed_count: usize = 1; - exposed[0] = canopy; - - if (neighbor_gx) |nx| { - if (neighbor_gz) |nz| { - const neighbor_veg = representativeVegetationForLOD(data, nx, nz, lod_level); - if (neighbor_veg.tree_coverage >= LOD_TREE_COVERAGE_THRESHOLD) { - const neighbor_is_water_cell = isLODWaterCellForLOD(data, nx, nz, lod_level); - const neighbor_base = quantizedCellVisualTerrainHeightForLOD(data, nx, nz, lod_level, neighbor_is_water_cell); - const neighbor_canopy = treeCanopyInterval(neighbor_base, neighbor_veg); - subtractCoveredInterval(&exposed, &exposed_count, neighbor_canopy.min_height, neighbor_canopy.max_height); - } - } - } - - var i: usize = 0; - while (i < exposed_count) : (i += 1) { - const interval = exposed[i]; - if (interval.max_height <= interval.min_height + 0.01) continue; - const brightness = heightfieldSideBrightness(dir); - try addSideFaceQuad(allocator, vertices, wx, interval.max_height, wz, size, interval.min_height, unpackR(color) * brightness, unpackG(color) * brightness, unpackB(color) * brightness, dir, tile_id, world_x, world_z); - } -} - -/// Adds a rectangular prism column to the mesh buffers. -/// Used by vegetation impostors and other box-like LOD details that need all exposed faces. -pub fn addBoxColumn( - allocator: std.mem.Allocator, - vertices: *std.ArrayListUnmanaged(Vertex), - x: f32, - z: f32, - size: f32, - min_height: f32, - max_height: f32, - color: u32, - tile_id: u16, - world_x: i32, - world_z: i32, -) !void { - if (max_height <= min_height + 0.01) return; - try addTopFaceQuad(allocator, vertices, x, max_height, z, size, unpackR(color), unpackG(color), unpackB(color), tile_id, world_x, world_z); - try addSideFaceQuad(allocator, vertices, x, max_height, z, size, min_height, unpackR(color) * 0.8, unpackG(color) * 0.8, unpackB(color) * 0.8, .west, tile_id, world_x, world_z); - try addSideFaceQuad(allocator, vertices, x, max_height, z, size, min_height, unpackR(color) * 0.8, unpackG(color) * 0.8, unpackB(color) * 0.8, .east, tile_id, world_x, world_z); - try addSideFaceQuad(allocator, vertices, x, max_height, z, size, min_height, unpackR(color) * 0.7, unpackG(color) * 0.7, unpackB(color) * 0.7, .north, tile_id, world_x, world_z); - try addSideFaceQuad(allocator, vertices, x, max_height, z, size, min_height, unpackR(color) * 0.7, unpackG(color) * 0.7, unpackB(color) * 0.7, .south, tile_id, world_x, world_z); -} diff --git a/modules/world-lod/src/lod_ingest.zig b/modules/world-lod/src/lod_ingest.zig deleted file mode 100644 index be8e0d44..00000000 --- a/modules/world-lod/src/lod_ingest.zig +++ /dev/null @@ -1,722 +0,0 @@ -//! Chunk -> LOD downsampler for chunk-derived distant terrain. -//! -//! Part of issue #752 Phase 2: produces per-grid-cell representative LOD data -//! from a real generated/loaded Chunk and writes it into a region's -//! `LODSimplifiedData` with provenance `chunk_derived` (or `edited`). Higher -//! provenance always overwrites lower, so real chunk data upgrades worldgen -//! samples in place and player edits are never clobbered by regeneration. -//! -//! The downsampler is pure: it operates only on a Chunk and a region's source -//! data, so it is fully unit-testable without a renderer or job system. - -const std = @import("std"); - -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; - -const lod_chunk = @import("lod_chunk.zig"); -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const biome_color_provider = @import("biome_color_provider.zig"); - -const LODColumnProvenance = world_core.LODColumnProvenance; -const LODLightingHint = world_core.LODLightingHint; -const LODMaterialLayers = world_core.LODMaterialLayers; -const LODVerticalSpan = world_core.LODVerticalSpan; -const LODVegetationHint = world_core.LODVegetationHint; -const LODWaterState = world_core.LODWaterState; - -/// Per-column vegetation scan height: how many blocks above the terrain -/// surface we look for tree trunks/canopy. Trees taller than this are still -/// detected (their trunk is within the band); only the recorded height is -/// capped for the LOD vegetation hint. -const TREE_SCAN_BAND: u32 = 24; -/// Treat short underground air gaps as caves inside one terrain column. Larger -/// detached runs remain overhang spans for close/mid LODs. -const MAX_INTERNAL_CAVE_GAP: i32 = 16; - -/// Maximum number of grid vertices a single 16x16 chunk can touch along one -/// axis. The finest LOD cell is ~2 blocks, so a chunk spans at most ~9 -/// vertices per axis; 24 is a generous upper bound used for the stack -/// accumulator buffer. -const MAX_SUB_VERTICES: usize = 24; - -/// A representative terrain sample for one LOD grid vertex, aggregated from -/// the chunk columns that fall inside that vertex. -pub const VertexSample = struct { - terrain_height: f32 = 0.0, - biome: BiomeId = .plains, - layers: LODMaterialLayers = .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, - has_water: bool = false, - water_surface_height: f32 = 0.0, - water_depth: f32 = 0.0, - water_coverage: f32 = 0.0, - has_tree: bool = false, - tree_height: f32 = 0.0, - sky_light: u8 = 15, - has_overhang: bool = false, - overhang_min: f32 = 0.0, - overhang_max: f32 = 0.0, - overhang_block: BlockType = .stone, -}; - -const VertexAccumulator = struct { - sample_count: u32 = 0, - max_terrain_y: i32 = -1, - dominant_biome: BiomeId = .plains, - dominant_surface: BlockType = .grass, - dominant_subsurface: BlockType = .dirt, - dominant_foundation: BlockType = .stone, - water_count: u32 = 0, - max_water_surface_y: i32 = -1, - tree_count: u32 = 0, - tree_height_sum: u32 = 0, - sky_light_sum: u32 = 0, - has_overhang: bool = false, - overhang_min_y: i32 = 0, - overhang_max_y: i32 = -1, - dominant_overhang_block: BlockType = .stone, - - fn toSample(self: VertexAccumulator) ?VertexSample { - if (self.sample_count == 0 or self.max_terrain_y < 0) return null; - const height_f: f32 = @floatFromInt(self.max_terrain_y); - const water_coverage: f32 = if (self.sample_count > 0) - @as(f32, @floatFromInt(self.water_count)) / @as(f32, @floatFromInt(self.sample_count)) - else - 0.0; - const has_water = self.water_count > 0 and self.max_water_surface_y > self.max_terrain_y; - const water_surface: f32 = if (has_water) @floatFromInt(self.max_water_surface_y) else height_f; - const water_depth: f32 = if (has_water) @as(f32, @floatFromInt(self.max_water_surface_y - self.max_terrain_y)) else 0.0; - const has_tree = self.tree_count > 0; - const tree_height: f32 = if (self.tree_count > 0) - @as(f32, @floatFromInt(self.tree_height_sum)) / @as(f32, @floatFromInt(self.tree_count)) - else - 0.0; - const avg_sky: u32 = if (self.sample_count > 0) self.sky_light_sum / self.sample_count else 15; - return .{ - .terrain_height = height_f, - .biome = self.dominant_biome, - .layers = .{ - .surface = self.dominant_surface, - .subsurface = self.dominant_subsurface, - .foundation = self.dominant_foundation, - }, - .has_water = has_water, - .water_surface_height = water_surface, - .water_depth = water_depth, - .water_coverage = water_coverage, - .has_tree = has_tree, - .tree_height = tree_height, - .sky_light = @intCast(@min(avg_sky, 15)), - .has_overhang = self.has_overhang, - .overhang_min = if (self.has_overhang) @floatFromInt(self.overhang_min_y) else 0.0, - .overhang_max = if (self.has_overhang) @floatFromInt(self.overhang_max_y) else 0.0, - .overhang_block = self.dominant_overhang_block, - }; - } -}; - -const ColumnGeometry = struct { - terrain_y: i32 = -1, - has_overhang: bool = false, - overhang_min_y: i32 = 0, - overhang_max_y: i32 = -1, - overhang_block: BlockType = .stone, -}; - -/// Downsample a chunk into a region's LOD source data. -/// -/// `region_min_x` / `region_min_z` are the world-space block origin of the -/// region (region.rx * regionSizeBlocks(lod), region.rz * ...). `region_size_blocks` -/// is the edge length of the region in world blocks. Only grid vertices whose -/// nearest world column falls inside the chunk are updated, so partial regions -/// accumulate correctly as neighboring chunks stream in. -/// -/// Returns the number of grid vertices written (i.e. upgraded by this call). -pub fn downsampleChunkIntoRegion( - chunk: *const Chunk, - chunk_x: i32, - chunk_z: i32, - data: *LODSimplifiedData, - region_min_x: i32, - region_min_z: i32, - region_size_blocks: i32, - provenance: LODColumnProvenance, -) u32 { - const width_i: i32 = @intCast(data.width); - if (width_i <= 1) return 0; - const denom: i32 = region_size_blocks; - if (denom <= 0) return 0; - - // Map the chunk's block footprint to the grid-vertex sub-range it touches. - const wx_lo: i32 = chunk_x * CHUNK_SIZE_X; - const wx_hi: i32 = wx_lo + CHUNK_SIZE_X - 1; - const wz_lo: i32 = chunk_z * CHUNK_SIZE_Z; - const wz_hi: i32 = wz_lo + CHUNK_SIZE_Z - 1; - - const gx_lo = clampVertex(vertexForWorld(wx_lo, region_min_x, width_i, denom), width_i); - const gx_hi = clampVertex(vertexForWorld(wx_hi, region_min_x, width_i, denom), width_i); - const gz_lo = clampVertex(vertexForWorld(wz_lo, region_min_z, width_i, denom), width_i); - const gz_hi = clampVertex(vertexForWorld(wz_hi, region_min_z, width_i, denom), width_i); - - const span_x: usize = @intCast(gx_hi - gx_lo + 1); - const span_z: usize = @intCast(gz_hi - gz_lo + 1); - if (span_x > MAX_SUB_VERTICES or span_z > MAX_SUB_VERTICES) return 0; - - var acc: [MAX_SUB_VERTICES][MAX_SUB_VERTICES]VertexAccumulator = undefined; - clearAccumulators(&acc); - - // Forward-map every chunk column to its target grid vertex and aggregate. - var lz: u32 = 0; - while (lz < CHUNK_SIZE_Z) : (lz += 1) { - var lx: u32 = 0; - while (lx < CHUNK_SIZE_X) : (lx += 1) { - const wx: i32 = wx_lo + @as(i32, @intCast(lx)); - const wz: i32 = wz_lo + @as(i32, @intCast(lz)); - const gx = clampVertex(vertexForWorld(wx, region_min_x, width_i, denom), width_i); - const gz = clampVertex(vertexForWorld(wz, region_min_z, width_i, denom), width_i); - const ax: usize = @intCast(gx - gx_lo); - const az: usize = @intCast(gz - gz_lo); - accumulateColumn(&acc[az][ax], chunk, lx, lz); - } - } - - var written: u32 = 0; - var az: usize = 0; - while (az < span_z) : (az += 1) { - const gz: u32 = @intCast(gz_lo + @as(i32, @intCast(az))); - var ax: usize = 0; - while (ax < span_x) : (ax += 1) { - const gx: u32 = @intCast(gx_lo + @as(i32, @intCast(ax))); - const sample = acc[az][ax].toSample() orelse continue; - if (writeIngestedColumn(data, gx, gz, sample, provenance)) written += 1; - } - } - return written; -} - -/// Write one aggregated column into the region source data, honoring the -/// provenance ordering: a cell is only overwritten if `provenance` is at least -/// as authoritative as the cell's current provenance. Returns true if written. -pub fn writeIngestedColumn( - data: *LODSimplifiedData, - gx: u32, - gz: u32, - sample: VertexSample, - provenance: LODColumnProvenance, -) bool { - if (!provenance.canOverwrite(data.getColumnProvenance(gx, gz))) return false; - - const water_state: LODWaterState = if (sample.has_water) - .{ - .is_surface = true, - .surface_height = sample.water_surface_height, - .depth = sample.water_depth, - .coverage = if (sample.water_coverage > 0.0) sample.water_coverage else 1.0, - } - else - LODWaterState.empty; - - const lighting: LODLightingHint = .{ - .sky_light = sample.sky_light, - .block_light = 0, - .ambient_occlusion = 1.0, - }; - - const vegetation: LODVegetationHint = if (sample.has_tree and !water_state.is_surface) - .{ - .tree_coverage = 1.0, - .avg_tree_height = sample.tree_height, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - } - else - LODVegetationHint.empty; - - const color = biome_color_provider.getBiomeColor(sample.biome); - - data.setColumn( - gx, - gz, - sample.terrain_height, - sample.biome, - sample.layers, - color, - water_state, - lighting, - vegetation, - ); - - writeIngestedSpans(data, gx, gz, sample, water_state, lighting, vegetation, color); - data.setColumnProvenance(gx, gz, provenance); - return true; -} - -// --------------------------------------------------------------------------- -// Internals -// --------------------------------------------------------------------------- - -fn clearAccumulators(acc: *[MAX_SUB_VERTICES][MAX_SUB_VERTICES]VertexAccumulator) void { - var z: usize = 0; - while (z < MAX_SUB_VERTICES) : (z += 1) { - var x: usize = 0; - while (x < MAX_SUB_VERTICES) : (x += 1) acc[z][x] = .{}; - } -} - -/// World block coordinate -> grid vertex index (rounded to nearest). -fn vertexForWorld(world_block: i32, region_min: i32, width: i32, region_size_blocks: i32) i32 { - const offset: i32 = world_block - region_min; - // vertex = offset * (width-1) / region_size_blocks, rounded to nearest. - const numer: i32 = offset * (width - 1); - const half: i32 = @divFloor(region_size_blocks, 2); - return @divFloor(numer + half, region_size_blocks); -} - -fn clampVertex(vertex: i32, width: i32) i32 { - if (vertex < 0) return 0; - if (vertex >= width) return width - 1; - return vertex; -} - -/// True for blocks that are part of vegetation (tree) rather than terrain -/// surface. These are excluded from terrain-height detection so a canopy does -/// not masquerade as the ground. -fn isVegetationBlock(block: BlockType) bool { - return switch (block) { - .wood, - .leaves, - .mangrove_log, - .mangrove_leaves, - .mangrove_roots, - .jungle_log, - .jungle_leaves, - .acacia_log, - .acacia_leaves, - .acacia_sapling, - .birch_log, - .birch_leaves, - .spruce_log, - .spruce_leaves, - .bamboo, - .vine, - => true, - else => false, - }; -} - -/// True for blocks that form the solid terrain surface (everything that is -/// not air, water, or vegetation). -fn isTerrainSolid(block: BlockType) bool { - return block != .air and block != .water and !isVegetationBlock(block); -} - -/// Highest terrain-surface Y in a column, or -1 if the column has no solid -/// ground. Skips air, water, and vegetation so tree canopies do not inflate -/// the recorded surface height. -fn columnGeometry(chunk: *const Chunk, lx: u32, lz: u32) ColumnGeometry { - var result: ColumnGeometry = .{}; - var in_solid_run = false; - var run_min: i32 = 0; - var y: u32 = 0; - while (y < CHUNK_SIZE_Y) : (y += 1) { - const block = chunk.getBlock(lx, y, lz); - if (isTerrainSolid(block)) { - if (!in_solid_run) { - in_solid_run = true; - run_min = @intCast(y); - } - continue; - } - - if (in_solid_run) { - finishSolidRun(&result, run_min, @as(i32, @intCast(y)) - 1, chunk.getBlock(lx, @intCast(y - 1), lz)); - in_solid_run = false; - } - } - - if (in_solid_run) { - finishSolidRun(&result, run_min, @as(i32, @intCast(CHUNK_SIZE_Y)) - 1, chunk.getBlock(lx, CHUNK_SIZE_Y - 1, lz)); - } - return result; -} - -fn finishSolidRun(result: *ColumnGeometry, min_y: i32, max_y: i32, top_block: BlockType) void { - if (result.terrain_y < 0) { - result.terrain_y = max_y; - return; - } - if (result.terrain_y >= 0 and min_y > result.terrain_y) { - const air_gap = min_y - result.terrain_y - 1; - if (air_gap <= MAX_INTERNAL_CAVE_GAP) { - result.terrain_y = max_y; - return; - } - } - if (max_y > result.overhang_max_y) { - result.has_overhang = true; - result.overhang_min_y = min_y; - result.overhang_max_y = max_y; - result.overhang_block = top_block; - } -} - -/// Highest water block Y in a column, or -1 if there is no water above the -/// terrain surface. -fn waterSurfaceY(chunk: *const Chunk, lx: u32, lz: u32, terrain_y: i32) i32 { - var top_water: i32 = -1; - var y: i32 = CHUNK_SIZE_Y - 1; - while (y > terrain_y) : (y -= 1) { - if (chunk.getBlock(lx, @intCast(y), lz) == .water) { - top_water = y; - break; - } - } - return top_water; -} - -fn accumulateColumn(acc: *VertexAccumulator, chunk: *const Chunk, lx: u32, lz: u32) void { - const geometry = columnGeometry(chunk, lx, lz); - const terrain_y = geometry.terrain_y; - if (terrain_y < 0) return; // empty column: no terrain to contribute - - acc.sample_count += 1; - const terrain_u32: u32 = @intCast(terrain_y); - - if (terrain_y > acc.max_terrain_y) { - acc.max_terrain_y = terrain_y; - // Dominant biome/material comes from the highest column in the vertex. - acc.dominant_biome = chunk.getBiome(lx, lz); - acc.dominant_surface = chunk.getBlock(lx, terrain_u32, lz); - acc.dominant_subsurface = if (terrain_y > 0) - chunk.getBlock(lx, terrain_u32 - 1, lz) - else - acc.dominant_surface; - const foundation_y: u32 = if (terrain_u32 >= 4) terrain_u32 - 4 else 0; - acc.dominant_foundation = chunk.getBlock(lx, foundation_y, lz); - if (acc.dominant_foundation == .air) acc.dominant_foundation = .stone; - if (acc.dominant_subsurface == .air) acc.dominant_subsurface = acc.dominant_foundation; - } - - // Water above terrain. - const water_y = waterSurfaceY(chunk, lx, lz, terrain_y); - if (water_y > terrain_y) { - acc.water_count += 1; - if (water_y > acc.max_water_surface_y) acc.max_water_surface_y = water_y; - } - - if (geometry.has_overhang and geometry.overhang_max_y > acc.overhang_max_y) { - acc.has_overhang = true; - acc.overhang_min_y = geometry.overhang_min_y; - acc.overhang_max_y = geometry.overhang_max_y; - acc.dominant_overhang_block = geometry.overhang_block; - } - - // Vegetation canopy above terrain. - const scan_top: u32 = @min(CHUNK_SIZE_Y - 1, terrain_u32 + TREE_SCAN_BAND); - var tree_top: i32 = terrain_y; - var has_tree = false; - var y: u32 = terrain_u32 + 1; - while (y <= scan_top) : (y += 1) { - const block = chunk.getBlock(lx, y, lz); - if (isVegetationBlock(block)) { - has_tree = true; - const yi: i32 = @intCast(y); - if (yi > tree_top) tree_top = yi; - } - } - if (has_tree) { - acc.tree_count += 1; - acc.tree_height_sum += @intCast(tree_top - terrain_y); - } - - // Sky light sampled just above the surface. - const sample_y: u32 = @min(CHUNK_SIZE_Y - 1, terrain_u32 + 1); - acc.sky_light_sum += chunk.getSkyLight(lx, sample_y, lz); -} - -fn writeIngestedSpans( - data: *LODSimplifiedData, - gx: u32, - gz: u32, - sample: VertexSample, - water_state: LODWaterState, - lighting: LODLightingHint, - vegetation: LODVegetationHint, - color: u32, -) void { - if (!data.hasVerticalSpans()) return; - - data.clearVerticalSpans(gx, gz); - var span_index: u8 = 0; - _ = data.setVerticalSpan(gx, gz, span_index, .{ - .min_height = 0.0, - .max_height = sample.terrain_height, - .biome = sample.biome, - .material_layers = sample.layers, - .color = color, - .water = LODWaterState.empty, - .lighting = lighting, - .vegetation = vegetation, - }); - span_index += 1; - - if (water_state.is_surface and water_state.coverage > 0.0 and span_index < world_core.MAX_LOD_VERTICAL_SPANS) { - _ = data.setVerticalSpan(gx, gz, span_index, .{ - .min_height = sample.terrain_height, - .max_height = @max(water_state.surface_height, sample.terrain_height), - .biome = sample.biome, - .material_layers = .{ .surface = .water, .subsurface = .water, .foundation = sample.layers.foundation }, - .color = color, - .water = water_state, - .lighting = lighting, - .vegetation = LODVegetationHint.empty, - }); - span_index += 1; - } - - if (sample.has_overhang and sample.overhang_max > sample.overhang_min + 0.01 and span_index < world_core.MAX_LOD_VERTICAL_SPANS) { - const layers = LODMaterialLayers.default(sample.overhang_block); - _ = data.setVerticalSpan(gx, gz, span_index, LODVerticalSpan{ - .min_height = sample.overhang_min, - .max_height = sample.overhang_max, - .biome = sample.biome, - .material_layers = layers, - .color = color, - .water = LODWaterState.empty, - .lighting = lighting, - .vegetation = LODVegetationHint.empty, - }); - span_index += 1; - } - - if (vegetation.tree_coverage > 0.0 and vegetation.avg_tree_height >= 2.0 and span_index < world_core.MAX_LOD_VERTICAL_SPANS) { - const canopy_top = sample.terrain_height + vegetation.avg_tree_height; - const canopy_bottom = @max(sample.terrain_height + 1.0, canopy_top - @max(vegetation.avg_tree_height * 0.45, 2.0)); - const leaves = if (vegetation.leaves == .air) BlockType.leaves else vegetation.leaves; - _ = data.setVerticalSpan(gx, gz, span_index, LODVerticalSpan{ - .min_height = canopy_bottom, - .max_height = canopy_top, - .biome = sample.biome, - .material_layers = .{ .surface = leaves, .subsurface = leaves, .foundation = sample.layers.foundation }, - .color = color, - .water = LODWaterState.empty, - .lighting = lighting, - .vegetation = vegetation, - }); - } -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -const testing = std.testing; - -fn fillTerrainColumn(chunk: *Chunk, lx: u32, lz: u32, surface_y: u32, surface: BlockType, subsurface: BlockType, foundation: BlockType) void { - var y: u32 = 0; - while (y <= surface_y) : (y += 1) { - const block: BlockType = if (y == surface_y) - surface - else if (y + 4 > surface_y) - subsurface - else - foundation; - chunk.setBlock(lx, y, lz, block); - } -} - -test "vegetation detection includes tree variants" { - try testing.expect(isVegetationBlock(.wood)); - try testing.expect(isVegetationBlock(.leaves)); - try testing.expect(isVegetationBlock(.mangrove_log)); - try testing.expect(isVegetationBlock(.mangrove_leaves)); - try testing.expect(isVegetationBlock(.jungle_log)); - try testing.expect(isVegetationBlock(.jungle_leaves)); - try testing.expect(isVegetationBlock(.acacia_log)); - try testing.expect(isVegetationBlock(.acacia_leaves)); - try testing.expect(isVegetationBlock(.birch_log)); - try testing.expect(isVegetationBlock(.birch_leaves)); - try testing.expect(isVegetationBlock(.spruce_log)); - try testing.expect(isVegetationBlock(.spruce_leaves)); - try testing.expect(!isVegetationBlock(.stone)); - try testing.expect(!isVegetationBlock(.water)); -} - -test "downsampleChunkIntoRegion writes terrain height and biome for an overlapping cell" { - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - // Pre-seed one cell as worldgen so we can observe the provenance upgrade. - data.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - try testing.expectEqual(LODColumnProvenance.worldgen, data.getColumnProvenance(0, 0)); - - var chunk = Chunk.init(0, 0); - fillTerrainColumn(&chunk, 0, 0, 64, .grass, .dirt, .stone); - chunk.setBiome(0, 0, .forest); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod1)); - const written = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - try testing.expect(written > 0); - - try testing.expectEqual(LODColumnProvenance.chunk_derived, data.getColumnProvenance(0, 0)); - try testing.expectEqual(@as(f32, 64.0), data.getHeight(0, 0)); - try testing.expectEqual(BiomeId.forest, data.biomes[0]); -} - -test "writeIngestedColumn respects provenance authority" { - var data = try LODSimplifiedData.init(testing.allocator, .lod2); - defer data.deinit(); - - // Worldgen cell can be upgraded by chunk_derived. - data.setColumn(1, 1, 5.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0, LODWaterState.empty, LODLightingHint.daylight, LODVegetationHint.empty); - const upgraded = writeIngestedColumn(&data, 1, 1, .{ .terrain_height = 40.0, .biome = .forest }, .chunk_derived); - try testing.expect(upgraded); - try testing.expectEqual(@as(f32, 40.0), data.getHeight(1, 1)); - - // chunk_derived refresh can replace previous chunk_derived data. - const refreshed = writeIngestedColumn(&data, 1, 1, .{ .terrain_height = 45.0, .biome = .forest }, .chunk_derived); - try testing.expect(refreshed); - try testing.expectEqual(@as(f32, 45.0), data.getHeight(1, 1)); - - // chunk_derived must NOT be overwritten by worldgen. - const clobbered = writeIngestedColumn(&data, 1, 1, .{ .terrain_height = 1.0, .biome = .desert }, .worldgen); - try testing.expect(!clobbered); - try testing.expectEqual(@as(f32, 45.0), data.getHeight(1, 1)); - try testing.expectEqual(BiomeId.forest, data.biomes[1 + data.width]); - - // edited beats chunk_derived. - const edited = writeIngestedColumn(&data, 1, 1, .{ .terrain_height = 70.0, .biome = .mountains }, .edited); - try testing.expect(edited); - try testing.expectEqual(@as(f32, 70.0), data.getHeight(1, 1)); - try testing.expectEqual(LODColumnProvenance.edited, data.getColumnProvenance(1, 1)); -} - -test "downsampleChunkIntoRegion emits water state for a flooded column" { - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - - var chunk = Chunk.init(0, 0); - // Terrain at y=58, water filling up to y=63. - fillTerrainColumn(&chunk, 0, 0, 58, .sand, .sand, .stone); - var y: u32 = 59; - while (y <= 63) : (y += 1) chunk.setBlock(0, y, 0, .water); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod1)); - _ = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - - try testing.expectEqual(@as(f32, 58.0), data.getHeight(0, 0)); - try testing.expect(data.water[0].is_surface); - try testing.expectEqual(@as(f32, 63.0), data.water[0].surface_height); - try testing.expectEqual(@as(f32, 5.0), data.water[0].depth); -} - -test "writeIngestedColumn clears vegetation from flooded samples" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer data.deinit(); - - try testing.expect(writeIngestedColumn(&data, 0, 0, .{ - .terrain_height = 58.0, - .biome = .forest, - .has_water = true, - .water_surface_height = 63.0, - .water_depth = 5.0, - .water_coverage = 1.0, - .has_tree = true, - .tree_height = 8.0, - }, .chunk_derived)); - - try testing.expect(data.water[0].is_surface); - try testing.expectEqual(@as(f32, 0.0), data.vegetation[0].tree_coverage); - try testing.expectEqual(@as(u8, 2), data.verticalSpanCount(0, 0)); -} - -test "downsampleChunkIntoRegion keeps terrain surface above cave gaps" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer data.deinit(); - - var chunk = Chunk.init(0, 0); - fillTerrainColumn(&chunk, 0, 0, 64, .grass, .dirt, .stone); - var y: u32 = 50; - while (y <= 55) : (y += 1) chunk.setBlock(0, y, 0, .air); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod1)); - _ = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - - try testing.expectEqual(@as(f32, 64.0), data.getHeight(0, 0)); - try testing.expectEqual(@as(u8, 1), data.verticalSpanCount(0, 0)); - const terrain = data.getVerticalSpan(0, 0, 0) orelse return error.TestExpectedEqual; - try testing.expectEqual(@as(f32, 64.0), terrain.max_height); -} - -test "downsampleChunkIntoRegion emits a floating overhang span" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer data.deinit(); - - var chunk = Chunk.init(0, 0); - fillTerrainColumn(&chunk, 0, 0, 48, .grass, .dirt, .stone); - var y: u32 = 72; - while (y <= 80) : (y += 1) chunk.setBlock(0, y, 0, .stone); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod1)); - _ = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - - try testing.expectEqual(@as(f32, 48.0), data.getHeight(0, 0)); - try testing.expectEqual(@as(u8, 2), data.verticalSpanCount(0, 0)); - - const terrain = data.getVerticalSpan(0, 0, 0) orelse return error.TestExpectedEqual; - try testing.expectEqual(@as(f32, 0.0), terrain.min_height); - try testing.expectEqual(@as(f32, 48.0), terrain.max_height); - try testing.expectEqual(BlockType.grass, terrain.material_layers.surface); - - const overhang = data.getVerticalSpan(0, 0, 1) orelse return error.TestExpectedEqual; - try testing.expectEqual(@as(f32, 72.0), overhang.min_height); - try testing.expectEqual(@as(f32, 80.0), overhang.max_height); - try testing.expectEqual(BlockType.stone, overhang.material_layers.surface); -} - -test "downsampleChunkIntoRegion emits canopy span when budget remains" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer data.deinit(); - - var chunk = Chunk.init(0, 0); - fillTerrainColumn(&chunk, 0, 0, 64, .grass, .dirt, .stone); - var y: u32 = 68; - while (y <= 72) : (y += 1) chunk.setBlock(0, y, 0, .birch_leaves); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod1)); - _ = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - - try testing.expectEqual(@as(u8, 2), data.verticalSpanCount(0, 0)); - const canopy = data.getVerticalSpan(0, 0, 1) orelse return error.TestExpectedEqual; - try testing.expect(canopy.min_height > 64.0); - try testing.expectEqual(@as(f32, 72.0), canopy.max_height); - try testing.expectEqual(BlockType.leaves, canopy.material_layers.surface); -} - -test "downsampleChunkIntoRegion updates the correct cells at a region corner" { - // A chunk at the corner of two regions must only update cells inside its - // own region. Use LOD3 (large cells) so a chunk maps to a single vertex, - // making the boundary unambiguous. - var data = try LODSimplifiedData.init(testing.allocator, .lod3); - defer data.deinit(); - - var chunk = Chunk.init(0, 0); - fillTerrainColumn(&chunk, 0, 0, 80, .stone, .stone, .stone); - - const region_size: i32 = @intCast(world_core.regionSizeBlocks(.lod3)); - const written = downsampleChunkIntoRegion(&chunk, 0, 0, &data, 0, 0, region_size, .chunk_derived); - try testing.expect(written > 0); - - // At least one cell was upgraded to chunk_derived. - var any_upgraded = false; - for (data.provenance) |p| { - if (p == .chunk_derived) any_upgraded = true; - } - try testing.expect(any_upgraded); -} diff --git a/modules/world-lod/src/lod_manager.zig b/modules/world-lod/src/lod_manager.zig deleted file mode 100644 index a61968aa..00000000 --- a/modules/world-lod/src/lod_manager.zig +++ /dev/null @@ -1,757 +0,0 @@ -//! LOD Manager - orchestrates multi-level chunk loading for extreme render distances. -//! -//! Implements a Distant Horizons-style system where: -//! - LOD0 (0-16 chunks): Full detail, 2x2 chunks merged -//! - LOD1 (16-32 chunks): 2x simplified, 4x4 chunks merged -//! - LOD2 (32-64 chunks): 4x simplified, 8x8 chunks merged -//! - LOD3 (64-100 chunks): 8x simplified, 16x16 chunks merged, heightmap only -//! -//! Key principles: -//! - Near/fine LODs are queued first so coarse parents do not dominate mid-ground -//! - Coarse LODs remain available as fallback while finer children stream -//! - Smooth transitions via fog masking -//! -//! GPU operations are decoupled via LODGPUBridge and LODRenderInterface (Issue #246). - -const std = @import("std"); -const fs = @import("fs"); -const sync = @import("sync"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODRegionKeyContext = lod_chunk.LODRegionKeyContext; -const LODConfig = lod_chunk.LODConfig; -const ILODConfig = lod_chunk.ILODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -pub const LODStats = @import("lod_stats.zig").LODStats; -pub const LODWaitIdleReason = @import("lod_stats.zig").LODWaitIdleReason; - -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const worldToChunkFromFloat = world_core.worldToChunkFromFloat; -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; -const ChunkMesh = @import("world-meshing").ChunkMesh; -const math = @import("engine-math"); -const Vec3 = math.Vec3; -const Mat4 = math.Mat4; -const Frustum = math.Frustum; -const AABB = math.AABB; -const Vertex = @import("engine-rhi").Vertex; -const engine_core = @import("engine-core"); -const log = engine_core.log; - -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; - -const RingBuffer = engine_core.ring_buffer.RingBuffer; - -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const TextureAtlas = @import("engine-assets").TextureAtlas; - -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const lod_manager_context = @import("lod_manager_context.zig"); -const lod_manager_core = @import("lod_manager_core_ops.zig"); -const lod_manager_cache_ops = @import("lod_manager_cache_ops.zig"); -const lod_manager_ingestion_ops = @import("lod_manager_ingestion_ops.zig"); -const lod_manager_generation_ops = @import("lod_manager_generation_ops.zig"); -const lod_manager_upload_ops = @import("lod_manager_upload_ops.zig"); -const lod_manager_eviction_ops = @import("lod_manager_eviction_ops.zig"); -const lod_manager_benchmark_fixture_ops = @import("lod_manager_benchmark_fixture_ops.zig"); -const CacheIoPipeline = @import("lod_cache_io.zig").CacheIoPipeline; -const LODColumnProvenance = world_core.LODColumnProvenance; -const ChunkCoordKey = lod_manager_context.ChunkCoordKey; -const ChunkCoordKeyContext = lod_manager_context.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const PendingIngestion = lod_manager_context.PendingIngestion; -const PlayerChunkPos = lod_manager_context.PlayerChunkPos; -const LifecycleQueue = lod_manager_context.LifecycleQueue; -const LODScanState = lod_manager_context.LODScanState; -pub const ChunkResolver = lod_manager_context.ChunkResolver; -const MAX_LOD_REGIONS = lod_manager_context.MAX_LOD_REGIONS; - -/// LOD transition request -const LODTransition = struct { - region_key: LODRegionKey, - target_lod: LODLevel, - priority: i32, -}; - -pub const LODCacheStore = struct { - cache_dir_path: ?[]const u8 = null, - logged_legacy_cache_notice: bool = false, - store_size_cap_mb: u32 = lod_store.DEFAULT_STORE_SIZE_CAP_MB, - use_config_store_size_cap: bool = false, - store_mutex: sync.Mutex = .{}, -}; - -pub const LODIngestionQueue = struct { - pending_ingestions: std.ArrayListUnmanaged(PendingIngestion) = .empty, - edit_dirty: ChunkCoordSet, - mutex: sync.Mutex = .{}, - chunk_resolver: ?ChunkResolver = null, - edit_cooldown: f32 = 0.0, - drain_per_frame: u32 = 4, - - /// Creates an empty ingestion queue for chunk-derived LOD source updates. - /// The caller owns the queue and must call `deinit` with the same allocator before discarding it. - pub fn init(allocator: std.mem.Allocator) LODIngestionQueue { - return .{ .edit_dirty = ChunkCoordSet.init(allocator) }; - } - - /// Releases pending ingestion records and dirty-edit tracking owned by the queue. - /// The caller must ensure no other thread is appending to or draining this queue. - pub fn deinit(self: *LODIngestionQueue, allocator: std.mem.Allocator) void { - self.pending_ingestions.deinit(allocator); - self.edit_dirty.deinit(); - } -}; - -pub const LODMeshDisposalQueue = struct { - queue: std.ArrayListUnmanaged(*LODMesh) = .empty, - timer: f32 = 0, -}; - -pub const LODMemoryGovernor = struct { - used_bytes: usize = 0, - /// Exact known allocations plus conservative reservations for admitted - /// regions. This is the value checked before accepting new work. - logical_admission_bytes: usize = 0, - radius_shrink_chunks: [LODLevel.count]i32 = [_]i32{0} ** LODLevel.count, -}; - -pub const LODJobDispatcher = struct { - queues: [LODLevel.count]*JobQueue, - worker_pool: ?*WorkerPool = null, - next_token: u32 = 1, - stop_flag: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), -}; - -/// LOD Manager - coordinates all LOD levels. -/// Uses callback interfaces (LODGPUBridge, LODRenderInterface) for GPU operations -/// instead of a direct RHI dependency. -pub const LODManager = struct { - const Self = @This(); - - allocator: std.mem.Allocator, - config: ILODConfig, - - // Storage per LOD level (LOD0 uses existing World.chunks) - regions: [LODLevel.count]RegionMap, - - // Mesh storage per LOD level - meshes: [LODLevel.count]MeshMap, - - // LOD job queues, worker pool, tokens, and teardown signaling. - job_dispatcher: LODJobDispatcher, - - // Upload queues per LOD level - upload_queues: [LODLevel.count]RingBuffer(*LODChunk), - - // Transition queue for LOD upgrades/downgrades - transition_queue: std.ArrayListUnmanaged(LODTransition), - - // Current player position (chunk coords), read by worker threads for stale-job checks. - player_cx: std.atomic.Value(i32), - player_cz: std.atomic.Value(i32), - scan_states: [LODLevel.count]LODScanState, - - // Stats - stats: LODStats, - profiling: @import("lod_stats.zig").LODProfilingCollector, - cache_hits: u32, - cache_misses: u32, - cancelled_jobs: u32, - - // Mutex for thread safety - mutex: sync.RwLock, - - // GPU bridge for upload/destroy/sync operations (replaces direct RHI field) - gpu_bridge: LODGPUBridge, - - // Terrain generator for LOD generation (mutable for cache recentering) - generator: LODGenerator, - - atlas: *const TextureAtlas, - - // Paused state - paused: bool, - - // Memory tracking and pressure hysteresis. - memory_governor: LODMemoryGovernor, - - // Performance tracking for throttling - update_tick: u32 = 0, - - // Deferred mesh deletion queue (Vulkan optimization) - mesh_disposal: LODMeshDisposalQueue, - - // Type-erased renderer interface (replaces direct LODRenderer(RHI) field) - renderer: LODRenderInterface, - - // Optional source-data persistence store. - cache_store: LODCacheStore, - - // Dedicated, bounded single-worker cache I/O pipeline. It is intentionally - // separate from the generation pool so disk latency cannot occupy terrain - // generation workers or the update path. - cache_io: *CacheIoPipeline, - - // Keep cleanup behavior testable, but allow the live world to opt out. - cleanup_covered_regions: bool = true, - - // -- Chunk-derived LOD ingestion (issue #752 Phase 2) -- - // Pending chunk ingestions whose containing LOD region did not yet have - // source data when the chunk finished generating/loading. Drained from - // update() once the regions appear. - ingestion_queue: LODIngestionQueue, - - // Bounded, tokenized lifecycle heaps. Resident maps are consulted only - // when a token is popped; stale work is rejected by token/revision. - generation_tokens: LifecycleQueue = .{}, - transition_tokens: LifecycleQueue = .{}, - fade_tokens: LifecycleQueue = .{}, - - // Number of regions admitted to the generation/mesh/upload pipeline. This - // is maintained at lifecycle boundaries so scheduling does not need to - // recount every region map before each admission pass. - pending_region_count: usize = 0, - - /// A benchmark-only resident fixture owns the normal maps/meshes but opts - /// out of scheduler/eviction mutation for the duration of its session. - benchmark_fixture_active: bool = false, - - // Callback type to check if a regular chunk is loaded and renderable - pub const ChunkChecker = lod_gpu.ChunkChecker; - - // ---------------------------------------------------------------------- - // Chunk-derived LOD ingestion (issue #752 Phase 2) - // ---------------------------------------------------------------------- - - /// Initialize the LOD manager facade and its extracted operation state. - pub fn init(allocator: std.mem.Allocator, config: ILODConfig, gpu_bridge: LODGPUBridge, render_iface: LODRenderInterface, generator: LODGenerator, atlas: *const TextureAtlas) !*Self { - return lod_manager_core.init(allocator, config, gpu_bridge, render_iface, generator, atlas); - } - - /// Test-only lightweight manager state. Cache ownership starts disabled; - /// tests that need persistence should call enableCache() and free it after. - pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []const u8) !Self { - return lod_manager_cache_ops.initCacheTestManager(allocator, cache_dir_path); - } - - /// Stores the player's current chunk position for scheduler and stale-job checks. - /// Safe for worker reads through atomics; write from the world update thread. - pub fn storePlayerChunkPos(self: *Self, cx: i32, cz: i32) void { - return lod_manager_core.storePlayerChunkPos(self, cx, cz); - } - - /// Loads the most recently stored player chunk position. - /// Worker jobs use this snapshot to decide whether generated LOD work is still relevant. - pub fn loadPlayerChunkPos(self: *const Self) PlayerChunkPos { - return lod_manager_core.loadPlayerChunkPos(self); - } - - /// Releases LOD manager queues, caches, meshes, and worker-owned resources. - /// Call after pending LOD work has been quiesced or made safe to discard. - pub fn deinit(self: *Self) void { - return lod_manager_core.deinit(self); - } - - /// Advances LOD scheduling, ingestion, generation, uploads, transitions, cache flushes, and eviction for one frame. - /// Call from the world update thread; errors report failed queueing, mesh creation, or budget/eviction work. - pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque) !void { - return lod_manager_core.update(self, player_pos, player_velocity, chunk_checker, checker_ctx); - } - - /// Installs the bounded `gpu-culling-scale` benchmark source set through - /// the normal compact upload bridge. This is intentionally unavailable to - /// normal session code except through the benchmark fixture launcher. - pub fn installGpuCullingScaleFixture(self: *Self) !void { - return lod_manager_benchmark_fixture_ops.install(self); - } - - /// Returns a snapshot of LOD scheduling, cache, generation, upload, and renderability counters. - /// The returned data is diagnostic only and does not hold locks after the call returns. - pub fn getStats(self: *Self) LODStats { - return lod_manager_core.getStats(self); - } - - /// Returns the configured outer radius of the coarsest active LOD band. - pub fn getHorizonRenderRadius(self: *Self) i32 { - self.mutex.lockShared(); - defer self.mutex.unlockShared(); - const active_count = lod_chunk.activeLODCount(self.config); - return self.config.getRadii()[active_count - 1]; - } - - /// Returns whether the coarsest active level has produced drawable fallback - /// terrain within the current horizon. Scoping this to the player prevents - /// stale regions after a teleport from releasing foreground prefetch early. - pub fn hasRenderableCoarsestNear(self: *Self, player_cx: i32, player_cz: i32) bool { - self.mutex.lockShared(); - defer self.mutex.unlockShared(); - const active_count = lod_chunk.activeLODCount(self.config); - if (active_count == 0) return false; - const lod: LODLevel = @enumFromInt(active_count - 1); - const scale: i64 = @intCast(lod.chunksPerSide()); - const radius: i64 = self.config.getRadii()[active_count - 1]; - var it = self.regions[active_count - 1].iterator(); - while (it.next()) |entry| { - const chunk = entry.value_ptr.*; - if (chunk.getState() != .renderable) continue; - const center_x = @as(i64, chunk.region_x) * scale + @divFloor(scale, 2); - const center_z = @as(i64, chunk.region_z) * scale + @divFloor(scale, 2); - const dx = center_x - player_cx; - const dz = center_z - player_cz; - if (@as(i128, dx) * dx + @as(i128, dz) * dz <= @as(i128, radius) * radius) return true; - } - return false; - } - - /// Pauses new LOD generation and scheduling work. - /// Existing regions and meshes remain available for rendering while paused. - pub fn pause(self: *Self) void { - return lod_manager_core.pause(self); - } - - /// Resumes LOD scheduling after a pause. - /// Pending queues may be processed by subsequent `update` calls. - pub fn unpause(self: *Self) void { - return lod_manager_core.unpause(self); - } - - /// Selects the configured LOD level for a chunk coordinate relative to the stored player position. - /// This is a read-only policy query used by streaming and debug code. - pub fn getLODForDistance(self: *const Self, chunk_x: i32, chunk_z: i32) LODLevel { - return lod_manager_core.getLODForDistance(self, chunk_x, chunk_z); - } - - /// Returns whether a chunk coordinate falls inside the configured LOD horizon. - /// This is a read-only culling/scheduling query based on the current player chunk position. - pub fn isInRange(self: *const Self, chunk_x: i32, chunk_z: i32) bool { - return lod_manager_core.isInRange(self, chunk_x, chunk_z); - } - - /// Renders currently renderable LOD regions through the configured renderer. - /// Generation and upload queues are not drained by this call. - pub fn render(self: *Self, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, use_frustum: bool, max_distance_chunks: ?i32, layer: LODRenderLayer) void { - return lod_manager_core.render(self, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer); - } - - /// Renders a frame-aware LOD layer. The monotonic WorldRenderer serial - /// allows terrain and water to share one visibility projection. - pub fn renderFrame(self: *Self, frame_serial: u64, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, use_frustum: bool, max_distance_chunks: ?i32, detail_render_radius: i32, layer: LODRenderLayer) void { - return lod_manager_core.renderFrame(self, frame_serial, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, layer); - } - - /// Prepares same-frame GPU LOD culling before active graphics passes. - pub fn prepareFrame(self: *Self, frame_serial: u64, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, max_distance_chunks: ?i32, detail_render_radius: i32) void { - return lod_manager_core.prepareFrame(self, frame_serial, view_proj, camera_pos, chunk_checker, checker_ctx, max_distance_chunks, detail_render_radius); - } - - /// Enables persistent source-data caching for LOD regions below `save_dir_path`. - /// Allocates and stores the cache path; errors indicate allocation or filesystem setup failure. - pub fn enableCache(self: *Self, save_dir_path: []const u8) !void { - return lod_manager_cache_ops.enableCache(self, save_dir_path); - } - - /// Writes dirty cached source-data containers to persistent storage. - /// Intended for the world update/shutdown path; IO failures are logged and leave data eligible for retry. - pub fn flushDirtyStores(self: *Self) void { - return lod_manager_cache_ops.flushDirtyStores(self); - } - - /// Settles older writes, then queues and waits for every current dirty - /// source snapshot. Used by explicit save points. - pub fn flushDirtyStoresNow(self: *Self) void { - return lod_manager_cache_ops.flushDirtyStoresNow(self); - } - - /// Deletes settled cache payloads for edited source updates that are still - /// blocked on missing or in-flight regions. The pending ingestion remains - /// queued and will write a fresh snapshot after it can be applied. - pub fn invalidatePendingEditedStoresNow(self: *Self) void { - return lod_manager_cache_ops.invalidatePendingEditedStoresNow(self); - } - - /// Flushes completed cache IO work and applies read/write completions. - /// Call from the main thread when synchronous cache progress is required. - pub fn flushCacheIO(self: *Self) void { - return lod_manager_cache_ops.flushCacheIO(self); - } - - /// Applies completed cache reads/writes without waiting for worker I/O. - pub fn drainCacheCompletions(self: *Self) void { - return lod_manager_cache_ops.drainCacheCompletions(self); - } - - /// Shutdown-only cache flush. This may block until the bounded worker has - /// serialized and persisted accepted snapshots. - pub fn shutdownCacheIO(self: *Self) void { - return lod_manager_cache_ops.shutdownCacheIO(self); - } - - /// Builds the persistent cache key for an LOD region using generator identity and LOD coordinates. - /// The returned key is deterministic for the same generator seed/version and region key. - pub fn cacheKey(self: *const Self, key: LODRegionKey) lod_cache.Key { - return lod_manager_cache_ops.cacheKey(self, key); - } - - /// Constructs the legacy single-region cache file path for migration or cleanup. - /// The returned path is heap allocated and must be freed by the caller. - pub fn legacyCacheFilePath(self: *Self, save_dir_path: []const u8, key: lod_cache.Key) ![]u8 { - return lod_manager_cache_ops.legacyCacheFilePath(self, save_dir_path, key); - } - - /// Logs the one-time notice that legacy LOD cache data was detected. - /// This mutates only the cache-store diagnostic flag to avoid repeated log spam. - pub fn logLegacyCacheNotice(self: *Self) void { - return lod_manager_cache_ops.logLegacyCacheNotice(self); - } - - /// Returns a heap-owned snapshot of the configured cache directory path. - /// Returns `null` when caching is disabled or allocation fails. - pub fn cacheDirPathSnapshot(self: *Self) ?[]u8 { - return lod_manager_cache_ops.cacheDirPathSnapshot(self); - } - - /// Reports whether persistent LOD source-data caching is currently configured. - /// This is a read-only query protected by the cache-store synchronization. - pub fn cacheEnabled(self: *Self) bool { - return lod_manager_cache_ops.cacheEnabled(self); - } - - /// Reads a serialized LOD cache payload from the container store. - /// Returns `null` for cache misses; errors report filesystem or allocation failures. - pub fn readStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key) !?[]u8 { - return lod_manager_cache_ops.readStorePayload(self, save_dir_path, cache_key); - } - - /// Writes a serialized LOD cache payload to the container store. - /// Errors report filesystem, allocation, or store-encoding failures. - pub fn writeStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key, bytes: []const u8) !void { - return lod_manager_cache_ops.writeStorePayload(self, save_dir_path, cache_key, bytes); - } - - /// Deletes one payload from the LOD cache store if it exists. - /// Missing payloads are ignored because cache deletion is best-effort cleanup. - pub fn deleteStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key) void { - return lod_manager_cache_ops.deleteStorePayload(self, save_dir_path, cache_key); - } - - /// Deletes an entire LOD cache store container path. - /// Used by cleanup and migration paths; failures are intentionally non-fatal. - pub fn deleteStoreContainer(self: *Self, path: []const u8) void { - return lod_manager_cache_ops.deleteStoreContainer(self, path); - } - - /// Loads simplified source data for an LOD region from cache when available and valid. - /// Returns `null` for misses, invalid payloads, disabled cache, or generator-version mismatch. - pub fn loadCachedSourceData(self: *Self, key: LODRegionKey) ?LODSimplifiedData { - return lod_manager_cache_ops.loadCachedSourceData(self, key); - } - - /// Increments cache-hit counters used by LOD diagnostics. - /// This does not mutate cached data or scheduling state. - pub fn recordCacheHit(self: *Self) void { - return lod_manager_cache_ops.recordCacheHit(self); - } - - /// Increments cache-miss counters used by LOD diagnostics. - /// This is called when no reusable source data was found for a region. - pub fn recordCacheMiss(self: *Self) void { - return lod_manager_cache_ops.recordCacheMiss(self); - } - - /// Serializes and stores simplified source data for a completed LOD region. - /// The input data is borrowed for the call; persistence failures are logged for later diagnosis. - pub fn saveCachedSourceData(self: *Self, key: LODRegionKey, data: *const LODSimplifiedData) void { - return lod_manager_cache_ops.saveCachedSourceData(self, key, data); - } - - /// Dispatches a generation fallback after an asynchronous cache miss. - /// Cache completion processing is the only caller; it never performs I/O. - pub fn dispatchCacheMiss(self: *Self, key: LODRegionKey, token: u32) void { - return lod_manager_generation_ops.dispatchCacheMiss(self, key, token); - } - - /// Installs the callback used to resolve full-detail chunks for ingestion retries. - /// Call from the world update thread before requesting deferred ingestion. - pub fn setChunkResolver(self: *Self, resolver: ChunkResolver) void { - return lod_manager_ingestion_ops.setChunkResolver(self, resolver); - } - - /// Folds one full-detail chunk into every matching LOD region's simplified source data. - /// Marks affected source data dirty so meshes and cache payloads can be refreshed. - pub fn ingestChunk(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, provenance: LODColumnProvenance) void { - // The scale fixture's source set is immutable for a matched CPU/GPU - // run. Full-detail streamer arrivals must not remesh a fixture tile. - if (self.benchmark_fixture_active) return; - return lod_manager_ingestion_ops.ingestChunk(self, cx, cz, chunk, provenance); - } - - /// Queues ingestion for a chunk whose target LOD regions may not exist yet. - /// The request is retried by later update ticks until it applies or manager teardown. - pub fn requestIngestion(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance) void { - if (self.benchmark_fixture_active) return; - return lod_manager_ingestion_ops.requestIngestion(self, cx, cz, provenance); - } - - /// Marks a full-detail chunk as edited so its containing LOD regions can be refreshed. - /// The edit is debounced and flushed into ingestion work from the update thread. - pub fn markChunkEdited(self: *Self, cx: i32, cz: i32) void { - if (self.benchmark_fixture_active) return; - return lod_manager_ingestion_ops.markChunkEdited(self, cx, cz); - } - - /// Applies queued edit provenance before full-detail storage unloads the - /// resolver-owned chunk. Returns the LOD-level mask still waiting for a - /// source region and optionally retains that work for a later retry. - pub fn flushEditedChunkForUnload(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, retain_pending: bool) u8 { - if (self.benchmark_fixture_active) return 0; - return lod_manager_ingestion_ops.flushEditedChunkForUnload(self, cx, cz, chunk, retain_pending); - } - - /// Applies chunk-derived source samples to currently loaded LOD regions. - /// Returns a bitmask describing which LOD levels still need the update. - pub fn applyIngestionToRegions(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, provenance: LODColumnProvenance) u8 { - return lod_manager_ingestion_ops.applyIngestionToRegions(self, cx, cz, chunk, provenance); - } - - /// Records a deferred ingestion request while the ingestion mutex is already held. - /// `mask` tracks which LOD levels still need the chunk once regions become - /// available. Returns false when bounded queue admission fails. - pub fn recordPendingLocked(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance, mask: u8) bool { - return lod_manager_ingestion_ops.recordPendingLocked(self, cx, cz, provenance, mask); - } - - /// Refreshes an existing deferred ingestion request with a new mask. The - /// legacy `ttl` argument is ignored; work remains durable until it applies. - pub fn rerecordPending(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance, mask: u8, ttl: u16) void { - return lod_manager_ingestion_ops.rerecordPending(self, cx, cz, provenance, mask, ttl); - } - - /// Removes completed pending-ingestion records while the ingestion mutex is held. - /// Unresolved requests remain durable; queue capacity bounds memory usage. - pub fn decayPendingLocked(self: *Self) void { - return lod_manager_ingestion_ops.decayPendingLocked(self); - } - - /// Resolves and applies a bounded number of deferred ingestion requests for this frame. - /// Uses the installed chunk resolver and leaves unresolved requests queued for later ticks. - pub fn drainPendingIngestions(self: *Self) void { - return lod_manager_ingestion_ops.drainPendingIngestions(self); - } - - /// Immediately attempts every currently deferred ingestion once. Entries - /// that are still unavailable remain queued for later update ticks. - pub fn drainPendingIngestionsNow(self: *Self) void { - return lod_manager_ingestion_ops.drainPendingIngestionsNow(self); - } - - /// Converts debounced edited-chunk coordinates into ingestion requests. - /// Clears the dirty-edit set once requests have been queued or applied. - pub fn flushEditedChunks(self: *Self) void { - return lod_manager_ingestion_ops.flushEditedChunks(self); - } - - /// Immediately applies pending edited chunks without waiting for the - /// coalescing cooldown. Used by explicit save points. - pub fn flushEditedChunksNow(self: *Self) void { - return lod_manager_ingestion_ops.flushEditedChunksNow(self); - } - - /// Immediately consumes at most the ordinary per-frame edit budget. - /// Intended for autosave paths that must not synchronously drain all edits. - pub fn flushEditedChunksBounded(self: *Self) void { - return lod_manager_ingestion_ops.flushEditedChunksBounded(self); - } - - /// Queues missing or dirty regions for generation at one LOD level. - /// Errors report allocation or job-queue failures; call from the world update thread. - pub fn queueLODRegions(self: *Self, lod: LODLevel, velocity: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque) !void { - return lod_manager_generation_ops.queueLODRegions(self, lod, velocity, chunk_checker, checker_ctx); - } - - /// Drains completed generation jobs and schedules mesh work for accepted source data. - /// Errors report mesh allocation or queueing failures. - pub fn processQueuedGenerations(self: *Self, velocity: Vec3) !void { - return lod_manager_generation_ops.processQueuedGenerations(self, velocity); - } - - /// Advances LOD region lifecycle transitions such as generated-to-meshing and meshing-to-uploading. - /// Errors report failed mesh creation or upload queue insertion. - pub fn processStateTransitions(self: *Self, velocity: Vec3) !void { - return lod_manager_generation_ops.processStateTransitions(self, velocity); - } - - /// Adds a revision-checked mesh or upload lifecycle token. Used when - /// workers and ingestion change state without a resident-map sweep. - pub fn enqueueTransition(self: *Self, key: LODRegionKey, chunk: *const LODChunk, stage: lod_manager_context.LifecycleStage) void { - return lod_manager_generation_ops.enqueueTransition(self, key, chunk, stage); - } - - /// Schedules bounded fade decay only for regions actively transitioning. - pub fn enqueueFade(self: *Self, key: LODRegionKey, chunk: *const LODChunk) void { - return lod_manager_generation_ops.enqueueFade(self, key, chunk); - } - - /// Returns the mesh object for a region, allocating it when absent. - /// The returned mesh is manager-owned and remains valid until eviction or manager teardown. - pub fn getOrCreateMesh(self: *Self, key: LODRegionKey) !*LODMesh { - return lod_manager_generation_ops.getOrCreateMesh(self, key); - } - - /// Builds CPU mesh data for one LOD chunk from its current source data. - /// Errors report allocation or meshing failures; successful builds enqueue upload work. - pub fn buildMeshForChunk(self: *Self, chunk: *LODChunk) !void { - return lod_manager_generation_ops.buildMeshForChunk(self, chunk); - } - - /// Selects the mesh-building path to use for a particular LOD level. - /// The result combines global config with per-level fallbacks for performance and quality. - pub fn effectiveMeshPath(self: *Self, lod: LODLevel) lod_chunk.LODMeshPath { - return lod_manager_generation_ops.effectiveMeshPath(self, lod); - } - - /// Uploads queued LOD meshes using the configured per-frame upload budget. - /// Must run on the render/update thread that owns the GPU bridge. - pub fn processUploads(self: *Self) void { - return lod_manager_upload_ops.processUploads(self); - } - - /// Uploads queued LOD meshes until `upload_budget_bytes` is exhausted. - /// Chunks that cannot be uploaded within the budget are left queued for later frames. - pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { - return lod_manager_upload_ops.processUploadsWithBudget(self, upload_budget_bytes); - } - - /// Recovers compact draw failures on the update thread through the normal - /// CPU mesh and bounded upload path. - pub fn recoverCompactDrawFailures(self: *Self) void { - return lod_manager_upload_ops.recoverCompactDrawFailures(self); - } - - /// Explicitly records a device-wide LOD wait. Routine upload, eviction, - /// and pool maintenance must use frame-safe retirement instead. - pub fn waitIdleTracked(self: *Self, reason: LODWaitIdleReason) void { - self.gpu_bridge.waitIdleTracked(&self.profiling, reason); - } - - /// Places an LOD chunk back onto an upload queue after a deferred or failed upload attempt. - /// The chunk remains manager-owned and must stay pinned as required by the caller path. - pub fn requeueUpload(self: *Self, lod_idx: usize, chunk: *LODChunk) void { - return lod_manager_upload_ops.requeueUpload(self, lod_idx, chunk); - } - - /// Replaces a failed compact upload with the maintained CPU heightfield. - pub fn fallbackCompactMeshToCpu(self: *Self, mesh: *LODMesh, chunk: *LODChunk) !void { - return lod_manager_generation_ops.fallbackCompactMeshToCpu(self, mesh, chunk); - } - - /// Counts direct finer child regions that are currently renderable for a parent region. - /// Used to decide parent fallback visibility and transition fades. - pub fn countRenderableChildren(self: *Self, key: LODRegionKey) u8 { - return lod_manager_upload_ops.countRenderableChildren(self, key); - } - - /// Reports whether a region should contribute visible geometry in the current hierarchy state. - /// Excludes empty, covered, or non-renderable regions that should not affect parent readiness. - pub fn regionContributesGeometry(self: *Self, key: LODRegionKey, chunk: *const LODChunk) bool { - return lod_manager_upload_ops.regionContributesGeometry(self, key, chunk); - } - - /// Adjusts a parent region's renderable-child count after child visibility changes. - /// This may start or cancel transition fades used to hide parent fallback terrain. - pub fn adjustParentReadyChildren(self: *Self, key: LODRegionKey, delta: i8) void { - return lod_manager_upload_ops.adjustParentReadyChildren(self, key, delta); - } - - /// Marks a region renderable after its mesh upload completes. - /// Updates parent readiness and transition state so hierarchy blending remains consistent. - pub fn markRegionRenderable(self: *Self, key: LODRegionKey, chunk: *LODChunk) void { - return lod_manager_upload_ops.markRegionRenderable(self, key, chunk); - } - - /// Decrements active transition fade counters for renderable regions. - /// Call once per update tick before rendering fade-dependent LOD hierarchy state. - pub fn decayTransitionFrames(self: *Self) void { - return lod_manager_upload_ops.decayTransitionFrames(self); - } - - /// Updates hierarchy bookkeeping when a region is removed or evicted. - /// Parent ready-child counts are reduced if the removed region contributed geometry. - pub fn noteRegionRemoved(self: *Self, key: LODRegionKey, chunk: *const LODChunk) void { - return lod_manager_upload_ops.noteRegionRemoved(self, key, chunk); - } - - /// Demotes a renderable region back to mesh/upload work after its source data changes. - /// Parent readiness is updated before the region leaves the renderable set. - pub fn demoteRegionForRemesh(self: *Self, key: LODRegionKey, chunk: *LODChunk) void { - return lod_manager_upload_ops.demoteRegionForRemesh(self, key, chunk); - } - - /// Evicts LOD regions outside their configured radii and queues their meshes for destruction. - /// Errors report allocation or bookkeeping failures during eviction. - pub fn unloadDistantRegions(self: *Self) !void { - return lod_manager_eviction_ops.unloadDistantRegions(self); - } - - /// Evicts regions for one LOD level beyond `max_radius` chunks from the player. - /// Pinned or in-flight regions are preserved until background work releases them. - pub fn unloadDistantForLevel(self: *Self, lod: LODLevel, max_radius: i32) !void { - return lod_manager_eviction_ops.unloadDistantForLevel(self, lod, max_radius); - } - - /// Defers GPU mesh destruction so the backend is not forced to free resources mid-frame. - /// The mesh pointer must remain valid until `processMeshDeletions` consumes it. - pub fn queueMeshDeletion(self: *Self, mesh: *LODMesh) void { - return lod_manager_eviction_ops.queueMeshDeletion(self, mesh); - } - - /// Destroys up to `max_count` deferred LOD meshes through the GPU bridge. - /// Intended to amortize resource deletion across frames. - pub fn processMeshDeletions(self: *Self, max_count: usize) void { - return lod_manager_eviction_ops.processMeshDeletions(self, max_count); - } - - /// Enforces the configured LOD memory budget by shrinking radii or evicting regions. - /// Errors report eviction failures; call from the world update thread. - pub fn enforceMemoryBudget(self: *Self) !void { - return lod_manager_eviction_ops.enforceMemoryBudget(self); - } - - /// Recomputes diagnostic stats from current LOD maps, queues, cache counters, and memory state. - /// This mutates only the stats snapshot exposed by `getStats`. - pub fn updateStats(self: *Self) void { - return lod_manager_eviction_ops.updateStats(self); - } - - /// Removes LOD fallback regions fully covered by loaded full-detail chunks. - /// Uses `checker` to avoid drawing duplicate distant terrain inside the high-detail area. - pub fn unloadLODWhereChunksLoaded(self: *Self, checker: ChunkChecker, ctx: *anyopaque) void { - return lod_manager_eviction_ops.unloadLODWhereChunksLoaded(self, checker, ctx); - } - - /// Tests whether every full-detail chunk inside `bounds` is loaded and renderable. - /// Used before unloading an overlapping LOD region to prevent visible holes. - pub fn areAllChunksLoaded(self: *Self, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool { - return lod_manager_eviction_ops.areAllChunksLoaded(self, bounds, checker, ctx); - } -}; diff --git a/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig b/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig deleted file mode 100644 index 4c66fa66..00000000 --- a/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig +++ /dev/null @@ -1,131 +0,0 @@ -//! Bounded, renderer-backed LOD fixture used only by the GPU-culling benchmark. -//! -//! This deliberately installs normal manager-owned regions and compact meshes; -//! it never writes renderer telemetry or culling counters. Keeping it here -//! makes the production ownership path (maps -> bridge -> compact pool -> -//! renderer) the only route to a ready fixture region. -const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; -const lod_chunk = @import("lod_chunk.zig"); -const LODChunk = lod_chunk.LODChunk; -const LODLevel = lod_chunk.LODLevel; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const LODMesh = @import("lod_mesh.zig").LODMesh; - -pub const NAME = "gpu-culling-scale"; -pub const REGION_COUNT: usize = 1024; -pub const GRID_SIDE: i32 = 32; -pub const REGION_STRIDE: i32 = 1; -pub const FIXTURE_LOD: LODLevel = .lod4; -pub const SOURCE_DENSITY: f32 = 0.25; -pub const MIN_HORIZON_CHUNKS: i32 = 4096; - -comptime { - std.debug.assert(REGION_COUNT == @as(usize, @intCast(GRID_SIDE * GRID_SIDE))); -} - -/// Returns one deterministic, contiguous same-level region in a 32x32 lattice. -/// The negative half is intentional: it verifies floor-based region ownership -/// and keeps the fixed source set centered around the benchmark camera path. -pub fn regionKey(index: usize) LODRegionKey { - std.debug.assert(index < REGION_COUNT); - const x: i32 = @intCast(index % @as(usize, @intCast(GRID_SIDE))); - const z: i32 = @intCast(index / @as(usize, @intCast(GRID_SIDE))); - return .{ - .rx = (x - GRID_SIDE / 2) * REGION_STRIDE, - .rz = (z - GRID_SIDE / 2) * REGION_STRIDE, - .lod = FIXTURE_LOD, - }; -} - -/// Installs exactly `REGION_COUNT` non-overlapping LOD4 regions through the -/// production compact upload bridge. There are intentionally no finer regions -/// or parents: LOD4 has no parent fallback and same-level contiguous regions retain -/// CPU hierarchy/coverage authority without a synthetic visibility shortcut. -pub fn install(self: *Self) !void { - if (self.benchmark_fixture_active) return error.BenchmarkFixtureAlreadyInstalled; - if (self.config.getRadii()[@intFromEnum(FIXTURE_LOD)] < MIN_HORIZON_CHUNKS) return error.BenchmarkFixtureHorizonTooSmall; - - for (0..REGION_COUNT) |index| { - const key = regionKey(index); - var source = try LODSimplifiedData.initWithSampleDensity(self.allocator, FIXTURE_LOD, SOURCE_DENSITY); - var source_transferred = false; - errdefer if (!source_transferred) source.deinit(); - fillRenderableTerrain(&source, key); - - const chunk = try self.allocator.create(LODChunk); - errdefer self.allocator.destroy(chunk); - chunk.* = LODChunk.init(key.rx, key.rz, FIXTURE_LOD); - chunk.data = .{ .simplified = source }; - source_transferred = true; - errdefer chunk.deinit(self.allocator); - - const mesh = try self.allocator.create(LODMesh); - errdefer self.allocator.destroy(mesh); - mesh.* = LODMesh.init(self.allocator, FIXTURE_LOD); - errdefer mesh.releasePendingCompactTile(); - try mesh.buildCompactTile(simplifiedData(chunk)); - try self.gpu_bridge.upload(mesh); - errdefer self.gpu_bridge.destroy(mesh); - - self.mutex.lock(); - const regions = &self.regions[@intFromEnum(FIXTURE_LOD)]; - const meshes = &self.meshes[@intFromEnum(FIXTURE_LOD)]; - if (regions.contains(key) or meshes.contains(key)) { - self.mutex.unlock(); - return error.BenchmarkFixtureKeyCollision; - } - regions.put(key, chunk) catch |err| { - self.mutex.unlock(); - return err; - }; - meshes.put(key, mesh) catch |err| { - _ = regions.remove(key); - self.mutex.unlock(); - return err; - }; - // The bridge succeeded, so this is the normal published renderable - // state. LOD4 has no parent to update and no fabricated child count. - chunk.markRenderable(0); - self.mutex.unlock(); - } - self.benchmark_fixture_active = true; - self.updateStats(); -} - -fn simplifiedData(chunk: *LODChunk) *const LODSimplifiedData { - return switch (chunk.data) { - .simplified => |*data| data, - else => unreachable, - }; -} - -/// Populate valid, supported source columns. A gentle deterministic slope -/// makes each tile renderable without invoking world generation or injecting a -/// draw/candidate counter. -fn fillRenderableTerrain(data: *LODSimplifiedData, key: LODRegionKey) void { - var z: u32 = 0; - while (z < data.width) : (z += 1) { - var x: u32 = 0; - while (x < data.width) : (x += 1) { - const wave: i32 = @mod(key.rx * 3 + key.rz * 5 + @as(i32, @intCast(x)) + @as(i32, @intCast(z)), 7); - data.setColumn(x, z, @as(f32, @floatFromInt(72 + wave)), .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x0068_a84f, .empty, .daylight, .empty); - } - } -} - -test "gpu culling scale fixture has exactly spaced negative and positive LOD4 keys" { - try std.testing.expectEqual(REGION_COUNT, @as(usize, @intCast(GRID_SIDE * GRID_SIDE))); - const first = regionKey(0); - const next = regionKey(1); - const next_row = regionKey(@as(usize, @intCast(GRID_SIDE))); - const last = regionKey(REGION_COUNT - 1); - try std.testing.expectEqual(FIXTURE_LOD, first.lod); - try std.testing.expectEqual(-16, first.rx); - try std.testing.expectEqual(-16, first.rz); - try std.testing.expectEqual(REGION_STRIDE, next.rx - first.rx); - try std.testing.expectEqual(REGION_STRIDE, next_row.rz - first.rz); - try std.testing.expectEqual(15, last.rx); - try std.testing.expectEqual(15, last.rz); -} diff --git a/modules/world-lod/src/lod_manager_cache_ops.zig b/modules/world-lod/src/lod_manager_cache_ops.zig deleted file mode 100644 index fbff8e81..00000000 --- a/modules/world-lod/src/lod_manager_cache_ops.zig +++ /dev/null @@ -1,439 +0,0 @@ -const std = @import("std"); -const fs = @import("fs"); -const Self = @import("lod_manager.zig").LODManager; -const LODRegionKey = @import("lod_chunk.zig").LODRegionKey; -const LODRegionKeyContext = @import("lod_chunk.zig").LODRegionKeyContext; -const LODSimplifiedData = @import("lod_chunk.zig").LODSimplifiedData; -const lod_chunk = @import("lod_chunk.zig"); -const manager_ctx = @import("lod_manager_context.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const cache_io = @import("lod_cache_io.zig"); -const log = @import("engine-core").log; - -/// Cache setup is an explicit world-load operation. It may perform synchronous -/// filesystem maintenance, unlike the frame/update path. -pub fn enableCache(self: *Self, save_dir_path: []const u8) !void { - self.flushCacheIO(); - const cache_dir_path = try self.allocator.dupe(u8, save_dir_path); - errdefer self.allocator.free(cache_dir_path); - - const live_header = lod_store.StoreHeader{ - .seed = self.generator.seed, - .generator_identity_hash = self.generator.identity_hash, - .generator_version = self.generator.version, - }; - if (try lod_store.readHeader(self.allocator, save_dir_path)) |stored_header| { - if (stored_header.seed != live_header.seed or - stored_header.lod_data_version != live_header.lod_data_version or - stored_header.generator_identity_hash != live_header.generator_identity_hash or - stored_header.generator_version != live_header.generator_version) - { - log.log.warn("LOD store identity changed; discarding stale LOD source store", .{}); - try lod_store.deleteStore(self.allocator, save_dir_path); - } - } - try lod_store.writeHeader(self.allocator, save_dir_path, live_header); - - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.cache_store.cache_dir_path) |old_path| self.allocator.free(old_path); - self.cache_store.cache_dir_path = cache_dir_path; - self.cache_store.logged_legacy_cache_notice = false; - log.log.info("LOD source store enabled at '{s}/lod'", .{cache_dir_path}); -} - -/// Enqueues at most one source-data snapshot. Cloning is intentionally the -/// only update-thread cache work; serialization and filesystem writes run on -/// CacheIoPipeline's dedicated worker. -pub fn flushDirtyStores(self: *Self) void { - _ = queueDirtyStores(self, 1); -} - -/// Settles older writes, then drains every currently eligible dirty source -/// snapshot in bounded batches. Explicit save points use this so an edited -/// snapshot cannot be skipped behind a stale worldgen write. -pub fn flushDirtyStoresNow(self: *Self) void { - flushAllDirtyStores(self); -} - -/// Removes known-stale payloads for edited ingestions that could not be -/// applied synchronously. Call only after `flushDirtyStoresNow`, which settles -/// older asynchronous writes before these payloads are deleted. -pub fn invalidatePendingEditedStoresNow(self: *Self) void { - const path = self.cacheDirPathSnapshot() orelse return; - defer self.allocator.free(path); - - var stale_keys = std.HashMap(LODRegionKey, void, LODRegionKeyContext, std.hash_map.default_max_load_percentage).init(self.allocator); - defer stale_keys.deinit(); - - self.ingestion_queue.mutex.lock(); - for (self.ingestion_queue.pending_ingestions.items) |pending| { - if (pending.provenance != .edited) continue; - var level: usize = 1; - while (level < lod_chunk.LODLevel.count) : (level += 1) { - const level_mask = @as(u8, 1) << @intCast(level); - if (pending.pending_levels & level_mask == 0) continue; - const lod: lod_chunk.LODLevel = @enumFromInt(@as(u3, @intCast(level))); - stale_keys.put(LODRegionKey.fromChunkCoords(pending.cx, pending.cz, lod), {}) catch { - log.log.warn("Failed to track stale LOD{} store payload for invalidation", .{level}); - }; - } - } - // Queue saturation can leave an edited coordinate in `edit_dirty` rather - // than `pending_ingestions`. Its full active LOD ladder is equally stale. - var dirty_iter = self.ingestion_queue.edit_dirty.keyIterator(); - while (dirty_iter.next()) |dirty| { - var level: usize = 1; - while (level < lod_chunk.activeLODCount(self.config)) : (level += 1) { - const lod: lod_chunk.LODLevel = @enumFromInt(@as(u3, @intCast(level))); - stale_keys.put(LODRegionKey.fromChunkCoords(dirty.cx, dirty.cz, lod), {}) catch { - log.log.warn("Failed to track dirty LOD{} store payload for invalidation", .{level}); - }; - } - } - self.ingestion_queue.mutex.unlock(); - - self.cache_store.store_mutex.lock(); - defer self.cache_store.store_mutex.unlock(); - var iter = stale_keys.keyIterator(); - while (iter.next()) |key| { - const cache_key = self.cacheKey(key.*); - lod_store.deletePayload(self.allocator, path, cache_key); - const legacy_path = self.legacyCacheFilePath(path, cache_key) catch continue; - fs.cwd().deleteFile(legacy_path) catch |err| { - if (err != error.FileNotFound) log.log.warn("Failed to invalidate stale legacy LOD cache '{s}': {}", .{ legacy_path, err }); - }; - self.allocator.free(legacy_path); - } -} - -pub fn flushCacheIO(self: *Self) void { - self.cache_io.waitUntilIdle(); - drainCacheCompletions(self); -} - -/// Deinit-only flushing. Accepted work may block here; normal updates must use -/// `flushDirtyStores` and never wait for I/O. -pub fn shutdownCacheIO(self: *Self) void { - flushAllDirtyStores(self); -} - -fn flushAllDirtyStores(self: *Self) void { - // An older write can occupy a region's queued flag. Apply its completion - // before scanning, otherwise a stale completion may hide the newer dirty - // revision from the first (and only) batch. - self.flushCacheIO(); - - var attempts: usize = 0; - while (attempts < 2048) : (attempts += 1) { - const queued = queueDirtyStores(self, cache_io.MAX_PENDING_TASKS); - if (queued == 0) break; - self.flushCacheIO(); - } - self.flushCacheIO(); - - if (attempts == 2048) { - log.log.warn("LOD source-store flush stopped after {} batches; dirty snapshots remain eligible for retry", .{attempts}); - } -} - -pub fn drainCacheCompletions(self: *Self) void { - var completions: std.ArrayListUnmanaged(cache_io.Completion) = .empty; - defer { - for (completions.items) |*completion| deinitCompletion(completion); - completions.deinit(self.allocator); - } - self.cache_io.drainCompletions(&completions); - - for (completions.items) |*completion| switch (completion.*) { - .read => |*read| { - var dispatch_generation = false; - var log_legacy_notice = false; - self.mutex.lock(); - const chunk = self.regions[@intFromEnum(read.key.lod)].get(read.key); - if (chunk) |region| { - if (region.cache_read_queued and region.job_token == read.token and region.getState() == .queued_for_generation) { - region.cache_read_queued = false; - log_legacy_notice = read.used_legacy; - switch (read.result) { - .hit => |*data| { - if ((regionRequiresSpans(self, read.key) and !data.hasVerticalSpans()) or - data.width != LODSimplifiedData.getGridSizeForDensity(read.key.lod, self.config.getSampleDensity(read.key.lod))) - { - data.deinit(); - read.result = .miss; - self.cache_misses += 1; - dispatch_generation = true; - } else { - region.data = .{ .simplified = data.* }; - // Ownership moved into the region. Retag the - // completion so deferred cleanup does not - // deinitialize the consumed payload. - read.result = .miss; - region.updateHeightBoundsFromData(); - region.bumpSourceRevision(); - region.setState(.generated); - self.enqueueTransition(read.key, region, .mesh); - self.cache_hits += 1; - } - }, - .miss => { - self.cache_misses += 1; - dispatch_generation = true; - }, - } - } - } - self.mutex.unlock(); - if (log_legacy_notice) self.logLegacyCacheNotice(); - if (dispatch_generation) self.dispatchCacheMiss(read.key, read.token); - }, - .write => |write| { - // Explicit saveCachedSourceData requests are not tied to a live - // region lifecycle and use revision zero. - if (write.revision == 0) continue; - self.mutex.lock(); - if (self.regions[@intFromEnum(write.key.lod)].get(write.key)) |region| { - if (region.store_write_queued and region.source_revision == write.revision) { - region.store_write_queued = false; - region.store_dirty = !write.success; - region.store_size_limited = write.size_limited; - region.store_size_limit_cap_mb = if (write.size_limited) write.store_size_cap_mb else 0; - } else if (region.store_write_queued) { - // A newer source snapshot arrived while this write was in - // flight. Preserve dirty state and let a later update - // enqueue that newer revision. - region.store_write_queued = false; - } - } - self.mutex.unlock(); - }, - }; -} - -fn queueDirtyStores(self: *Self, max_count: usize) usize { - const path = self.cacheDirPathSnapshot() orelse return 0; - defer self.allocator.free(path); - var queued: usize = 0; - self.mutex.lock(); - defer self.mutex.unlock(); - const active = lod_chunk.activeLODCount(self.config); - var level: usize = 1; - while (level < active and queued < max_count) : (level += 1) { - var iter = self.regions[level].iterator(); - while (iter.next()) |entry| { - if (queued >= max_count) break; - const region = entry.value_ptr.*; - if (!region.store_dirty or region.store_write_queued) continue; - const store_size_cap_mb = if (self.cache_store.use_config_store_size_cap) self.config.getLODStoreSizeCapMB() else self.cache_store.store_size_cap_mb; - if (region.store_size_limited and store_size_cap_mb <= region.store_size_limit_cap_mb) continue; - const snapshot = switch (region.data) { - .simplified => |*data| lod_cache.cloneSourceData(data, entry.key_ptr.*.lod, self.allocator) catch |err| { - log.log.warn("Failed to snapshot LOD{} cache ({}, {}): {}", .{ @intFromEnum(entry.key_ptr.*.lod), entry.key_ptr.*.rx, entry.key_ptr.*.rz, err }); - continue; - }, - else => continue, - }; - const key = entry.key_ptr.*; - const revision = region.source_revision; - region.store_write_queued = true; - const accepted = self.cache_io.enqueueWrite(path, key, self.cacheKey(key), revision, snapshot, store_size_cap_mb) catch |err| blk: { - log.log.warn("Failed to queue LOD{} cache write ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - break :blk false; - }; - if (!accepted) { - var unused = snapshot; - unused.deinit(); - region.store_write_queued = false; - return queued; - } - queued += 1; - } - } - return queued; -} - -fn regionRequiresSpans(self: *Self, key: LODRegionKey) bool { - return self.config.getVerticalSpanBudget() > 0 and self.effectiveMeshPath(key.lod) == .column_spans; -} - -pub fn cacheKey(self: *const Self, key: LODRegionKey) lod_cache.Key { - return .{ .seed = self.generator.seed, .generator_identity_hash = self.generator.identity_hash, .generator_version = self.generator.version, .rx = key.rx, .rz = key.rz, .lod = key.lod }; -} - -pub fn legacyCacheFilePath(self: *Self, save_dir_path: []const u8, key: lod_cache.Key) ![]u8 { - return std.fmt.allocPrint(self.allocator, "{s}/lod_cache/lod_{}_{}_{}_{}_{}_{}.dat", .{ save_dir_path, key.seed, key.generator_identity_hash, key.generator_version, key.rx, key.rz, @intFromEnum(key.lod) }); -} - -pub fn logLegacyCacheNotice(self: *Self) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.cache_store.logged_legacy_cache_notice) return; - self.cache_store.logged_legacy_cache_notice = true; - log.log.warn("Using read-only legacy LOD cache fallback; new writes go to lod/ region store", .{}); -} - -pub fn cacheDirPathSnapshot(self: *Self) ?[]u8 { - self.mutex.lockShared(); - defer self.mutex.unlockShared(); - const path = self.cache_store.cache_dir_path orelse return null; - return self.allocator.dupe(u8, path) catch |err| { - log.log.warn("LOD cache path snapshot allocation failed: {}", .{err}); - return null; - }; -} - -pub fn cacheEnabled(self: *Self) bool { - self.mutex.lockShared(); - defer self.mutex.unlockShared(); - return self.cache_store.cache_dir_path != null; -} - -// Synchronous helpers remain explicit setup/diagnostic APIs. Update and -// generation paths use CacheIoPipeline exclusively. -pub fn readStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key) !?[]u8 { - self.cache_store.store_mutex.lock(); - defer self.cache_store.store_mutex.unlock(); - return lod_store.readPayload(self.allocator, save_dir_path, cache_key); -} - -pub fn writeStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key, bytes: []const u8) !void { - self.cache_store.store_mutex.lock(); - defer self.cache_store.store_mutex.unlock(); - const store_size_cap_mb = if (self.cache_store.use_config_store_size_cap) self.config.getLODStoreSizeCapMB() else self.cache_store.store_size_cap_mb; - try lod_store.writePayload(self.allocator, save_dir_path, cache_key, bytes, store_size_cap_mb); -} - -pub fn deleteStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key) void { - self.cache_store.store_mutex.lock(); - defer self.cache_store.store_mutex.unlock(); - lod_store.deletePayload(self.allocator, save_dir_path, cache_key); -} - -pub fn deleteStoreContainer(self: *Self, path: []const u8) void { - self.cache_store.store_mutex.lock(); - defer self.cache_store.store_mutex.unlock(); - fs.cwd().deleteFile(path) catch |err| { - if (err != error.FileNotFound) log.log.warn("Failed to delete corrupt LOD store container '{s}': {}", .{ path, err }); - }; -} - -pub fn loadCachedSourceData(self: *Self, key: LODRegionKey) ?LODSimplifiedData { - const path = self.cacheDirPathSnapshot() orelse return null; - defer self.allocator.free(path); - const cache_key = self.cacheKey(key); - if (self.readStorePayload(path, cache_key) catch |err| switch (err) { - lod_store.StoreError.CorruptContainer => { - const container_path = lod_store.containerPath(self.allocator, path, cache_key) catch return null; - defer self.allocator.free(container_path); - log.log.warn("Discarding corrupt LOD store container '{s}'", .{container_path}); - self.deleteStoreContainer(container_path); - return null; - }, - else => { - log.log.warn("Failed to read LOD store for LOD{} ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - return null; - }, - }) |bytes| { - defer self.allocator.free(bytes); - return lod_cache.deserialize(bytes, cache_key, self.allocator) catch |err| { - log.log.warn("Discarding corrupt LOD store payload LOD{} ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - self.deleteStorePayload(path, cache_key); - return null; - }; - } - const legacy_path = self.legacyCacheFilePath(path, cache_key) catch return null; - defer self.allocator.free(legacy_path); - const bytes = fs.cwd().readFileAlloc(legacy_path, self.allocator, 16 * 1024 * 1024) catch |err| switch (err) { - error.FileNotFound => return null, - else => { - log.log.warn("Failed to read legacy LOD cache '{s}': {}", .{ legacy_path, err }); - return null; - }, - }; - defer self.allocator.free(bytes); - self.logLegacyCacheNotice(); - return lod_cache.deserialize(bytes, cache_key, self.allocator) catch |err| { - log.log.warn("Discarding corrupt legacy LOD cache '{s}': {}", .{ legacy_path, err }); - fs.cwd().deleteFile(legacy_path) catch |delete_err| { - if (delete_err != error.FileNotFound) log.log.warn("Failed to delete corrupt legacy LOD cache '{s}': {}", .{ legacy_path, delete_err }); - }; - return null; - }; -} - -pub fn recordCacheHit(self: *Self) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.cache_hits += 1; -} - -pub fn recordCacheMiss(self: *Self) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.cache_misses += 1; -} - -/// Explicit API used by tools/tests. Like frame writes, it snapshots then -/// serializes on the cache worker; callers may use flushCacheIO to wait. -pub fn saveCachedSourceData(self: *Self, key: LODRegionKey, data: *const LODSimplifiedData) void { - const path = self.cacheDirPathSnapshot() orelse return; - defer self.allocator.free(path); - const snapshot = lod_cache.cloneSourceData(data, key.lod, self.allocator) catch |err| { - log.log.warn("Failed to snapshot LOD{} cache ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - return; - }; - const store_size_cap_mb = if (self.cache_store.use_config_store_size_cap) self.config.getLODStoreSizeCapMB() else self.cache_store.store_size_cap_mb; - const accepted = self.cache_io.enqueueWrite(path, key, self.cacheKey(key), 0, snapshot, store_size_cap_mb) catch false; - if (!accepted) { - var unused = snapshot; - unused.deinit(); - } -} - -pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []const u8) !Self { - _ = cache_dir_path; - var manager = Self{ - .allocator = allocator, - .config = undefined, - .regions = undefined, - .meshes = undefined, - .job_dispatcher = undefined, - .upload_queues = undefined, - .transition_queue = .empty, - .player_cx = std.atomic.Value(i32).init(0), - .player_cz = std.atomic.Value(i32).init(0), - .scan_states = [_]manager_ctx.LODScanState{manager_ctx.LODScanState{}} ** lod_chunk.LODLevel.count, - .stats = .{}, - .profiling = .init(false), - .cache_hits = 0, - .cache_misses = 0, - .cancelled_jobs = 0, - .mutex = .{}, - .gpu_bridge = undefined, - .generator = .{ .ptr = undefined, .generate_heightmap_only = undefined, .maybe_recenter_cache = undefined, .seed = 42, .identity_hash = 99, .version = 7 }, - .atlas = undefined, - .paused = false, - .memory_governor = .{}, - .update_tick = 0, - .mesh_disposal = .{}, - .renderer = undefined, - .cache_store = .{}, - .cache_io = undefined, - .cleanup_covered_regions = true, - .ingestion_queue = @import("lod_manager.zig").LODIngestionQueue.init(allocator), - }; - manager.cache_io = try cache_io.CacheIoPipeline.init(allocator, &manager.profiling); - return manager; -} - -fn deinitCompletion(completion: *cache_io.Completion) void { - switch (completion.*) { - .read => |*read| switch (read.result) { - .hit => |*data| data.deinit(), - .miss => {}, - }, - .write => {}, - } -} diff --git a/modules/world-lod/src/lod_manager_context.zig b/modules/world-lod/src/lod_manager_context.zig deleted file mode 100644 index e3af0e7e..00000000 --- a/modules/world-lod/src/lod_manager_context.zig +++ /dev/null @@ -1,298 +0,0 @@ -const std = @import("std"); -const engine_core = @import("engine-core"); -const sync = @import("sync"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const LODColumnProvenance = world_core.LODColumnProvenance; - -pub const ChunkCoordKey = struct { - cx: i32, - cz: i32, - - pub fn hash(self: ChunkCoordKey) u64 { - const ux: u64 = @bitCast(@as(i64, self.cx)); - const uz: u64 = @bitCast(@as(i64, self.cz)); - return ux ^ (uz *% 0x9e3779b97f4a7c15); - } - pub fn eql(a: ChunkCoordKey, b: ChunkCoordKey) bool { - return a.cx == b.cx and a.cz == b.cz; - } -}; - -pub const ChunkCoordKeyContext = struct { - pub fn hash(self: @This(), key: ChunkCoordKey) u64 { - _ = self; - return key.hash(); - } - pub fn eql(self: @This(), a: ChunkCoordKey, b: ChunkCoordKey) bool { - _ = self; - return a.eql(b); - } -}; - -pub const PendingIngestion = struct { - cx: i32, - cz: i32, - provenance: LODColumnProvenance, - pending_levels: u8, - /// Retained for wire/API compatibility with the former expiring queue. - /// Deferred ingestion is now durable for the manager lifetime, so this is - /// always zero and must not be decremented into deletion. - ttl: u16 = 0, -}; - -pub const GenerationCandidate = struct { - key: LODRegionKey, - chunk: *LODChunk, - encoded_priority: i32, - level: u3, - coord_scale: i32, - job_token: u32, - lod_radius: i32, - want_spans: bool, -}; - -pub const MeshCandidate = struct { - chunk: *LODChunk, - encoded_priority: i32, - level: u3, - coord_scale: i32, - job_token: u32, - lod_radius: i32, -}; - -pub const UploadCandidate = struct { - chunk: *LODChunk, - encoded_priority: i32, - level: u3, -}; - -/// A lifecycle record is deliberately value-only: maps remain the source of -/// truth and the token is checked when popped. This makes superseded work -/// cheap to leave in the heap while edits, teleports, and remeshes advance a -/// region token/revision. -pub const LifecycleStage = enum { generation, mesh, upload, fade }; - -pub const LifecycleToken = struct { - key: LODRegionKey, - job_token: u32, - source_revision: u32, - priority: i32, - stage: LifecycleStage, - - /// Map records are authoritative; queued copies are accepted only while - /// both lifecycle and source revisions still match. - pub fn matches(self: LifecycleToken, chunk: *const LODChunk) bool { - return self.job_token == chunk.job_token and self.source_revision == chunk.source_revision; - } -}; - -pub const LifecyclePushResult = enum { primary, overflow }; - -pub const LifecycleQueue = struct { - const Heap = std.PriorityQueue(LifecycleToken, void, compare); - - mutex: sync.Mutex = .{}, - heap: Heap = Heap.initContext({}), - overflow_heap: Heap = Heap.initContext({}), - /// Bounds transition work independently of the number of resident maps. - capacity: usize = MAX_LIFECYCLE_TOKENS, - /// A bounded overflow heap retains accepted work immediately rather than - /// abandoning it until reconciliation. It is large enough to hold several - /// stale generations for every resident region while remaining bounded. - overflow_capacity: usize = MAX_LIFECYCLE_OVERFLOW_TOKENS, - overflow_events: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - - fn compare(_: void, a: LifecycleToken, b: LifecycleToken) std.math.Order { - return std.math.order(a.priority, b.priority); - } - - pub fn deinit(self: *LifecycleQueue, allocator: std.mem.Allocator) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.heap.deinit(allocator); - self.overflow_heap.deinit(allocator); - } - - /// All heap mutations are serialized because workers publish transitions - /// while the main thread consumes them. Overflow work stays prioritized and - /// is consumed on the next pop rather than waiting for reconciliation. - pub fn push(self: *LifecycleQueue, allocator: std.mem.Allocator, token: LifecycleToken) !LifecyclePushResult { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.heap.count() < self.capacity) { - try self.heap.push(allocator, token); - return .primary; - } - if (self.overflow_heap.count() >= self.overflow_capacity) return error.LifecycleQueueFull; - try self.overflow_heap.push(allocator, token); - _ = self.overflow_events.fetchAdd(1, .monotonic); - return .overflow; - } - - pub fn pop(self: *LifecycleQueue) ?LifecycleToken { - self.mutex.lock(); - defer self.mutex.unlock(); - const primary = self.heap.peek(); - const overflow = self.overflow_heap.peek(); - if (primary == null) return self.overflow_heap.pop(); - if (overflow == null) return self.heap.pop(); - return if (compare({}, overflow.?, primary.?) == .lt) - self.overflow_heap.pop() - else - self.heap.pop(); - } - - pub fn count(self: *LifecycleQueue) usize { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.heap.count() + self.overflow_heap.count(); - } - - pub fn overflowEvents(self: *const LifecycleQueue) u64 { - return self.overflow_events.load(.monotonic); - } -}; - -pub const PlayerChunkPos = struct { - cx: i32, - cz: i32, -}; - -/// Persistent bounded concentric-scan cursor for one LOD level. -pub const LODScanState = struct { - player_rx: i32 = 0, - player_rz: i32 = 0, - effective_radius: i32 = -1, - next_ring: i64 = 0, - ring_index: i64 = 0, - last_examined: usize = 0, -}; - -pub const ChunkResolver = struct { - ptr: *anyopaque, - resolve_fn: *const fn (ptr: *anyopaque, cx: i32, cz: i32) ?*const Chunk, - - pub fn resolve(self: ChunkResolver, cx: i32, cz: i32) ?*const Chunk { - return self.resolve_fn(self.ptr, cx, cz); - } -}; - -pub const MAX_LOD_REGIONS = 2048; -/// Conservative logical reservation made before a region has source data or a -/// mesh. It bounds cold-cache admissions against the configured total LOD -/// memory cap; exact accounting remains available in diagnostics. -pub const LOGICAL_LOD_REGION_RESERVATION_BYTES: usize = 1024 * 1024; -// Bound queued and in-flight regions so a cold cache cannot bury the horizon -// fallback behind thousands of expensive generation jobs. -pub const MAX_PENDING_LOD_REGIONS: usize = 64; -pub const CHUNK_COVERAGE_PADDING: i32 = 1; -pub const LOD_UPDATE_DIVISOR: u32 = 2; -// WorldStreamer reserves these workers from its foreground pools whenever LOD -// is enabled, so horizon generation can be fast without oversubscribing CPUs. -pub const MIN_LOD_WORKERS: usize = 1; -pub const MAX_LOD_WORKERS: usize = 8; -pub const MAX_MEMORY_EVICTIONS_PER_UPDATE: usize = 32; -pub const MAX_MESH_DELETIONS_PER_SWEEP: usize = 64; -pub const DELETION_SWEEP_SECONDS: f32 = 1.0; -pub const DEFAULT_LOD_UPLOAD_BUDGET_BYTES: usize = 32 * 1024 * 1024; -pub const LOD_UPLOAD_BUDGET_ENV = "ZIGCRAFT_LOD_UPLOAD_BUDGET_MB"; -pub const MAX_CACHE_LOADS_PER_UPDATE: usize = 8; -pub const MAX_PENDING_INGESTIONS: usize = 4096; -pub const EDIT_FLUSH_COOLDOWN: f32 = 1.0; -pub const LOD_FRAME_DT_APPROX: f32 = 0.016; -pub const MAX_LIFECYCLE_TOKENS: usize = 256; -pub const MAX_LIFECYCLE_OVERFLOW_TOKENS: usize = MAX_LOD_REGIONS * 4; -pub const MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE: usize = 48; -/// This is an explicit safety net for lost/overflowed tokens, not normal -/// scheduling. Keep it rare so resident map size does not drive frame work. -pub const LIFECYCLE_RECONCILIATION_INTERVAL: u32 = 240; - -pub fn lodUploadBudgetBytes() usize { - const raw = engine_core.getenv(LOD_UPLOAD_BUDGET_ENV) orelse return DEFAULT_LOD_UPLOAD_BUDGET_BYTES; - const mb = std.fmt.parseUnsigned(usize, raw, 10) catch return DEFAULT_LOD_UPLOAD_BUDGET_BYTES; - if (mb == 0) return std.math.maxInt(usize); - return std.math.mul(usize, mb, 1024 * 1024) catch DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -} - -pub fn wouldExceedUploadBudget(uploaded_bytes: usize, pending_bytes: usize, budget_bytes: usize) bool { - if (budget_bytes == 0 or budget_bytes == std.math.maxInt(usize)) return false; - if (pending_bytes == 0) return false; - if (uploaded_bytes >= budget_bytes) return true; - return pending_bytes > budget_bytes - uploaded_bytes; -} - -pub fn isUploadPressureError(err: anyerror) bool { - return switch (err) { - error.OutOfMemory, error.PendingCopyOverflow => true, - else => false, - }; -} - -test "LifecycleQueue bounds transition work independently of resident regions" { - var queue = LifecycleQueue{ .capacity = 2, .overflow_capacity = 2 }; - defer queue.deinit(std.testing.allocator); - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod3 }; - - try std.testing.expectEqual(LifecyclePushResult.primary, try queue.push(std.testing.allocator, .{ .key = key, .job_token = 1, .source_revision = 0, .priority = 20, .stage = .mesh })); - try std.testing.expectEqual(LifecyclePushResult.primary, try queue.push(std.testing.allocator, .{ .key = key, .job_token = 2, .source_revision = 1, .priority = 4, .stage = .mesh })); - try std.testing.expectEqual(LifecyclePushResult.overflow, try queue.push(std.testing.allocator, .{ .key = key, .job_token = 3, .source_revision = 2, .priority = 1, .stage = .mesh })); - try std.testing.expectEqual(@as(usize, 3), queue.count()); - try std.testing.expectEqual(@as(u64, 1), queue.overflowEvents()); - try std.testing.expectEqual(@as(u32, 3), queue.pop().?.job_token); -} - -test "LifecycleToken rejects stale lifecycle and source revisions" { - var chunk = LODChunk.init(0, 0, .lod3); - chunk.job_token = 8; - chunk.source_revision = 3; - const current = LifecycleToken{ .key = chunk.key(), .job_token = 8, .source_revision = 3, .priority = 0, .stage = .mesh }; - const stale_job = LifecycleToken{ .key = chunk.key(), .job_token = 7, .source_revision = 3, .priority = 0, .stage = .mesh }; - const stale_source = LifecycleToken{ .key = chunk.key(), .job_token = 8, .source_revision = 2, .priority = 0, .stage = .mesh }; - try std.testing.expect(current.matches(&chunk)); - try std.testing.expect(!stale_job.matches(&chunk)); - try std.testing.expect(!stale_source.matches(&chunk)); -} - -test "LifecycleQueue serializes concurrent production push and pop operations" { - const Producer = struct { - fn run(queue: *LifecycleQueue, base: i32) void { - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod4 }; - for (0..64) |i| { - _ = queue.push(std.heap.page_allocator, .{ .key = key, .job_token = @intCast(i + 1), .source_revision = 0, .priority = base + @as(i32, @intCast(i)), .stage = .mesh }) catch unreachable; - } - } - }; - var queue = LifecycleQueue{ .capacity = 128 }; - defer queue.deinit(std.heap.page_allocator); - var first = try std.Thread.spawn(.{}, Producer.run, .{ &queue, @as(i32, 64) }); - var second = try std.Thread.spawn(.{}, Producer.run, .{ &queue, @as(i32, 0) }); - first.join(); - second.join(); - try std.testing.expectEqual(@as(usize, 128), queue.count()); - - const Consumer = struct { - fn run(queue_ptr: *LifecycleQueue, consumed_ptr: *std.atomic.Value(u32)) void { - while (queue_ptr.pop()) |_| { - _ = consumed_ptr.fetchAdd(1, .monotonic); - } - } - }; - var consumed = std.atomic.Value(u32).init(0); - var consumer_a = try std.Thread.spawn(.{}, Consumer.run, .{ &queue, &consumed }); - var consumer_b = try std.Thread.spawn(.{}, Consumer.run, .{ &queue, &consumed }); - consumer_a.join(); - consumer_b.join(); - try std.testing.expectEqual(@as(u32, 128), consumed.load(.monotonic)); - try std.testing.expectEqual(@as(usize, 0), queue.count()); -} - -comptime { - if (LODLevel.count < 2) { - @compileError("LOD system requires at least two levels (LOD0 and at least one simplified level)"); - } -} diff --git a/modules/world-lod/src/lod_manager_core_ops.zig b/modules/world-lod/src/lod_manager_core_ops.zig deleted file mode 100644 index 26cee4d4..00000000 --- a/modules/world-lod/src/lod_manager_core_ops.zig +++ /dev/null @@ -1,575 +0,0 @@ -const std = @import("std"); -const lod_options = @import("world_lod_options"); -const Self = @import("lod_manager.zig").LODManager; -const fs = @import("fs"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const ILODConfig = lod_chunk.ILODConfig; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const worldToChunkFromFloat = world_core.worldToChunkFromFloat; -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const Vertex = @import("engine-rhi").Vertex; -const engine_core = @import("engine-core"); -const log = engine_core.log; -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; -const RingBuffer = engine_core.ring_buffer.RingBuffer; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const ChunkChecker = lod_gpu.ChunkChecker; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const TextureAtlas = @import("engine-assets").TextureAtlas; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = @import("lod_stats.zig").LODStats; -const manager_ctx = @import("lod_manager_context.zig"); -const LODIngestionQueue = @import("lod_manager.zig").LODIngestionQueue; -const ChunkCoordKey = manager_ctx.ChunkCoordKey; -const ChunkCoordKeyContext = manager_ctx.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const ChunkResolver = manager_ctx.ChunkResolver; -const PendingIngestion = manager_ctx.PendingIngestion; -const PlayerChunkPos = manager_ctx.PlayerChunkPos; -const MAX_CACHE_LOADS_PER_UPDATE = manager_ctx.MAX_CACHE_LOADS_PER_UPDATE; -const MAX_MEMORY_EVICTIONS_PER_UPDATE = manager_ctx.MAX_MEMORY_EVICTIONS_PER_UPDATE; -const MAX_MESH_DELETIONS_PER_SWEEP = manager_ctx.MAX_MESH_DELETIONS_PER_SWEEP; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = manager_ctx.DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const LOD_UPLOAD_BUDGET_ENV = manager_ctx.LOD_UPLOAD_BUDGET_ENV; -const LOD_UPDATE_DIVISOR = manager_ctx.LOD_UPDATE_DIVISOR; -const DELETION_SWEEP_SECONDS = manager_ctx.DELETION_SWEEP_SECONDS; -const CHUNK_COVERAGE_PADDING = manager_ctx.CHUNK_COVERAGE_PADDING; -const MIN_LOD_WORKERS = manager_ctx.MIN_LOD_WORKERS; -const MAX_LOD_WORKERS = manager_ctx.MAX_LOD_WORKERS; -const MAX_PENDING_INGESTIONS = manager_ctx.MAX_PENDING_INGESTIONS; -const PENDING_INGESTION_TTL = manager_ctx.PENDING_INGESTION_TTL; -const EDIT_FLUSH_COOLDOWN = manager_ctx.EDIT_FLUSH_COOLDOWN; -const LOD_FRAME_DT_APPROX = manager_ctx.LOD_FRAME_DT_APPROX; -const lodUploadBudgetBytes = manager_ctx.lodUploadBudgetBytes; -const wouldExceedUploadBudget = manager_ctx.wouldExceedUploadBudget; -const isUploadPressureError = manager_ctx.isUploadPressureError; -const TELEPORT_CANCEL_DISTANCE_CHUNKS: i32 = 32; - -pub fn storePlayerChunkPos(self: *Self, cx: i32, cz: i32) void { - self.player_cx.store(cx, .release); - self.player_cz.store(cz, .release); -} - -pub fn loadPlayerChunkPos(self: *const Self) PlayerChunkPos { - return .{ - .cx = self.player_cx.load(.acquire), - .cz = self.player_cz.load(.acquire), - }; -} - -pub fn init(allocator: std.mem.Allocator, config: ILODConfig, gpu_bridge: LODGPUBridge, render_iface: LODRenderInterface, generator: LODGenerator, atlas: *const TextureAtlas) !*Self { - const mgr = try allocator.create(Self); - errdefer allocator.destroy(mgr); - - var regions: [LODLevel.count]RegionMap = undefined; - var meshes: [LODLevel.count]MeshMap = undefined; - var gen_queues: [LODLevel.count]*JobQueue = undefined; - var upload_queues: [LODLevel.count]RingBuffer(*LODChunk) = undefined; - var initialized_levels: usize = 0; - - errdefer { - var i: usize = 0; - while (i < initialized_levels) : (i += 1) { - upload_queues[i].deinit(); - gen_queues[i].deinit(); - allocator.destroy(gen_queues[i]); - meshes[i].deinit(); - regions[i].deinit(); - } - } - - for (0..LODLevel.count) |i| { - var region_map = RegionMap.init(allocator); - errdefer region_map.deinit(); - - var mesh_map = MeshMap.init(allocator); - errdefer mesh_map.deinit(); - - const queue = try allocator.create(JobQueue); - errdefer allocator.destroy(queue); - queue.* = JobQueue.init(allocator); - errdefer queue.deinit(); - - var upload_queue = try RingBuffer(*LODChunk).init(allocator, 128); - errdefer upload_queue.deinit(); - - regions[i] = region_map; - meshes[i] = mesh_map; - gen_queues[i] = queue; - upload_queues[i] = upload_queue; - initialized_levels += 1; - } - - mgr.* = .{ - .allocator = allocator, - .config = config, - .regions = regions, - .meshes = meshes, - .job_dispatcher = .{ .queues = gen_queues }, - .upload_queues = upload_queues, - .transition_queue = .empty, - .player_cx = std.atomic.Value(i32).init(0), - .player_cz = std.atomic.Value(i32).init(0), - .scan_states = [_]manager_ctx.LODScanState{manager_ctx.LODScanState{}} ** LODLevel.count, - .stats = .{}, - .profiling = .init(engine_core.envFlag("ZIGCRAFT_LOD_PROFILE", false) or lod_options.benchmark_lod_profile), - .cache_hits = 0, - .cache_misses = 0, - .cancelled_jobs = 0, - .mutex = .{}, - .gpu_bridge = gpu_bridge, - .generator = generator, - .atlas = atlas, - .paused = false, - .memory_governor = .{}, - .update_tick = 0, - .mesh_disposal = .{}, - .renderer = render_iface, - .cleanup_covered_regions = true, - .cache_store = .{ .store_size_cap_mb = config.getLODStoreSizeCapMB(), .use_config_store_size_cap = true }, - .cache_io = undefined, - .ingestion_queue = LODIngestionQueue.init(allocator), - }; - - mgr.cache_io = try @import("lod_cache_io.zig").CacheIoPipeline.init(allocator, &mgr.profiling); - errdefer mgr.cache_io.deinit(); - - const cpu_count = std.Thread.getCpuCount() catch MIN_LOD_WORKERS; - const total_budget = @max(@as(usize, 5), cpu_count -| 1); - const foreground_minimum: usize = 4; - const lod_capacity = @max(MIN_LOD_WORKERS, total_budget -| foreground_minimum); - const lod_worker_count = @min(std.math.clamp(cpu_count / 2, MIN_LOD_WORKERS, MAX_LOD_WORKERS), lod_capacity); - - // All LOD jobs go through one shared queue. LOD-aware priority bits keep - // fine near-detail jobs ahead of coarse fallback regions. - mgr.job_dispatcher.worker_pool = try WorkerPool.init(allocator, lod_worker_count, mgr.job_dispatcher.queues[LODLevel.count - 1], mgr, @import("lod_manager_generation_ops.zig").processLODJob); - - const radii = config.getRadii(); - log.log.info("LODManager initialized with radii: {any} | workers={}", .{ - radii, - lod_worker_count, - }); - - return mgr; -} - -pub fn deinit(self: *Self) void { - // Abort any in-flight heightmap jobs BEFORE joining the worker pool. - // Coarse-LOD heightmap generation can take seconds per region and is - // only interruptible via this flag (checked inside the generation loop). - // This is the ONLY place stop_flag is set: normal pause()/unpause() and - // map-open leave it false so LOD generation runs uninterrupted. - self.job_dispatcher.stop_flag.store(true, .release); - - const cancellation_lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, cancellation_lock_wait_timer); - const cancellation_lock_hold_timer = self.profiling.begin(); - for (0..LODLevel.count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| entry.value_ptr.*.requestCancellation(); - } - self.profiling.end(.manager_lock_hold, cancellation_lock_hold_timer); - self.mutex.unlock(); - - // Stop and cleanup queues - for (0..LODLevel.count) |i| { - self.job_dispatcher.queues[i].stop(); - } - - // Cleanup worker pool. In-flight heightmap jobs were aborted by - // stop_flag above, so this join completes promptly. - if (self.job_dispatcher.worker_pool) |pool| { - pool.deinit(); - } - - // This is an explicit shutdown boundary: it is permitted to wait for the - // dedicated cache worker and flush source snapshots before region storage - // is released. The normal update path only enqueues/drains completions. - self.shutdownCacheIO(); - self.cache_io.deinit(); - - // All LOD workers and cache I/O are stopped. This is the sole manager - // lifecycle boundary allowed to issue a device-wide synchronization; the - // shutdown bucket keeps it out of runtime streaming telemetry. - self.waitIdleTracked(.shutdown); - - for (0..LODLevel.count) |i| { - self.job_dispatcher.queues[i].deinit(); - self.allocator.destroy(self.job_dispatcher.queues[i]); - self.upload_queues[i].deinit(); - - // Cleanup meshes - var mesh_iter = self.meshes[i].iterator(); - while (mesh_iter.next()) |entry| { - entry.value_ptr.*.releasePendingCompactTile(); - self.gpu_bridge.destroy(entry.value_ptr.*); - self.allocator.destroy(entry.value_ptr.*); - } - self.meshes[i].deinit(); - - // Cleanup regions - var region_iter = self.regions[i].iterator(); - while (region_iter.next()) |entry| { - entry.value_ptr.*.deinit(self.allocator); - self.allocator.destroy(entry.value_ptr.*); - } - self.regions[i].deinit(); - } - - self.transition_queue.deinit(self.allocator); - - // Process any pending deletions after all LOD users have stopped. - self.processMeshDeletions(std.math.maxInt(usize)); - self.mesh_disposal.queue.deinit(self.allocator); - - if (self.cache_store.cache_dir_path) |path| { - self.allocator.free(path); - } - - self.ingestion_queue.deinit(self.allocator); - self.generation_tokens.deinit(self.allocator); - self.transition_tokens.deinit(self.allocator); - self.fade_tokens.deinit(self.allocator); - - // NOTE: LODManager does NOT own the renderer lifetime. - // The renderer is owned by World and deinit'd there. - - self.allocator.destroy(self); -} - -/// Update LOD system with player position -pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque) !void { - const update_timer = self.profiling.begin(); - defer self.profiling.end(.update, update_timer); - // Completion application only: no filesystem or serialization occurs on - // this frame/update path. - self.drainCacheCompletions(); - // The GPU-culling scale fixture is already fully resident through the - // production upload bridge. Do not let normal streaming add finer coverage - // or evict its fixed source set while it is being measured. - if (self.benchmark_fixture_active) { - self.updateStats(); - return; - } - if (self.paused) return; - - // Render detects compact submission failure under its shared lock. Only - // this update thread may retire its GPU range and publish CPU fallback. - self.recoverCompactDrawFailures(); - - // Deferred deletion is frame-safe: renderer resource destruction retires - // buffers/ranges through its frame-fence path rather than stalling the - // device during routine streaming. - self.mesh_disposal.timer += LOD_FRAME_DT_APPROX; - if (self.mesh_disposal.timer >= DELETION_SWEEP_SECONDS) { - self.processMeshDeletions(MAX_MESH_DELETIONS_PER_SWEEP); - self.mesh_disposal.timer = 0; - } - - // Safety: Check for NaN/Inf player position - if (!std.math.isFinite(player_pos.x) or !std.math.isFinite(player_pos.z)) return; - - const pc = worldToChunkFromFloat(player_pos.x, player_pos.z); - const previous_pc = self.loadPlayerChunkPos(); - self.storePlayerChunkPos(pc.chunk_x, pc.chunk_z); - const moved_x = @abs(@as(i64, pc.chunk_x) - @as(i64, previous_pc.cx)); - const moved_z = @abs(@as(i64, pc.chunk_z) - @as(i64, previous_pc.cz)); - const teleport_distance_sq = @as(i64, TELEPORT_CANCEL_DISTANCE_CHUNKS) * TELEPORT_CANCEL_DISTANCE_CHUNKS; - if (moved_x * moved_x + moved_z * moved_z >= teleport_distance_sq) { - cancelWorkOutsideHorizon(self, pc.chunk_x, pc.chunk_z); - } - - // Keep LOD job priorities fresh as the player moves. doReprioritize is - // LOD-aware (scales region coords to chunk space, preserves LOD-bias - // bits), so this safely re-orders stale jobs after chunk crossings. - // The actual rebuild is lazy (only on pop when the queue is large). - self.job_dispatcher.queues[LODLevel.count - 1].updatePlayerPos(pc.chunk_x, pc.chunk_z) catch {}; - - // Throttle heavy LOD management logic (generation queuing, state processing, unloads). - // LOD management involves iterating over thousands of potential regions and can - // take several milliseconds. Throttling to every 4 frames (approx 15Hz at 60fps) - // significantly reduces CPU overhead while remaining responsive to player movement. - self.update_tick += 1; - if (self.update_tick % LOD_UPDATE_DIVISOR != 0) { - self.processUploads(); - self.decayTransitionFrames(); - return; - } - - if (self.cleanup_covered_regions) { - if (chunk_checker) |checker| { - self.unloadLODWhereChunksLoaded(checker, checker_ctx.?); - } - } - - // Issue #119 Phase 4: Recenter classification cache if player moved far enough. - // This ensures LOD chunks have cache coverage for consistent biome/surface data. - const player_wx: i32 = @intFromFloat(player_pos.x); - const player_wz: i32 = @intFromFloat(player_pos.z); - _ = self.generator.maybeRecenterCache(player_wx, player_wz); - - self.mutex.lock(); - const active_lod_count = lod_chunk.activeLODCount(self.config); - self.mutex.unlock(); - - // Queue the coarsest concentric fallback first, then let LOD0/LOD1/LOD2 - // refinements fill and replace it without creating outer-horizon islands. - const scheduling_timer = self.profiling.begin(); - var order_idx: usize = 0; - while (order_idx < active_lod_count) : (order_idx += 1) { - const i = lod_scheduler.priorityLevelIndex(order_idx, active_lod_count); - self.queueLODRegions(@enumFromInt(@as(u3, @intCast(i))), player_velocity, chunk_checker, checker_ctx) catch |err| { - log.log.warn("LOD queue error for level {}: {} (non-fatal)", .{ i, err }); - }; - } - self.profiling.end(.scheduling, scheduling_timer); - - self.processQueuedGenerations(player_velocity) catch |err| { - log.log.warn("LOD cache/generation dispatch error: {} (non-fatal)", .{err}); - }; - - // Process state transitions - const transitions_timer = self.profiling.begin(); - self.processStateTransitions(player_velocity) catch |err| { - log.log.warn("LOD state transitions error: {} (non-fatal)", .{err}); - }; - self.profiling.end(.state_transition, transitions_timer); - - // Process uploads (limited per frame) - self.processUploads(); - - // Update stats - self.updateStats(); - const eviction_timer = self.profiling.begin(); - self.enforceMemoryBudget() catch |err| { - log.log.warn("LOD memory budget eviction error: {} (non-fatal)", .{err}); - }; - self.profiling.end(.eviction, eviction_timer); - - // Periodic WARN-level LOD stats so logs/zigcraft.log shows LOD fill - // progress by default (no env vars needed). update_tick counts frames; - // every ~180 frames (~3s @ 60fps) gives a trend over a 20s diagnostic run. - if (self.update_tick % 180 == 0) { - const s = self.stats; - log.log.warn("LOD_STATS: loaded={any} generating={any} meshing={any} genQ={} uploadQ={any} meshes={any} cache_hit/miss={}/{} store_hit/miss={}/{} evictions={}", .{ - s.loaded, - s.generating, - s.meshing, - s.gen_queue_depth[LODLevel.count - 1], - s.upload_queue_depth, - s.mesh_count, - s.cache_hits, - s.cache_misses, - s.store_hits, - s.store_misses, - s.evictions, - }); - } - - // Unload distant regions - const distant_eviction_timer = self.profiling.begin(); - self.unloadDistantRegions() catch |err| { - log.log.warn("LOD unload error: {} (non-fatal)", .{err}); - }; - self.profiling.end(.eviction, distant_eviction_timer); - - // Chunk-derived ingestion: replay deferred ingestions, flush debounced - // player edits, and persist any dirty source regions to the store. - self.drainPendingIngestions(); - self.flushEditedChunks(); - self.flushDirtyStores(); - self.decayTransitionFrames(); -} - -/// Get current statistics -pub fn getStats(self: *Self) LODStats { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lockShared(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlockShared(); - var snapshot = self.stats; - snapshot.profiling = self.profiling.snapshot(); - return snapshot; -} - -/// Pause all LOD generation -pub fn pause(self: *Self) void { - self.paused = true; - for (0..LODLevel.count) |i| { - self.job_dispatcher.queues[i].setPaused(true); - } - - // setPaused clears queued jobs. In-flight jobs are cancelled through their - // per-region signal and token so they cannot publish stale data after an - // unpause, even when unpause happens before a worker checks cancellation. - self.mutex.lock(); - defer self.mutex.unlock(); - for (0..LODLevel.count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const chunk = entry.value_ptr.*; - switch (chunk.getState()) { - .generating => { - chunk.requestCancellation(); - chunk.job_token +%= 1; - chunk.cache_read_queued = false; - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(.missing); - self.cancelled_jobs +|= 1; - }, - .meshing => { - chunk.requestCancellation(); - chunk.job_token +%= 1; - // The generated source data remains valid. Requeue the - // mesh transition after unpause without discarding its - // pending admission slot. - chunk.setState(.generated); - self.cancelled_jobs +|= 1; - }, - .queued_for_generation => { - chunk.requestCancellation(); - chunk.job_token +%= 1; - chunk.cache_read_queued = false; - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(.missing); - self.cancelled_jobs +|= 1; - }, - else => {}, - } - } - } -} - -pub fn cancelWorkOutsideHorizon(self: *Self, player_cx: i32, player_cz: i32) void { - self.mutex.lock(); - defer self.mutex.unlock(); - const radii = self.config.getRadii(); - const active = lod_chunk.activeLODCount(self.config); - for (0..LODLevel.count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const chunk = entry.value_ptr.*; - switch (chunk.getState()) { - .queued_for_generation, .generating, .meshing => {}, - else => continue, - } - if (i < active and entry.key_ptr.*.chunkBounds().intersectsRadius(player_cx, player_cz, radii[i])) continue; - - const was_generation = chunk.getState() != .meshing; - chunk.requestCancellation(); - chunk.job_token +%= 1; - chunk.cache_read_queued = false; - if (was_generation and self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(if (was_generation) .missing else .generated); - self.cancelled_jobs +|= 1; - } - } -} - -/// Resume LOD generation -pub fn unpause(self: *Self) void { - self.paused = false; - for (0..LODLevel.count) |i| { - self.job_dispatcher.queues[i].setPaused(false); - } -} - -/// Get LOD level for a given chunk distance -pub fn getLODForDistance(self: *const Self, chunk_x: i32, chunk_z: i32) LODLevel { - const player = self.loadPlayerChunkPos(); - const dist_sq = pointDistanceSquared(chunk_x, chunk_z, player.cx, player.cz); - const radii = self.config.getRadii(); - - const active_lod_count = lod_chunk.activeLODCount(self.config); - for (0..active_lod_count) |i| { - const radius_sq = @as(i64, radii[i]) * @as(i64, radii[i]); - if (dist_sq <= radius_sq) return @enumFromInt(@as(u3, @intCast(i))); - } - - return @enumFromInt(@as(u3, @intCast(active_lod_count - 1))); -} - -/// Check if a position is within LOD range -pub fn isInRange(self: *const Self, chunk_x: i32, chunk_z: i32) bool { - const radii = self.config.getRadii(); - const max_radius = radii[lod_chunk.activeLODCount(self.config) - 1]; - const player = self.loadPlayerChunkPos(); - const dist_sq = pointDistanceSquared(chunk_x, chunk_z, player.cx, player.cz); - return dist_sq <= @as(i64, max_radius) * @as(i64, max_radius); -} - -/// Render all LOD meshes -/// chunk_checker: Optional callback to check if regular chunks cover this region. -/// If all chunks in region are loaded, the LOD region is skipped. -/// -/// NOTE: Acquires a shared lock on LODManager. LODRenderer must NOT attempt to acquire -/// a write lock on LODManager during rendering to avoid deadlocks. -pub fn render(self: *Self, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, use_frustum: bool, max_distance_chunks: ?i32, layer: LODRenderLayer) void { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lockShared(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlockShared(); - - const visibility_timer = self.profiling.begin(); - defer self.profiling.end(.visibility, visibility_timer); - self.renderer.render(&self.meshes, &self.regions, self.config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer, &self.stats, if (self.profiling.enabled) &self.profiling else null); -} - -/// Renders a layer using a WorldRenderer-monotonic frame serial. The concrete -/// LOD renderer projects visibility once for a serial and reuses safe value -/// snapshots for the terrain and water submissions. -pub fn renderFrame(self: *Self, frame_serial: u64, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, use_frustum: bool, max_distance_chunks: ?i32, detail_render_radius: i32, layer: LODRenderLayer) void { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lockShared(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlockShared(); - - self.renderer.renderFrame(frame_serial, &self.meshes, &self.regions, self.config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, layer, &self.stats, if (self.profiling.enabled) &self.profiling else null); -} - -pub fn prepareFrame(self: *Self, frame_serial: u64, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, max_distance_chunks: ?i32, detail_render_radius: i32) void { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lockShared(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlockShared(); - self.renderer.prepareFrame(frame_serial, &self.meshes, &self.regions, self.config, view_proj, camera_pos, chunk_checker, checker_ctx, max_distance_chunks, detail_render_radius, &self.stats, if (self.profiling.enabled) &self.profiling else null); -} - -pub fn pointDistanceSquared(x0: i32, z0: i32, x1: i32, z1: i32) i64 { - const dx = @as(i64, x0) - @as(i64, x1); - const dz = @as(i64, z0) - @as(i64, z1); - const distance_sq = @as(i128, dx) * dx + @as(i128, dz) * dz; - return @intCast(@min(distance_sq, std.math.maxInt(i64))); -} diff --git a/modules/world-lod/src/lod_manager_eviction_ops.zig b/modules/world-lod/src/lod_manager_eviction_ops.zig deleted file mode 100644 index 2efe776d..00000000 --- a/modules/world-lod/src/lod_manager_eviction_ops.zig +++ /dev/null @@ -1,525 +0,0 @@ -const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; -const fs = @import("fs"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const ILODConfig = lod_chunk.ILODConfig; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const engine_core = @import("engine-core"); -const log = engine_core.log; -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; -const RingBuffer = engine_core.ring_buffer.RingBuffer; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const ChunkChecker = lod_gpu.ChunkChecker; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const TextureAtlas = @import("engine-assets").TextureAtlas; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = @import("lod_stats.zig").LODStats; -const manager_ctx = @import("lod_manager_context.zig"); -const ChunkCoordKey = manager_ctx.ChunkCoordKey; -const ChunkCoordKeyContext = manager_ctx.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const ChunkResolver = manager_ctx.ChunkResolver; -const PendingIngestion = manager_ctx.PendingIngestion; -const PlayerChunkPos = manager_ctx.PlayerChunkPos; -const MAX_CACHE_LOADS_PER_UPDATE = manager_ctx.MAX_CACHE_LOADS_PER_UPDATE; -const MAX_MEMORY_EVICTIONS_PER_UPDATE = manager_ctx.MAX_MEMORY_EVICTIONS_PER_UPDATE; -const MAX_MESH_DELETIONS_PER_SWEEP = manager_ctx.MAX_MESH_DELETIONS_PER_SWEEP; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = manager_ctx.DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const LOGICAL_LOD_REGION_RESERVATION_BYTES = manager_ctx.LOGICAL_LOD_REGION_RESERVATION_BYTES; -const LOD_UPLOAD_BUDGET_ENV = manager_ctx.LOD_UPLOAD_BUDGET_ENV; -const LOD_UPDATE_DIVISOR = manager_ctx.LOD_UPDATE_DIVISOR; -const DELETION_SWEEP_SECONDS = manager_ctx.DELETION_SWEEP_SECONDS; -const CHUNK_COVERAGE_PADDING = manager_ctx.CHUNK_COVERAGE_PADDING; -const MIN_LOD_WORKERS = manager_ctx.MIN_LOD_WORKERS; -const MAX_LOD_WORKERS = manager_ctx.MAX_LOD_WORKERS; -const MAX_PENDING_INGESTIONS = manager_ctx.MAX_PENDING_INGESTIONS; -const PENDING_INGESTION_TTL = manager_ctx.PENDING_INGESTION_TTL; -const EDIT_FLUSH_COOLDOWN = manager_ctx.EDIT_FLUSH_COOLDOWN; -const LOD_FRAME_DT_APPROX = manager_ctx.LOD_FRAME_DT_APPROX; -const lodUploadBudgetBytes = manager_ctx.lodUploadBudgetBytes; -const wouldExceedUploadBudget = manager_ctx.wouldExceedUploadBudget; -const isUploadPressureError = manager_ctx.isUploadPressureError; - -/// Unload regions that are too far from player -pub fn unloadDistantRegions(self: *Self) !void { - const radii = self.config.getRadii(); - const active_lod_count = lod_chunk.activeLODCount(self.config); - for (0..active_lod_count) |i| { - try self.unloadDistantForLevel(@enumFromInt(@as(u3, @intCast(i))), radii[i]); - } -} - -pub fn unloadDistantForLevel(self: *Self, lod: LODLevel, max_radius: i32) !void { - _ = max_radius; // Interface provides current radii - const radii = self.config.getRadii(); - const lod_radius = radii[@intFromEnum(lod)]; - const storage = &self.regions[@intFromEnum(lod)]; - - var to_remove = std.ArrayListUnmanaged(LODRegionKey).empty; - defer to_remove.deinit(self.allocator); - - // Hold lock for entire operation to prevent races with worker threads - const lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlock(); - - const player = self.loadPlayerChunkPos(); - var iter = storage.iterator(); - while (iter.next()) |entry| { - const key = entry.key_ptr.*; - const chunk = entry.value_ptr.*; - - if (!key.chunkBounds().intersectsRadius(player.cx, player.cz, lod_radius)) { - if (!chunk.isPinned() and - chunk.getState() != .generating and - chunk.getState() != .meshing and - chunk.getState() != .uploading) - { - try to_remove.append(self.allocator, key); - } - } - } - - // Remove after iteration (still under lock) - if (to_remove.items.len > 0) { - for (to_remove.items) |key| { - if (storage.get(key)) |chunk| { - if (chunk.getState() != .missing and chunk.getState() != .renderable and self.pending_region_count > 0) { - self.pending_region_count -= 1; - } - // Clean up mesh before removing chunk - const meshes = &self.meshes[@intFromEnum(lod)]; - self.noteRegionRemoved(key, chunk); - if (meshes.get(key)) |mesh| { - // Push to deferred deletion queue instead of deleting immediately - self.queueMeshDeletion(mesh); - _ = meshes.remove(key); - } - - chunk.deinit(self.allocator); - self.allocator.destroy(chunk); - _ = storage.remove(key); - } - } - } -} - -pub fn queueMeshDeletion(self: *Self, mesh: *LODMesh) void { - const memory = mesh.memorySnapshot(); - self.mesh_disposal.queue.append(self.allocator, mesh) catch { - mesh.releasePendingCompactTile(); - self.gpu_bridge.destroy(mesh); - self.allocator.destroy(mesh); - return; - }; - self.profiling.addDeferredDeletionBytes(memory.capacity_bytes); - self.profiling.addDeferredDeletionCpuBytes(memory.pending_upload_bytes); -} - -pub fn processMeshDeletions(self: *Self, max_count: usize) void { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlock(); - - const count = @min(max_count, self.mesh_disposal.queue.items.len); - if (count == 0) return; - - // Meshes have already spent a full disposal grace period outside the - // render maps. Whole buffers are retired by the RHI's frame-fence deletion - // queue; pooled ranges are only returned here, after that grace period. - // Do not turn routine streaming eviction into a device-global stall. - var processed: usize = 0; - while (processed < count) : (processed += 1) { - const idx = self.mesh_disposal.queue.items.len - 1; - const mesh = self.mesh_disposal.queue.items[idx]; - const memory = mesh.memorySnapshot(); - self.profiling.removeDeferredDeletionBytes(memory.capacity_bytes); - self.profiling.removeDeferredDeletionCpuBytes(memory.pending_upload_bytes); - mesh.releasePendingCompactTile(); - self.gpu_bridge.destroy(mesh); - self.allocator.destroy(mesh); - self.mesh_disposal.queue.items.len = idx; - } -} - -pub fn regionMemoryBytes(chunk: *const LODChunk, mesh: ?*LODMesh) usize { - var total: usize = 0; - switch (chunk.data) { - .simplified => |*s| total += s.totalMemoryBytes(), - else => {}, - } - if (mesh) |m| { - const memory = m.memorySnapshot(); - // Returning a pooled range does not release the renderer's backing - // buffer or CPU shadow, so it cannot lower the known memory total. - if (!memory.pooled) total += memory.capacity_bytes; - } - return total; -} - -pub fn enforceMemoryBudget(self: *Self) !void { - const budget_mb = self.config.getMemoryBudgetMB(); - if (budget_mb == 0) return; - const budget_bytes = @as(usize, budget_mb) * 1024 * 1024; - const hysteresis_low = (budget_bytes * 4) / 5; // 80% re-expand threshold - - // Decay path: comfortably under budget -> gradually re-expand radii. - if (self.memory_governor.used_bytes < hysteresis_low) { - const lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - var decayed = false; - for (&self.memory_governor.radius_shrink_chunks) |*s| { - if (s.* > 0) { - s.* -= 1; - decayed = true; - } - } - self.profiling.end(.manager_lock_hold, lock_hold_timer); - self.mutex.unlock(); - if (decayed) { - log.log.trace("LOD memory below 80% budget; re-expanding radii", .{}); - } - return; - } - - if (self.memory_governor.used_bytes <= budget_bytes) return; // 80-100% band: hold - - const Candidate = struct { key: LODRegionKey, distance_sq: i64 }; - var candidates = std.ArrayListUnmanaged(Candidate).empty; - defer candidates.deinit(self.allocator); - - const lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlock(); - - const player = self.loadPlayerChunkPos(); - const active_lod_count = lod_chunk.activeLODCount(self.config); - for (0..active_lod_count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const key = entry.key_ptr.*; - const chunk = entry.value_ptr.*; - if (chunk.getState() != .renderable or chunk.isPinned()) continue; - // Coarsest active LOD regions have no renderable parent fallback and are - // intentionally excluded so eviction never opens horizon holes. - const parent = key.parentKey() orelse continue; - const parent_idx = @intFromEnum(parent.lod); - const parent_chunk = self.regions[parent_idx].get(parent) orelse continue; - if (parent_chunk.getState() != .renderable) continue; - const bounds = key.chunkBounds(); - try candidates.append(self.allocator, .{ .key = key, .distance_sq = bounds.distanceSquaredToPoint(player.cx, player.cz) }); - } - } - - std.mem.sort(Candidate, candidates.items, {}, struct { - fn lt(_: void, a: Candidate, b: Candidate) bool { - return a.distance_sq > b.distance_sq; - } - }.lt); - - var used = self.memory_governor.used_bytes; - var evicted_count: usize = 0; - for (candidates.items) |candidate| { - if (used <= budget_bytes) break; - if (evicted_count >= MAX_MEMORY_EVICTIONS_PER_UPDATE) break; - const idx = @intFromEnum(candidate.key.lod); - const chunk = self.regions[idx].get(candidate.key) orelse continue; - if (chunk.getState() != .renderable or chunk.isPinned()) continue; - const mesh = self.meshes[idx].get(candidate.key); - const bytes = regionMemoryBytes(chunk, mesh); - self.noteRegionRemoved(candidate.key, chunk); - if (mesh) |m| { - self.queueMeshDeletion(m); - _ = self.meshes[idx].remove(candidate.key); - } - chunk.deinit(self.allocator); - self.allocator.destroy(chunk); - _ = self.regions[idx].remove(candidate.key); - used = if (bytes >= used) 0 else used - bytes; - self.memory_governor.used_bytes = used; - self.stats.evictions += 1; - evicted_count += 1; - } - - // Any eviction means the active radii are too ambitious for the current - // budget. Shrink finer bands immediately so evicted regions do not get - // queued again next update. The coarsest horizon band is exempt so the - // vista never develops holes. - if (evicted_count > 0 or self.memory_governor.used_bytes > budget_bytes) { - const active = lod_chunk.activeLODCount(self.config); - var grew = false; - var i: usize = 1; - while (i + 1 < active) : (i += 1) { - if (self.memory_governor.radius_shrink_chunks[i] < 64) { - self.memory_governor.radius_shrink_chunks[i] += 1; - grew = true; - } - } - if (grew) { - log.log.warn("LOD memory pressure; evicted {} regions this update and shrank finer radii (shrink={any})", .{ evicted_count, self.memory_governor.radius_shrink_chunks }); - } - } -} - -/// Update statistics -pub fn updateStats(self: *Self) void { - self.stats.reset(); - var source_data_cpu_bytes: usize = 0; - var direct_mesh_gpu_bytes: usize = 0; - var pending_cpu_upload_bytes: usize = 0; - var deferred_deletion_gpu_bytes: usize = 0; - var deferred_deletion_cpu_bytes: usize = 0; - var resident_region_count: usize = 0; - var unmaterialized_region_count: usize = 0; - - const lock_wait_timer = self.profiling.begin(); - self.mutex.lockShared(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlockShared(); - - for (0..LODLevel.count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const chunk = entry.value_ptr.*; - resident_region_count += 1; - self.stats.recordState(i, chunk.getState()); - - // Calculate actual memory usage for this chunk's data - switch (chunk.data) { - .simplified => |*s| { - source_data_cpu_bytes += s.totalMemoryBytes(); - }, - .empty => unmaterialized_region_count += 1, - .full => {}, - } - } - - // Direct meshes own a dedicated GPU buffer. Pooled mesh capacity is a - // sub-allocation and is accounted once by the renderer pool snapshot. - var mesh_iter = self.meshes[i].iterator(); - while (mesh_iter.next()) |entry| { - const mesh = entry.value_ptr.*; - const memory = mesh.memorySnapshot(); - self.stats.mesh_count[i] += 1; - self.stats.mesh_vertices[i] += memory.vertex_count; - pending_cpu_upload_bytes += memory.pending_upload_bytes; - if (!memory.pooled) direct_mesh_gpu_bytes += memory.capacity_bytes; - } - - self.stats.gen_queue_depth[i] = @intCast(self.job_dispatcher.queues[i].count()); - self.stats.upload_queue_depth[i] = @intCast(self.upload_queues[i].count()); - } - - // A queued mesh is no longer in the render map. Count its retained GPU - // allocation and pending CPU vertices independently; pooled allocations - // remain included in the pool capacity below and must not be double-counted. - for (self.mesh_disposal.queue.items) |mesh| { - const memory = mesh.memorySnapshot(); - deferred_deletion_gpu_bytes += memory.capacity_bytes; - deferred_deletion_cpu_bytes += memory.pending_upload_bytes; - if (!memory.pooled) direct_mesh_gpu_bytes += memory.capacity_bytes; - } - - const pool_memory = self.renderer.memoryStats(); - const known_memory_bytes = source_data_cpu_bytes + - direct_mesh_gpu_bytes + - pool_memory.pool_gpu_capacity_bytes + - pool_memory.pool_cpu_shadow_bytes + - // Compact tiles are sub-allocations of this production GPU pool. Its - // capacity is the real allocation retained by the renderer and must be - // governed alongside source data; allocated bytes would double-count it. - pool_memory.compact_pool_capacity_bytes + - pending_cpu_upload_bytes + - deferred_deletion_cpu_bytes; - const budget_bytes = @as(usize, self.config.getMemoryBudgetMB()) * 1024 * 1024; - const reservation_per_region = if (budget_bytes == 0) 0 else @min(budget_bytes, LOGICAL_LOD_REGION_RESERVATION_BYTES); - // Reserve conservatively only for regions that do not have measurable - // source data yet. Materialized regions are governed by their actual CPU - // and GPU footprint instead of a permanent per-region distance cap. - const admission_reservation_bytes = std.math.mul(usize, unmaterialized_region_count, reservation_per_region) catch std.math.maxInt(usize); - const logical_admission_bytes = std.math.add(usize, known_memory_bytes, admission_reservation_bytes) catch std.math.maxInt(usize); - self.stats.addMemory(known_memory_bytes); - self.stats.pool_gpu_capacity_bytes = @intCast(pool_memory.pool_gpu_capacity_bytes); - self.stats.pool_gpu_allocated_bytes = @intCast(pool_memory.pool_gpu_allocated_bytes); - self.stats.pool_gpu_slack_bytes = @intCast(pool_memory.pool_gpu_slack_bytes); - self.stats.pool_cpu_shadow_bytes = @intCast(pool_memory.pool_cpu_shadow_bytes); - self.stats.compact_pool_capacity_bytes = @intCast(pool_memory.compact_pool_capacity_bytes); - self.stats.compact_pool_allocated_bytes = @intCast(pool_memory.compact_pool_allocated_bytes); - self.stats.compact_pool_free_bytes = @intCast(pool_memory.compact_pool_free_bytes); - self.stats.compact_pool_retired_bytes = @intCast(pool_memory.compact_pool_retired_bytes); - self.stats.direct_mesh_gpu_bytes = @intCast(direct_mesh_gpu_bytes); - self.stats.source_data_cpu_bytes = @intCast(source_data_cpu_bytes); - self.stats.resident_region_count = @intCast(resident_region_count); - self.stats.logical_admission_reservation_bytes = @intCast(admission_reservation_bytes); - self.stats.logical_admission_bytes = @intCast(logical_admission_bytes); - self.stats.pending_cpu_upload_bytes = @intCast(pending_cpu_upload_bytes); - self.stats.deferred_deletion_gpu_bytes = @intCast(deferred_deletion_gpu_bytes); - self.stats.deferred_deletion_cpu_bytes = @intCast(deferred_deletion_cpu_bytes); - self.stats.store_hits = self.cache_hits; - self.stats.store_misses = self.cache_misses; - self.stats.cache_hits = self.cache_hits; - self.stats.cache_misses = self.cache_misses; - self.stats.cancelled_jobs = self.cancelled_jobs; - self.stats.generation_token_overflows = self.generation_tokens.overflowEvents(); - self.stats.transition_token_overflows = self.transition_tokens.overflowEvents(); - self.stats.fade_token_overflows = self.fade_tokens.overflowEvents(); - self.memory_governor.used_bytes = known_memory_bytes; - self.memory_governor.logical_admission_bytes = logical_admission_bytes; - self.profiling.setPendingCpuUploadBytes(pending_cpu_upload_bytes); - self.profiling.setMemoryAccounting( - pool_memory.pool_gpu_capacity_bytes, - pool_memory.pool_gpu_allocated_bytes, - pool_memory.pool_gpu_slack_bytes, - pool_memory.pool_cpu_shadow_bytes, - pool_memory.compact_pool_capacity_bytes, - pool_memory.compact_pool_allocated_bytes, - pool_memory.compact_pool_free_bytes, - pool_memory.compact_pool_retired_bytes, - direct_mesh_gpu_bytes, - known_memory_bytes, - ); - - if (engine_core.envFlag("ZIGCRAFT_LOD_DIAG", false)) { - const S = struct { - var counter: u64 = 0; - }; - S.counter += 1; - if (S.counter % 120 == 1) { - log.log.info("LOD_STATS_DIAG gen_q={any} upload_q={any} meshes={any} verts={any} cache_hits={} cache_misses={} cache_hit_rate={d:.2} mem_mb={} upload_failures={} store_hits={} store_misses={} evictions={} ingestion_backlog={} drawn={any} instances={any}", .{ - self.stats.gen_queue_depth, - self.stats.upload_queue_depth, - self.stats.mesh_count, - self.stats.mesh_vertices, - self.stats.cache_hits, - self.stats.cache_misses, - self.stats.cacheHitRate(), - self.stats.memory_used_mb, - self.stats.upload_failures, - self.stats.store_hits, - self.stats.store_misses, - self.stats.evictions, - self.stats.ingestion_backlog, - self.stats.drawn, - self.stats.instances, - }); - } - } -} - -/// Free LOD meshes where all underlying chunks are loaded -pub fn unloadLODWhereChunksLoaded(self: *Self, checker: ChunkChecker, ctx: *anyopaque) void { - // Lock exclusive because we modify meshes and regions maps - const lock_wait_timer = self.profiling.begin(); - self.mutex.lock(); - self.profiling.end(.manager_lock_wait, lock_wait_timer); - const lock_hold_timer = self.profiling.begin(); - defer self.profiling.end(.manager_lock_hold, lock_hold_timer); - defer self.mutex.unlock(); - - const active_lod_count = lod_chunk.activeLODCount(self.config); - for (0..active_lod_count) |i| { - const storage = &self.regions[i]; - const meshes = &self.meshes[i]; - - var to_remove = std.ArrayListUnmanaged(LODRegionKey).empty; - defer to_remove.deinit(self.allocator); - - var iter = meshes.iterator(); - while (iter.next()) |entry| { - if (storage.get(entry.key_ptr.*)) |chunk| { - // Don't unload if being processed (pinned) or not ready - if (chunk.isPinned() or chunk.getState() == .generating or chunk.getState() == .meshing or chunk.getState() == .uploading) continue; - - const bounds = chunk.worldBounds(); - if (self.areAllChunksLoaded(bounds, checker, ctx)) { - to_remove.append(self.allocator, entry.key_ptr.*) catch {}; - } - } - } - - for (to_remove.items) |rem_key| { - if (storage.get(rem_key)) |chunk| { - self.noteRegionRemoved(rem_key, chunk); - } - if (meshes.fetchRemove(rem_key)) |mesh_entry| { - // Queue for deferred deletion to avoid waitIdle stutter - self.queueMeshDeletion(mesh_entry.value); - } - if (storage.fetchRemove(rem_key)) |chunk_entry| { - if (chunk_entry.value.getState() != .missing and chunk_entry.value.getState() != .renderable and self.pending_region_count > 0) { - self.pending_region_count -= 1; - } - chunk_entry.value.deinit(self.allocator); - self.allocator.destroy(chunk_entry.value); - } - } - } -} - -/// Check if all chunks within the given world bounds are loaded and renderable. -/// Checks if all chunks within the LOD0 radius that could cover this LOD region -/// are loaded and renderable. Chunks outside the LOD0 radius are skipped since -/// they represent LOD terrain, not full-detail chunks that would cover this region. -pub fn areAllChunksLoaded(self: *Self, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool { - const chunk_radius: i64 = @as(i64, self.config.getChunkRenderRadius()); - const radius_sq = chunk_radius * chunk_radius; - const player = self.loadPlayerChunkPos(); - - const min_cx = @divFloor(bounds.min_x, CHUNK_SIZE_X) - CHUNK_COVERAGE_PADDING; - const min_cz = @divFloor(bounds.min_z, CHUNK_SIZE_Z) - CHUNK_COVERAGE_PADDING; - const max_cx = @divFloor(bounds.max_x, CHUNK_SIZE_X) - 1 + CHUNK_COVERAGE_PADDING; - const max_cz = @divFloor(bounds.max_z, CHUNK_SIZE_Z) - 1 + CHUNK_COVERAGE_PADDING; - - var cz = min_cz; - while (cz <= max_cz) : (cz += 1) { - var cx = min_cx; - while (cx <= max_cx) : (cx += 1) { - const dx: i64 = @as(i64, cx) - @as(i64, player.cx); - const dz: i64 = @as(i64, cz) - @as(i64, player.cz); - if (dx * dx + dz * dz > radius_sq) { - return false; - } - if (!checker(cx, cz, ctx)) { - return false; - } - } - } - return true; -} diff --git a/modules/world-lod/src/lod_manager_generation_ops.zig b/modules/world-lod/src/lod_manager_generation_ops.zig deleted file mode 100644 index 911bda79..00000000 --- a/modules/world-lod/src/lod_manager_generation_ops.zig +++ /dev/null @@ -1,715 +0,0 @@ -const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; -const fs = @import("fs"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const ILODConfig = lod_chunk.ILODConfig; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const Vertex = @import("engine-rhi").Vertex; -const engine_core = @import("engine-core"); -const log = engine_core.log; -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; -const RingBuffer = engine_core.ring_buffer.RingBuffer; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_tile = @import("lod_tile.zig"); -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const ChunkChecker = lod_gpu.ChunkChecker; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const TextureAtlas = @import("engine-assets").TextureAtlas; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = @import("lod_stats.zig").LODStats; -const manager_ctx = @import("lod_manager_context.zig"); -const ChunkCoordKey = manager_ctx.ChunkCoordKey; -const ChunkCoordKeyContext = manager_ctx.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const ChunkResolver = manager_ctx.ChunkResolver; -const PendingIngestion = manager_ctx.PendingIngestion; -const PlayerChunkPos = manager_ctx.PlayerChunkPos; -const GenerationCandidate = manager_ctx.GenerationCandidate; -const LifecycleStage = manager_ctx.LifecycleStage; -const LifecycleToken = manager_ctx.LifecycleToken; -const MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE = manager_ctx.MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE; -const LIFECYCLE_RECONCILIATION_INTERVAL = manager_ctx.LIFECYCLE_RECONCILIATION_INTERVAL; -const MAX_CACHE_LOADS_PER_UPDATE = manager_ctx.MAX_CACHE_LOADS_PER_UPDATE; -const MAX_MEMORY_EVICTIONS_PER_UPDATE = manager_ctx.MAX_MEMORY_EVICTIONS_PER_UPDATE; -const MAX_MESH_DELETIONS_PER_SWEEP = manager_ctx.MAX_MESH_DELETIONS_PER_SWEEP; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = manager_ctx.DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const LOGICAL_LOD_REGION_RESERVATION_BYTES = manager_ctx.LOGICAL_LOD_REGION_RESERVATION_BYTES; -const LOD_UPLOAD_BUDGET_ENV = manager_ctx.LOD_UPLOAD_BUDGET_ENV; -const LOD_UPDATE_DIVISOR = manager_ctx.LOD_UPDATE_DIVISOR; -const DELETION_SWEEP_SECONDS = manager_ctx.DELETION_SWEEP_SECONDS; -const CHUNK_COVERAGE_PADDING = manager_ctx.CHUNK_COVERAGE_PADDING; -const MIN_LOD_WORKERS = manager_ctx.MIN_LOD_WORKERS; -const MAX_LOD_WORKERS = manager_ctx.MAX_LOD_WORKERS; -const MAX_PENDING_INGESTIONS = manager_ctx.MAX_PENDING_INGESTIONS; -const PENDING_INGESTION_TTL = manager_ctx.PENDING_INGESTION_TTL; -const EDIT_FLUSH_COOLDOWN = manager_ctx.EDIT_FLUSH_COOLDOWN; -const LOD_FRAME_DT_APPROX = manager_ctx.LOD_FRAME_DT_APPROX; -const lodUploadBudgetBytes = manager_ctx.lodUploadBudgetBytes; -const wouldExceedUploadBudget = manager_ctx.wouldExceedUploadBudget; -const isUploadPressureError = manager_ctx.isUploadPressureError; - -pub fn queueLODRegions(self: *Self, lod: LODLevel, velocity: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque) !void { - const player = self.loadPlayerChunkPos(); - self.mutex.lock(); - const radii = self.config.getRadii(); - const active_lod_count = lod_chunk.activeLODCount(self.config); - const use_vertical_spans = self.config.getVerticalSpanBudget() > 0 and self.effectiveMeshPath(lod) == .column_spans; - const memory_budget_bytes = @as(usize, self.config.getMemoryBudgetMB()) * 1024 * 1024; - self.mutex.unlock(); - - const Coverage = struct { - fn areAllLoaded(ptr: *anyopaque, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool { - const mgr: *Self = @ptrCast(@alignCast(ptr)); - return mgr.areAllChunksLoaded(bounds, checker, ctx); - } - }; - return lod_scheduler.queueLODRegions(.{ - .allocator = self.allocator, - .config = self.config, - .radii = radii, - .active_lod_count = active_lod_count, - .regions = &self.regions, - .gen_queues = &self.job_dispatcher.queues, - .mutex = &self.mutex, - .player_cx = player.cx, - .player_cz = player.cz, - .scan_states = &self.scan_states, - .next_job_token = &self.job_dispatcher.next_token, - .cleanup_covered_regions = self.cleanup_covered_regions, - .coverage_ptr = self, - .are_all_chunks_loaded = Coverage.areAllLoaded, - .radius_reduction = &self.memory_governor.radius_shrink_chunks, - // Route every region through the bounded admission path, whether or - // not persistent LOD caching is enabled. - .defer_generation_dispatch = true, - .pending_regions = &self.pending_region_count, - // Distance is not bounded by a fixed region count. The logical-memory - // reservation below provides the actual resource-based backpressure. - .resident_region_limit = std.math.maxInt(usize), - .logical_memory_limit_bytes = if (memory_budget_bytes == 0) std.math.maxInt(usize) else memory_budget_bytes, - .logical_memory_bytes = &self.memory_governor.logical_admission_bytes, - .logical_region_reservation_bytes = if (memory_budget_bytes == 0) 0 else @min(memory_budget_bytes, LOGICAL_LOD_REGION_RESERVATION_BYTES), - .use_vertical_spans = use_vertical_spans, - .generation_tokens = &self.generation_tokens, - }, lod, velocity, chunk_checker, checker_ctx); -} - -pub fn processQueuedGenerations(self: *Self, velocity: Vec3) !void { - const cache_path = self.cacheDirPathSnapshot(); - defer if (cache_path) |path| self.allocator.free(path); - var cache_reads: usize = 0; - var processed: usize = 0; - while (processed < MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE) : (processed += 1) { - const token = self.generation_tokens.pop() orelse break; - const candidate = generationCandidateFromToken(self, token, velocity) orelse continue; - if (cache_path) |path| { - if (cache_reads < MAX_CACHE_LOADS_PER_UPDATE) { - cache_reads += 1; - self.mutex.lock(); - if (candidate.chunk.getState() == .queued_for_generation and candidate.chunk.job_token == candidate.job_token and candidate.chunk.source_revision == token.source_revision and !candidate.chunk.cache_read_queued) { - candidate.chunk.cache_read_queued = true; - const accepted = self.cache_io.enqueueRead(path, candidate.key, self.cacheKey(candidate.key), candidate.job_token) catch false; - if (accepted) { - self.mutex.unlock(); - continue; - } - candidate.chunk.cache_read_queued = false; - } - self.mutex.unlock(); - } - } - - try dispatchGenerationCandidate(self, candidate); - } -} - -pub fn dispatchCacheMiss(self: *Self, key: LODRegionKey, token: u32) void { - const lod_idx = @intFromEnum(key.lod); - self.mutex.lock(); - const chunk = self.regions[lod_idx].get(key) orelse { - self.mutex.unlock(); - return; - }; - if (chunk.getState() != .queued_for_generation or chunk.job_token != token) { - self.mutex.unlock(); - return; - } - const scale: i32 = @intCast(key.lod.chunksPerSide()); - const candidate = GenerationCandidate{ - .key = key, - .chunk = chunk, - .encoded_priority = chunk.job_priority, - .level = @intCast(lod_idx), - .coord_scale = scale, - .job_token = token, - .lod_radius = self.config.getRadii()[lod_idx], - .want_spans = self.config.getVerticalSpanBudget() > 0 and self.effectiveMeshPath(key.lod) == .column_spans, - }; - self.mutex.unlock(); - dispatchGenerationCandidate(self, candidate) catch |err| { - log.log.warn("Failed to dispatch cache-miss LOD{} generation ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - }; -} - -fn generationCandidateFromToken(self: *Self, token: LifecycleToken, velocity: Vec3) ?GenerationCandidate { - _ = velocity; - if (token.stage != .generation) return null; - const lod_idx = @intFromEnum(token.key.lod); - self.mutex.lock(); - defer self.mutex.unlock(); - const chunk = self.regions[lod_idx].get(token.key) orelse return null; - if (chunk.getState() != .queued_for_generation or !token.matches(chunk)) return null; - const scale: i32 = @intCast(token.key.lod.chunksPerSide()); - return .{ - .key = token.key, - .chunk = chunk, - .encoded_priority = chunk.job_priority, - .level = @intCast(lod_idx), - .coord_scale = scale, - .job_token = token.job_token, - .lod_radius = self.config.getRadii()[lod_idx], - .want_spans = self.config.getVerticalSpanBudget() > 0 and self.effectiveMeshPath(token.key.lod) == .column_spans, - }; -} - -fn dispatchGenerationCandidate(self: *Self, candidate: GenerationCandidate) !void { - self.mutex.lock(); - if (candidate.chunk.getState() != .queued_for_generation or candidate.chunk.job_token != candidate.job_token or candidate.chunk.cache_read_queued) { - self.mutex.unlock(); - return; - } - candidate.chunk.resetCancellation(); - candidate.chunk.setState(.generating); - self.mutex.unlock(); - - const dispatch_timer = self.profiling.begin(); - self.job_dispatcher.queues[LODLevel.count - 1].push(.{ - .type = .chunk_generation, - .dist_sq = candidate.encoded_priority, - .data = .{ .chunk = .{ - .x = candidate.chunk.region_x, - .z = candidate.chunk.region_z, - .job_token = candidate.job_token, - .lod_level = candidate.level, - .coord_scale = candidate.coord_scale, - .preserve_priority = candidate.chunk.preserve_job_priority, - .lod_radius = candidate.lod_radius, - .use_vertical_spans = candidate.want_spans, - } }, - }) catch |err| { - self.profiling.end(.generation_dispatch, dispatch_timer); - self.mutex.lock(); - if (candidate.chunk.getState() == .generating and candidate.chunk.job_token == candidate.job_token) candidate.chunk.setState(.queued_for_generation); - self.mutex.unlock(); - return err; - }; - self.profiling.end(.generation_dispatch, dispatch_timer); -} - -/// Process state transitions (generated -> meshing -> ready) -pub fn processStateTransitions(self: *Self, velocity: Vec3) !void { - _ = velocity; - var processed: usize = 0; - while (processed < MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE) : (processed += 1) { - const token = self.transition_tokens.pop() orelse break; - const lod_idx = @intFromEnum(token.key.lod); - // Transition and enqueue atomically with respect to workers. A worker - // may pop immediately, but cannot inspect the chunk until this short - // manager critical section publishes the matching state. - self.mutex.lock(); - const chunk = self.regions[lod_idx].get(token.key) orelse { - self.mutex.unlock(); - continue; - }; - if (!token.matches(chunk) or chunk.isPinned()) { - self.mutex.unlock(); - continue; - } - if (token.stage == .upload) { - if (chunk.getState() != .mesh_ready) { - self.mutex.unlock(); - continue; - } - if (self.meshes[lod_idx].get(token.key)) |mesh| patchCompactAprons(self, lod_idx, token.key, mesh); - chunk.setState(.uploading); - self.upload_queues[lod_idx].push(chunk) catch |err| { - chunk.setState(.mesh_ready); - self.mutex.unlock(); - return err; - }; - self.mutex.unlock(); - continue; - } - if (token.stage != .mesh or chunk.getState() != .generated) { - self.mutex.unlock(); - continue; - } - const scale: i32 = @intCast(token.key.lod.chunksPerSide()); - chunk.setState(.meshing); - chunk.resetCancellation(); - self.job_dispatcher.queues[LODLevel.count - 1].push(.{ - .type = .chunk_meshing, - .dist_sq = token.priority, - .data = .{ - .chunk = .{ - .x = chunk.region_x, - .z = chunk.region_z, - .job_token = token.job_token, - .lod_level = @intCast(lod_idx), - .coord_scale = scale, - .preserve_priority = chunk.preserve_job_priority, - .lod_radius = self.config.getRadii()[lod_idx], - }, - }, - }) catch |err| { - if (chunk.getState() == .meshing and chunk.job_token == token.job_token) { - chunk.setState(.generated); - } - self.mutex.unlock(); - return err; - }; - // Never let a worker invoke RHI or release a live pooled range. Once - // the replacement job is guaranteed to be queued, detach the previous - // representation and retire it through the normal frame-delayed - // disposal queue. The worker will create a fresh mesh object. - if (self.meshes[lod_idx].fetchRemove(token.key)) |old_mesh| { - self.queueMeshDeletion(old_mesh.value); - } - self.mutex.unlock(); - } - if (self.update_tick % LIFECYCLE_RECONCILIATION_INTERVAL == 0) reconcileLifecycleTokens(self); -} - -/// Explicit, infrequent fallback for queue overflow or legacy state changes. -/// Normal work is entirely token-driven; this sweep is intentionally the only -/// resident-map lifecycle scan and is observable in code/reports. -fn reconcileLifecycleTokens(self: *Self) void { - self.mutex.lock(); - defer self.mutex.unlock(); - for (&self.regions) |*regions| { - var it = regions.iterator(); - while (it.next()) |entry| { - const chunk = entry.value_ptr.*; - if (chunk.transition_frames_remaining > 0) self.enqueueFade(entry.key_ptr.*, chunk); - const stage: LifecycleStage = switch (chunk.getState()) { - .queued_for_generation => .generation, - .generated => .mesh, - .mesh_ready => .upload, - else => continue, - }; - const token = LifecycleToken{ - .key = entry.key_ptr.*, - .job_token = chunk.job_token, - .source_revision = chunk.source_revision, - .priority = chunk.job_priority, - .stage = stage, - }; - if (stage == .generation) { - _ = self.generation_tokens.push(self.allocator, token) catch false; - } else { - _ = self.transition_tokens.push(self.allocator, token) catch false; - } - } - } -} - -pub fn enqueueTransition(self: *Self, key: LODRegionKey, chunk: *const LODChunk, stage: LifecycleStage) void { - const token = LifecycleToken{ - .key = key, - .job_token = chunk.job_token, - .source_revision = chunk.source_revision, - .priority = chunk.job_priority, - .stage = stage, - }; - if (stage == .generation) { - _ = self.generation_tokens.push(self.allocator, token) catch false; - } else { - _ = self.transition_tokens.push(self.allocator, token) catch false; - } -} - -pub fn enqueueFade(self: *Self, key: LODRegionKey, chunk: *const LODChunk) void { - if (chunk.transition_frames_remaining == 0) return; - _ = self.fade_tokens.push(self.allocator, .{ - .key = key, - .job_token = chunk.job_token, - .source_revision = chunk.source_revision, - .priority = 0, - .stage = .fade, - }) catch false; -} - -fn patchCompactAprons(self: *Self, lod_index: usize, key: LODRegionKey, mesh: *LODMesh) void { - const neighbors = [_]struct { dx: i32, dz: i32, edge: lod_tile.TileEdge }{ - .{ .dx = -1, .dz = 0, .edge = .west }, - .{ .dx = 1, .dz = 0, .edge = .east }, - .{ .dx = 0, .dz = -1, .edge = .north }, - .{ .dx = 0, .dz = 1, .edge = .south }, - }; - for (neighbors) |neighbor| { - const neighbor_key = LODRegionKey{ .rx = key.rx + neighbor.dx, .rz = key.rz + neighbor.dz, .lod = key.lod }; - const neighbor_mesh = self.meshes[lod_index].get(neighbor_key) orelse continue; - _ = mesh.patchCompactNeighbor(neighbor.edge, neighbor_mesh); - } -} - -pub fn getOrCreateMesh(self: *Self, key: LODRegionKey) !*LODMesh { - self.mutex.lock(); - defer self.mutex.unlock(); - - const lod_idx = @intFromEnum(key.lod); - if (lod_idx >= LODLevel.count) return error.InvalidLODLevel; - - const meshes = &self.meshes[lod_idx]; - - if (meshes.get(key)) |mesh| { - return mesh; - } - - const mesh = try self.allocator.create(LODMesh); - errdefer self.allocator.destroy(mesh); - mesh.* = LODMesh.init(self.allocator, key.lod); - try meshes.put(key, mesh); - return mesh; -} - -/// Build mesh for an LOD chunk (called after generation completes) -pub fn buildMeshForChunk(self: *Self, chunk: *LODChunk) !void { - // A meshing worker pins its chunk while it owns the immutable source data. - // Ingestion defers writes to .meshing chunks and eviction skips pinned - // chunks, so retaining the manager lock through mesh construction is both - // unnecessary and a substantial source of contention. - std.debug.assert(chunk.isPinned()); - std.debug.assert(chunk.getState() == .meshing); - - const key = LODRegionKey{ - .rx = chunk.region_x, - .rz = chunk.region_z, - .lod = chunk.lodLevel(), - }; - - const mesh = try self.getOrCreateMesh(key); - mesh.setSourceIdentity(chunk.job_token, chunk.source_revision); - - switch (chunk.data) { - .simplified => |*data| { - const bounds = chunk.worldBounds(); - // Compact tiles carry water samples too. Unsupported mixed-shore - // topology is rejected by `buildCompactTile` and immediately uses - // the maintained expanded CPU fallback. - if (shouldUseCompactTiles(self, chunk)) { - self.profiling.addCompactSelected(); - mesh.buildCompactTile(data) catch |err| switch (err) { - error.UnsupportedSourceFeatures => { - self.profiling.addCompactBuildRejected(); - const support = @import("lod_tile.zig").CompactLODTile.support(data, chunk.lodLevel()); - log.log.warn("LOD{} compact build rejected for ({}, {}): {s}", .{ @intFromEnum(chunk.lodLevel()), chunk.region_x, chunk.region_z, @tagName(support) }); - if (engine_core.envFlag("ZIGCRAFT_LOD_COMPACT_DIAG", false)) { - std.debug.print("LOD_COMPACT: event=build_rejected lod={} region=({}, {}) reason={s}\n", .{ @intFromEnum(chunk.lodLevel()), chunk.region_x, chunk.region_z, @tagName(support) }); - } - }, - else => { - self.profiling.addCompactBuildRejected(); - return err; - }, - }; - if (mesh.isCompact()) { - return; - } - } - switch (self.effectiveMeshPath(chunk.lodLevel())) { - .heightfield => try mesh.buildFromSimplifiedData(data, bounds.min_x, bounds.min_z, self.atlas), - .column_spans => try mesh.buildFromColumnSpans(data, bounds.min_x, bounds.min_z, self.atlas), - .qem => { - const lod = chunk.lodLevel(); - const horizontal_detail = self.config.getHorizontalDetail(lod); - const detail_target = horizontal_detail * horizontal_detail; - const target = @max(self.config.getQEMTarget(lod), detail_target); - if (target == 0) { - try mesh.buildFromSimplifiedData(data, bounds.min_x, bounds.min_z, self.atlas); - } else { - try mesh.buildFromSimplifiedDataWithQEM(data, bounds.min_x, bounds.min_z, target, self.config.getQEMMinInputTriangles(), self.atlas); - } - }, - } - }, - .full => { - // LOD0 meshes handled by World, not LODManager - }, - .empty => { - // No data to build mesh from - }, - } -} - -/// Converts an unrenderable compact upload to the established CPU heightfield -/// route. The upload task pins `chunk`, so its simplified source is stable for -/// this synchronous recovery and can immediately be requeued without a hole. -/// -/// CPU construction happens before compact retirement. If allocation fails, -/// the existing compact samples remain renderable and retryable. On success, -/// detach the new CPU payload across `destroy`: the bridge clears pending -/// vertices and the production compact pool clears all compact draw counts. -pub fn fallbackCompactMeshToCpu(self: *Self, mesh: *LODMesh, chunk: *LODChunk) !void { - std.debug.assert(mesh.isCompact()); - switch (chunk.data) { - .simplified => |*data| { - const bounds = chunk.worldBounds(); - try mesh.buildFromSimplifiedData(data, bounds.min_x, bounds.min_z, self.atlas); - }, - else => return error.InvalidState, - } - var cpu_build = mesh.takePendingCpuBuild(); - - self.gpu_bridge.destroy(mesh); - // Lightweight test bridges may only release their own handle. Make the - // transition deterministic in both cases without touching cpu_build. - mesh.clearRetiredState(); - mesh.restorePendingCpuBuild(&cpu_build); -} - -fn shouldUseCompactTiles(self: *Self, chunk: *const LODChunk) bool { - if (chunk.compact_disabled) return false; - if (!self.config.getCompactTilesEnabled()) return false; - const lod = chunk.lodLevel(); - if (lod != .lod3 and lod != .lod4) return false; - const mode = engine_core.getenv("ZIGCRAFT_LOD_COMPACT") orelse "auto"; - if (std.ascii.eqlIgnoreCase(mode, "off")) return false; - // Auto requires the immutable descriptor snapshots used by compact - // terrain/water GPU-culling streams, not only vertex-pulling support. - if (std.ascii.eqlIgnoreCase(mode, "auto")) return self.gpu_bridge.supportsCompactGpuCulling(); - if (std.ascii.eqlIgnoreCase(mode, "force")) return self.gpu_bridge.supportsCompact(); - return false; -} - -pub fn effectiveMeshPath(self: *Self, lod: LODLevel) lod_chunk.LODMeshPath { - // Far bands stay as high-resolution stepped block columns. LOD2 keeps - // the richer span path so mid-distance cliffs/trees remain voxel-like - // without turning the terrain into a smooth polygon surface. - if (@intFromEnum(lod) >= @intFromEnum(LODLevel.lod3)) return .heightfield; - if (lod == LODConfig.coarsestLOD()) return .heightfield; - if (engine_core.envFlag("ZIGCRAFT_LOD_MESH_PATH_QEM", false)) return .qem; - if (engine_core.envFlag("ZIGCRAFT_LOD_MESH_PATH_SPANS", false)) return .column_spans; - return self.config.getMeshPath(); -} - -/// Worker pool callback for LOD tasks (generation and meshing) -pub fn processLODJob(ctx: *anyopaque, job: Job) void { - const self: *Self = @ptrCast(@alignCast(ctx)); - - const lod_level: LODLevel = @enumFromInt(job.data.chunk.lod_level); - const key = LODRegionKey{ - .rx = job.data.chunk.x, - .rz = job.data.chunk.z, - .lod = lod_level, - }; - - const lod_idx = @intFromEnum(lod_level); - - // Phase 1: Acquire lock, validate job, pin chunk - self.mutex.lock(); - const storage = &self.regions[lod_idx]; - - const chunk = storage.get(key) orelse { - self.mutex.unlock(); - return; - }; - - // Reject invalidated work before any state reconciliation. A stale queued - // job must never demote a newer job for the same region. - if (chunk.job_token != job.data.chunk.job_token) { - self.mutex.unlock(); - return; - } - - // Stale job check (too far from player) - const player = self.loadPlayerChunkPos(); - const radius = job.data.chunk.lod_radius; - const use_vertical_spans = job.data.chunk.use_vertical_spans; - const job_key = LODRegionKey{ - .rx = job.data.chunk.x, - .rz = job.data.chunk.z, - .lod = lod_level, - }; - - if (!job_key.chunkBounds().intersectsRadius(player.cx, player.cz, radius)) { - if (chunk.getState() == .generating or chunk.getState() == .meshing) { - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(.missing); - } - self.mutex.unlock(); - return; - } - - // Check state and capture job type before releasing lock - const current_state = chunk.getState(); - const job_type = job.type; - - // Validate state matches expected for job type - const valid_state = switch (job_type) { - .chunk_generation => current_state == .generating, - .chunk_meshing => current_state == .meshing, - else => false, - }; - - if (!valid_state) { - self.mutex.unlock(); - return; - } - - // Check if we need to generate data (while still holding lock) - const needs_data_init = (job_type == .chunk_generation and chunk.data != .simplified); - - // Pin chunk during operation (prevents unload) - chunk.pin(); - self.mutex.unlock(); - - // Phase 2: Do expensive work without lock - var success = false; - var new_state: LODState = .missing; - // Only a job that publishes its representation may contribute far-path - // evidence; stale and retried work is intentionally excluded below. - const far_representation_timer = self.profiling.begin(); - - switch (job_type) { - .chunk_generation => { - const generation_timer = self.profiling.begin(); - defer self.profiling.end(.worker_generation, generation_timer); - // Initialize simplified data if needed - if (needs_data_init) { - var data = if (use_vertical_spans) - LODSimplifiedData.initWithVerticalSpansSampleDensity(self.allocator, lod_level, self.config.getSampleDensity(lod_level)) catch { - new_state = .missing; - self.mutex.lock(); - if (chunk.job_token == job.data.chunk.job_token) { - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(new_state); - } - chunk.unpin(); - self.mutex.unlock(); - return; - } - else - LODSimplifiedData.initWithSampleDensity(self.allocator, lod_level, self.config.getSampleDensity(lod_level)) catch { - new_state = .missing; - self.mutex.lock(); - if (chunk.job_token == job.data.chunk.job_token) { - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(new_state); - } - chunk.unpin(); - self.mutex.unlock(); - return; - }; - - // Generate heightmap data (expensive, done without lock). - // Pass the region cancellation signal so pause and teleport - // can interrupt a multi-second coarse-LOD generation loop. - // Teardown sets both the manager flag and every region signal. - self.generator.generateHeightmapOnly(&data, chunk.region_x, chunk.region_z, lod_level, &chunk.cancel_requested); - - // A parent can contribute only when its grid spacing exactly - // matches this child. This preserves seam samples and avoids - // copying a different coarse footprint into refinement data. - self.mutex.lock(); - if (key.parentKey()) |parent_key| { - if (self.regions[@intFromEnum(parent_key.lod)].get(parent_key)) |parent| switch (parent.data) { - .simplified => |*parent_data| { - const child_x: u1 = @intCast(@mod(chunk.region_x, 2)); - const child_z: u1 = @intCast(@mod(chunk.region_z, 2)); - _ = data.reuseAlignedParentSamples(parent_data, child_x, child_z); - }, - else => {}, - }; - } - self.mutex.unlock(); - - // If generation was aborted, discard the partial data - // and leave the chunk in .missing so it re-generates later. - if (self.job_dispatcher.stop_flag.load(.acquire) or chunk.cancellationRequested()) { - data.deinit(); - new_state = .missing; - self.mutex.lock(); - if (chunk.job_token == job.data.chunk.job_token) { - if (self.pending_region_count > 0) self.pending_region_count -= 1; - chunk.setState(new_state); - } - chunk.unpin(); - self.mutex.unlock(); - return; - } - - // Acquire lock to update chunk data. A cache read or forced - // save-time edit may have published source while this worker - // was generating. Never let stale worldgen replace that newer - // authoritative snapshot. - self.mutex.lock(); - if (chunk.data == .simplified) { - data.deinit(); - } else { - chunk.data = .{ .simplified = data }; - chunk.updateHeightBoundsFromData(); - chunk.markSourceDirty(); - } - self.mutex.unlock(); - } - success = true; - new_state = .generated; - }, - .chunk_meshing => { - const mesh_timer = self.profiling.begin(); - defer self.profiling.end(.worker_mesh_construction, mesh_timer); - // Build mesh (expensive, done without lock) - // Note: buildMeshForChunk -> getOrCreateMesh acquires its own lock - self.buildMeshForChunk(chunk) catch |err| { - log.log.errWithTrace("Failed to build LOD{} async mesh: {}", .{ @intFromEnum(lod_level), err }); - new_state = .generated; // Retry later - self.mutex.lock(); - if (chunk.job_token == job.data.chunk.job_token) chunk.setState(new_state); - chunk.unpin(); - self.mutex.unlock(); - return; - }; - success = true; - new_state = .mesh_ready; - }, - else => unreachable, - } - - // Phase 3: Acquire lock briefly to update state - self.mutex.lock(); - if (success and chunk.job_token == job.data.chunk.job_token) { - chunk.setState(new_state); - if (new_state == .mesh_ready and (lod_level == .lod3 or lod_level == .lod4)) { - const mesh = self.meshes[lod_idx].get(key) orelse unreachable; - self.profiling.end(if (mesh.isCompact()) .worker_compact_encode else .worker_far_expanded_mesh_construction, far_representation_timer); - } - if (new_state == .generated) self.enqueueTransition(key, chunk, .mesh); - if (new_state == .mesh_ready) self.enqueueTransition(key, chunk, .upload); - } - chunk.unpin(); - self.mutex.unlock(); -} diff --git a/modules/world-lod/src/lod_manager_ingestion_ops.zig b/modules/world-lod/src/lod_manager_ingestion_ops.zig deleted file mode 100644 index ef558189..00000000 --- a/modules/world-lod/src/lod_manager_ingestion_ops.zig +++ /dev/null @@ -1,406 +0,0 @@ -const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; -const fs = @import("fs"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const ILODConfig = lod_chunk.ILODConfig; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const Vertex = @import("engine-rhi").Vertex; -const engine_core = @import("engine-core"); -const log = engine_core.log; -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; -const RingBuffer = engine_core.ring_buffer.RingBuffer; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const ChunkChecker = lod_gpu.ChunkChecker; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const TextureAtlas = @import("engine-assets").TextureAtlas; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = @import("lod_stats.zig").LODStats; -const manager_ctx = @import("lod_manager_context.zig"); -const ChunkCoordKey = manager_ctx.ChunkCoordKey; -const ChunkCoordKeyContext = manager_ctx.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const ChunkResolver = manager_ctx.ChunkResolver; -const PendingIngestion = manager_ctx.PendingIngestion; -const PlayerChunkPos = manager_ctx.PlayerChunkPos; -const MAX_CACHE_LOADS_PER_UPDATE = manager_ctx.MAX_CACHE_LOADS_PER_UPDATE; -const MAX_MEMORY_EVICTIONS_PER_UPDATE = manager_ctx.MAX_MEMORY_EVICTIONS_PER_UPDATE; -const MAX_MESH_DELETIONS_PER_SWEEP = manager_ctx.MAX_MESH_DELETIONS_PER_SWEEP; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = manager_ctx.DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const LOD_UPLOAD_BUDGET_ENV = manager_ctx.LOD_UPLOAD_BUDGET_ENV; -const LOD_UPDATE_DIVISOR = manager_ctx.LOD_UPDATE_DIVISOR; -const DELETION_SWEEP_SECONDS = manager_ctx.DELETION_SWEEP_SECONDS; -const CHUNK_COVERAGE_PADDING = manager_ctx.CHUNK_COVERAGE_PADDING; -const MIN_LOD_WORKERS = manager_ctx.MIN_LOD_WORKERS; -const MAX_LOD_WORKERS = manager_ctx.MAX_LOD_WORKERS; -const MAX_PENDING_INGESTIONS = manager_ctx.MAX_PENDING_INGESTIONS; -const EDIT_FLUSH_COOLDOWN = manager_ctx.EDIT_FLUSH_COOLDOWN; -const LOD_FRAME_DT_APPROX = manager_ctx.LOD_FRAME_DT_APPROX; -const lodUploadBudgetBytes = manager_ctx.lodUploadBudgetBytes; -const wouldExceedUploadBudget = manager_ctx.wouldExceedUploadBudget; -const isUploadPressureError = manager_ctx.isUploadPressureError; - -pub fn setChunkResolver(self: *Self, resolver: ChunkResolver) void { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - self.ingestion_queue.chunk_resolver = resolver; -} - -/// Ingest a real chunk into every LOD level whose region contains it. The -/// chunk is downsampled into each region's source data with the given -/// provenance; higher provenance always wins. Regions that are missing or -/// currently in-flight are recorded as pending and replayed from -/// `update()`. Safe to call from the generation worker thread; the caller -/// must pin the chunk for the duration of the call. -pub fn ingestChunk(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, provenance: LODColumnProvenance) void { - const pending_mask = applyIngestionToRegionsMask(self, cx, cz, chunk, provenance, activeIngestionMask(self)); - if (pending_mask != 0) { - self.ingestion_queue.mutex.lock(); - const recorded = self.recordPendingLocked(cx, cz, provenance, pending_mask); - if (!recorded and provenance == .edited) { - // Preserve discoverability for persistence invalidation when the - // bounded pending queue is saturated entirely by edited work. - self.ingestion_queue.edit_dirty.put(.{ .cx = cx, .cz = cz }, {}) catch {}; - } - self.ingestion_queue.mutex.unlock(); - } -} - -/// Record a deferred ingestion without a chunk pointer. The chunk is -/// resolved later via `chunk_resolver` from `update()`. Used for chunk -/// loads that happen before their LOD region exists. -pub fn requestIngestion(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance) void { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - const recorded = self.recordPendingLocked(cx, cz, provenance, activeIngestionMask(self)); - if (!recorded and provenance == .edited) { - self.ingestion_queue.edit_dirty.put(.{ .cx = cx, .cz = cz }, {}) catch {}; - } -} - -/// Notify the LOD system that a block edit affected chunk (cx, cz). -/// Coalesced on a short cooldown and re-ingested with the `edited` -/// provenance so distant terrain reflects player changes after teleport. -pub fn markChunkEdited(self: *Self, cx: i32, cz: i32) void { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - self.ingestion_queue.edit_dirty.put(.{ .cx = cx, .cz = cz }, {}) catch |err| { - log.log.warn("Failed to track edited chunk for LOD ingestion ({}, {}): {}", .{ cx, cz, err }); - }; -} - -/// Consumes any queued edit work for a chunk that is about to leave full-detail -/// storage, then applies its final authoritative snapshot to every currently -/// available LOD region. Deferred entries are removed because their resolver -/// would become invalid as soon as the caller completes the unload. -pub fn flushEditedChunkForUnload(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, retain_pending: bool) u8 { - var requested_mask: u8 = 0; - self.ingestion_queue.mutex.lock(); - if (self.ingestion_queue.edit_dirty.remove(.{ .cx = cx, .cz = cz })) { - requested_mask = activeIngestionMask(self); - } - - var index: usize = 0; - while (index < self.ingestion_queue.pending_ingestions.items.len) { - const pending = self.ingestion_queue.pending_ingestions.items[index]; - if (pending.cx == cx and pending.cz == cz and pending.provenance == .edited) { - requested_mask |= pending.pending_levels; - _ = self.ingestion_queue.pending_ingestions.orderedRemove(index); - continue; - } - index += 1; - } - self.ingestion_queue.mutex.unlock(); - - if (requested_mask == 0) return 0; - const pending_mask = applyIngestionToRegionsMask(self, cx, cz, chunk, .edited, requested_mask); - if (pending_mask != 0 and retain_pending) { - self.ingestion_queue.mutex.lock(); - const recorded = self.recordPendingLocked(cx, cz, .edited, pending_mask); - if (!recorded) { - self.ingestion_queue.edit_dirty.put(.{ .cx = cx, .cz = cz }, {}) catch {}; - } - self.ingestion_queue.mutex.unlock(); - } - return pending_mask; -} - -/// Apply one chunk's contribution to every LOD region that already has -/// source data and is not in-flight. Returns a bitmask of LOD levels that -/// could not be applied (region missing, not yet generated, or meshing) -/// so the caller can record them as pending. -pub fn applyIngestionToRegions(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, provenance: LODColumnProvenance) u8 { - return applyIngestionToRegionsMask(self, cx, cz, chunk, provenance, activeIngestionMask(self)); -} - -fn applyIngestionToRegionsMask(self: *Self, cx: i32, cz: i32, chunk: *const Chunk, provenance: LODColumnProvenance, requested_mask: u8) u8 { - var pending_mask: u8 = 0; - const active = lod_chunk.activeLODCount(self.config); - - self.mutex.lock(); - defer self.mutex.unlock(); - - var i: usize = 1; - while (i < active) : (i += 1) { - const level_mask = @as(u8, 1) << @intCast(i); - if (requested_mask & level_mask == 0) continue; - const lod: LODLevel = @enumFromInt(@as(u3, @intCast(i))); - const key = LODRegionKey.fromChunkCoords(cx, cz, lod); - const lod_chunk_ptr = self.regions[i].get(key) orelse { - pending_mask |= level_mask; - continue; - }; - switch (lod_chunk_ptr.data) { - .simplified => |*data| { - // Defer if a mesh/generation/upload job is mid-flight for - // this region: writing source data concurrently with a - // mesh job reading it (under the shared lock) would race. - if (lod_chunk_ptr.isPinned() or - lod_chunk_ptr.getState() == .generating or - lod_chunk_ptr.getState() == .meshing or - lod_chunk_ptr.getState() == .uploading) - { - pending_mask |= level_mask; - continue; - } - const region_size: i32 = @intCast(world_core.regionSizeBlocks(lod)); - const min_x: i32 = lod_chunk_ptr.region_x * region_size; - const min_z: i32 = lod_chunk_ptr.region_z * region_size; - const written = lod_ingest.downsampleChunkIntoRegion(chunk, cx, cz, data, min_x, min_z, region_size, provenance); - if (written == 0) continue; - lod_chunk_ptr.markSourceDirty(); - lod_chunk_ptr.updateHeightBoundsFromData(); - // Force a remesh of already-rendered regions so the new - // chunk-derived data becomes visible. - self.demoteRegionForRemesh(key, lod_chunk_ptr); - }, - else => { - // Region exists but has no source data yet (not generated). - pending_mask |= level_mask; - }, - } - } - return pending_mask; -} - -fn activeIngestionMask(self: *Self) u8 { - var mask: u8 = 0; - const active = lod_chunk.activeLODCount(self.config); - var i: usize = 1; - while (i < active) : (i += 1) mask |= @as(u8, 1) << @intCast(i); - return mask; -} - -/// Assumes `ingestion_mutex` held. Coalesces by coordinate, keeping the -/// most authoritative provenance and the union of pending level bits. -/// Deferred work is deliberately durable: a player edit can outlive many -/// unload/reload or teleport cycles before its source chunk becomes resident. -pub fn recordPendingLocked(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance, mask: u8) bool { - for (self.ingestion_queue.pending_ingestions.items) |*entry| { - if (entry.cx == cx and entry.cz == cz) { - entry.pending_levels |= mask; - entry.ttl = 0; - if (provenance.canOverwrite(entry.provenance)) entry.provenance = provenance; - return true; - } - } - if (!makePendingRoomLocked(self, cx, cz, provenance)) return false; - self.ingestion_queue.pending_ingestions.append(self.allocator, .{ - .cx = cx, - .cz = cz, - .provenance = provenance, - .pending_levels = mask, - .ttl = 0, - }) catch |err| { - log.log.warn("Failed to defer LOD ingestion for chunk ({}, {}): {}", .{ cx, cz, err }); - return false; - }; - return true; -} - -/// Re-record a pending entry from outside the lock. Coalesces with any -/// existing entry for the coordinate without allowing retry pressure to age -/// out player-edit provenance. -pub fn rerecordPending(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance, mask: u8, ttl: u16) void { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - for (self.ingestion_queue.pending_ingestions.items) |*entry| { - if (entry.cx == cx and entry.cz == cz) { - entry.pending_levels |= mask; - _ = ttl; - entry.ttl = 0; - if (provenance.canOverwrite(entry.provenance)) entry.provenance = provenance; - return; - } - } - if (!makePendingRoomLocked(self, cx, cz, provenance)) return; - self.ingestion_queue.pending_ingestions.append(self.allocator, .{ - .cx = cx, - .cz = cz, - .provenance = provenance, - .pending_levels = mask, - .ttl = 0, - }) catch |err| { - log.log.warn("Failed to requeue deferred LOD ingestion for chunk ({}, {}): {}", .{ cx, cz, err }); - }; -} - -/// Remove only entries that have no remaining target levels. Deferred source -/// updates must not expire just because their chunks remain unloaded. -/// Assumes `ingestion_mutex` held. -pub fn decayPendingLocked(self: *Self) void { - var write: usize = 0; - for (self.ingestion_queue.pending_ingestions.items) |entry| { - if (entry.pending_levels != 0) { - self.ingestion_queue.pending_ingestions.items[write] = entry; - write += 1; - } - } - self.ingestion_queue.pending_ingestions.shrinkRetainingCapacity(write); -} - -/// Replay deferred ingestions: snapshot all pending under the ingestion -/// lock, resolve each chunk, and re-apply. Unresolved or still-in-flight -/// levels remain queued until they apply or manager teardown. -pub fn drainPendingIngestions(self: *Self) void { - drainPendingIngestionsWithLimit(self, self.ingestion_queue.drain_per_frame); -} - -/// Makes one immediate attempt to apply every currently deferred ingestion. -/// Requests that still cannot resolve or target in-flight regions remain -/// queued for later updates. -pub fn drainPendingIngestionsNow(self: *Self) void { - drainPendingIngestionsWithLimit(self, std.math.maxInt(usize)); -} - -fn drainPendingIngestionsWithLimit(self: *Self, max_count: usize) void { - var snapshot = std.ArrayListUnmanaged(PendingIngestion).empty; - { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - if (self.ingestion_queue.pending_ingestions.items.len == 0) return; - snapshot.appendSlice(self.allocator, self.ingestion_queue.pending_ingestions.items) catch { - self.decayPendingLocked(); - return; - }; - self.ingestion_queue.pending_ingestions.clearRetainingCapacity(); - } - defer snapshot.deinit(self.allocator); - - const resolver = self.ingestion_queue.chunk_resolver; - const limit = @min(snapshot.items.len, max_count); - - // Process the head of the snapshot and retain the tail for a later frame. - var i: usize = 0; - while (i < snapshot.items.len) : (i += 1) { - const entry = snapshot.items[i]; - if (entry.pending_levels == 0) continue; - if (i >= limit) { - self.rerecordPending(entry.cx, entry.cz, entry.provenance, entry.pending_levels, 0); - continue; - } - const chunk = if (resolver) |r| r.resolve(entry.cx, entry.cz) else null; - if (chunk) |c| { - const remaining = applyIngestionToRegionsMask(self, entry.cx, entry.cz, c, entry.provenance, entry.pending_levels); - if (remaining != 0) { - self.rerecordPending(entry.cx, entry.cz, entry.provenance, remaining, 0); - } - } else { - self.rerecordPending(entry.cx, entry.cz, entry.provenance, entry.pending_levels, 0); - } - } -} - -/// Preserve already accepted edit provenance when the bounded queue is under -/// pressure. Non-edit work may be displaced, but an edited entry is never -/// silently evicted to make room for ordinary streaming retries. -/// Assumes `ingestion_queue.mutex` is held. -fn makePendingRoomLocked(self: *Self, cx: i32, cz: i32, provenance: LODColumnProvenance) bool { - if (self.ingestion_queue.pending_ingestions.items.len < MAX_PENDING_INGESTIONS) return true; - - for (self.ingestion_queue.pending_ingestions.items, 0..) |entry, index| { - if (entry.provenance != .edited) { - _ = self.ingestion_queue.pending_ingestions.orderedRemove(index); - return true; - } - } - - log.log.warn("LOD deferred-ingestion queue is full; retaining existing edited work and rejecting ({}, {}) provenance={}", .{ cx, cz, provenance }); - return false; -} - -/// Immediately applies pending edited chunks, bypassing the ordinary -/// coalescing cooldown. Used by explicit save points that must persist the -/// corresponding LOD source snapshot in the same transaction. -pub fn flushEditedChunksNow(self: *Self) void { - self.ingestion_queue.edit_cooldown = 0.0; - flushEditedChunksWithLimit(self, std.math.maxInt(usize)); -} - -/// Bypasses the cooldown but consumes only the ordinary per-frame ingestion -/// budget. Autosave uses this to start persistence without a large edit burst. -pub fn flushEditedChunksBounded(self: *Self) void { - self.ingestion_queue.edit_cooldown = 0.0; - flushEditedChunksWithLimit(self, self.ingestion_queue.drain_per_frame); -} - -/// Flush debounced player edits: re-ingest edited chunks with the `edited` -/// provenance. Runs on a cooldown so rapid edits coalesce into one rebuild. -pub fn flushEditedChunks(self: *Self) void { - self.ingestion_queue.edit_cooldown -= LOD_FRAME_DT_APPROX; - if (self.ingestion_queue.edit_cooldown > 0.0) return; - - flushEditedChunksWithLimit(self, std.math.maxInt(usize)); -} - -fn flushEditedChunksWithLimit(self: *Self, max_count: usize) void { - var snapshot = std.ArrayListUnmanaged(ChunkCoordKey).empty; - { - self.ingestion_queue.mutex.lock(); - defer self.ingestion_queue.mutex.unlock(); - if (self.ingestion_queue.edit_dirty.count() == 0) return; - var it = self.ingestion_queue.edit_dirty.keyIterator(); - while (it.next()) |k| { - if (snapshot.items.len >= max_count) break; - snapshot.append(self.allocator, k.*) catch break; - } - for (snapshot.items) |key| _ = self.ingestion_queue.edit_dirty.remove(key); - } - defer snapshot.deinit(self.allocator); - - const resolver = self.ingestion_queue.chunk_resolver; - for (snapshot.items) |k| { - if (resolver) |r| { - if (r.resolve(k.cx, k.cz)) |chunk| { - self.ingestChunk(k.cx, k.cz, chunk, .edited); - continue; - } - } - // Chunk not resident now: defer until the resolver can fetch it. - self.requestIngestion(k.cx, k.cz, .edited); - } - self.ingestion_queue.edit_cooldown = EDIT_FLUSH_COOLDOWN; -} diff --git a/modules/world-lod/src/lod_manager_internal_tests.zig b/modules/world-lod/src/lod_manager_internal_tests.zig deleted file mode 100644 index b9c1ce9f..00000000 --- a/modules/world-lod/src/lod_manager_internal_tests.zig +++ /dev/null @@ -1,1495 +0,0 @@ -const std = @import("std"); -const fs = @import("fs"); -const sync = @import("sync"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const world_core = @import("world-core"); -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Vertex = @import("engine-rhi").Vertex; -const TextureAtlas = @import("engine-assets").TextureAtlas; -const RingBuffer = @import("engine-core").ring_buffer.RingBuffer; -const JobQueue = @import("engine-core").job_system.JobQueue; -const Job = @import("engine-core").job_system.Job; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const LODStagingCost = @import("lod_mesh_resources.zig").LODStagingCost; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_cache = @import("lod_cache.zig"); -const cache_io = @import("lod_cache_io.zig"); -const lod_store = @import("lod_store.zig"); -const manager_mod = @import("lod_manager.zig"); -const LODManager = manager_mod.LODManager; -const MAX_CACHE_LOADS_PER_UPDATE = @import("lod_manager_context.zig").MAX_CACHE_LOADS_PER_UPDATE; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = @import("lod_manager_context.zig").DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const wouldExceedUploadBudget = @import("lod_manager_context.zig").wouldExceedUploadBudget; -const cancelWorkOutsideHorizon = @import("lod_manager_core_ops.zig").cancelWorkOutsideHorizon; -const generation_ops = @import("lod_manager_generation_ops.zig"); -const testing = std.testing; - -test "LODManager cache helpers save and reload source data" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - const key = LODRegionKey{ .rx = 2, .rz = -3, .lod = .lod1 }; - - var data = try LODSimplifiedData.init(testing.allocator, .lod1); - defer data.deinit(); - data.setColumn(1, 1, 72.0, .forest, .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }, 0xFF112233, .empty, .daylight, .empty); - - manager.saveCachedSourceData(key, &data); - manager.flushCacheIO(); - - const store_path = try lod_store.containerPath(testing.allocator, save_dir_path, manager.cacheKey(key)); - defer testing.allocator.free(store_path); - fs.cwd().access(store_path, .{}) catch return error.ExpectedStoreContainer; - - var loaded = manager.loadCachedSourceData(key) orelse return error.ExpectedCacheHit; - defer loaded.deinit(); - - const idx = 1 + data.width; - try testing.expectEqual(data.heightmap[idx], loaded.heightmap[idx]); - try testing.expectEqual(data.biomes[idx], loaded.biomes[idx]); - try testing.expectEqual(data.material_layers[idx].foundation, loaded.material_layers[idx].foundation); -} - -test "flushDirtyStoresNow persists the latest edited source snapshot" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - const key = LODRegionKey{ .rx = 0, .rz = -1, .lod = .lod4 }; - const chunk = try putTestRegion(&manager, key, .generated); - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod4) }; - chunk.data.simplified.setColumn(1, 1, 64.0, .plains, .{ .surface = .sand, .subsurface = .sand, .foundation = .stone }, 0xc2b280, .{ - .is_surface = true, - .surface_height = 65.0, - .depth = 1.0, - .coverage = 0.5, - }, .daylight, .empty); - chunk.data.simplified.setColumnProvenance(1, 1, .edited); - chunk.markSourceDirty(); - - manager.flushDirtyStoresNow(); - - var loaded = manager.loadCachedSourceData(key) orelse return error.ExpectedCacheHit; - defer loaded.deinit(); - const idx = 1 + loaded.width; - try testing.expect(loaded.water[idx].is_surface); - try testing.expectEqual(@as(f32, 0.5), loaded.water[idx].coverage); - try testing.expectEqual(LODColumnProvenance.edited, loaded.provenance[idx]); -} - -test "explicit persistence invalidates blocked edited store payloads" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - defer manager.ingestion_queue.pending_ingestions.deinit(testing.allocator); - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - const key = LODRegionKey.fromChunkCoords(0, 0, .lod2); - var stale = try LODSimplifiedData.init(testing.allocator, key.lod); - defer stale.deinit(); - stale.setColumn(0, 0, 40.0, .plains, .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, 0x808080, .empty, .daylight, .empty); - manager.saveCachedSourceData(key, &stale); - manager.flushCacheIO(); - var initially_loaded = manager.loadCachedSourceData(key) orelse return error.ExpectedCacheHit; - initially_loaded.deinit(); - - manager.ingestion_queue.mutex.lock(); - const recorded = manager.recordPendingLocked(0, 0, .edited, @as(u8, 1) << @intFromEnum(LODLevel.lod2)); - manager.ingestion_queue.mutex.unlock(); - try testing.expect(recorded); - - manager.flushDirtyStoresNow(); - manager.invalidatePendingEditedStoresNow(); - try testing.expect(manager.loadCachedSourceData(key) == null); - - // A saturated pending queue re-retains an edit in edit_dirty. Explicit - // persistence must invalidate that coordinate's full active LOD ladder too. - manager.saveCachedSourceData(key, &stale); - manager.flushCacheIO(); - try manager.ingestion_queue.edit_dirty.put(.{ .cx = 0, .cz = 0 }, {}); - manager.invalidatePendingEditedStoresNow(); - try testing.expect(manager.loadCachedSourceData(key) == null); -} - -test "flushDirtyStoresNow drains more than one cache pipeline batch" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - const region_count = cache_io.MAX_PENDING_TASKS + 1; - for (0..region_count) |i| { - const key = LODRegionKey{ .rx = @intCast(i), .rz = -2, .lod = .lod4 }; - const chunk = try putTestRegion(&manager, key, .generated); - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, key.lod) }; - chunk.data.simplified.setColumn(0, 0, @floatFromInt(40 + i), .plains, .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, 0x808080, .empty, .daylight, .empty); - chunk.data.simplified.setColumnProvenance(0, 0, .edited); - chunk.markSourceDirty(); - } - - manager.flushDirtyStoresNow(); - - for (0..region_count) |i| { - const key = LODRegionKey{ .rx = @intCast(i), .rz = -2, .lod = .lod4 }; - var loaded = manager.loadCachedSourceData(key) orelse return error.ExpectedCacheHit; - defer loaded.deinit(); - try testing.expectEqual(@as(f32, @floatFromInt(40 + i)), loaded.getHeight(0, 0)); - try testing.expectEqual(LODColumnProvenance.edited, loaded.getColumnProvenance(0, 0)); - } -} - -test "LODManager cache helpers delete corrupt cache files" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod2 }; - const legacy_dir = try fs.path.join(testing.allocator, &.{ save_dir_path, "lod_cache" }); - defer testing.allocator.free(legacy_dir); - try fs.cwd().makePath(legacy_dir); - const path = try manager.legacyCacheFilePath(save_dir_path, manager.cacheKey(key)); - defer testing.allocator.free(path); - - const file = try fs.cwd().createFile(path, .{ .truncate = true }); - try file.writeAll(&.{ 0, 1, 2, 3 }); - file.close(); - - try testing.expect(manager.loadCachedSourceData(key) == null); - try testing.expectError(error.FileNotFound, fs.cwd().openFile(path, .{})); -} - -test "LODManager enableCache deletes stale generator-keyed store and writes live header" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - try lod_store.writeHeader(testing.allocator, save_dir_path, .{ - .seed = 42, - .generator_identity_hash = 1234, - .generator_version = 1, - }); - const stale_key = lod_cache.Key{ .seed = 42, .generator_identity_hash = 1234, .generator_version = 1, .rx = 0, .rz = 0, .lod = .lod1 }; - try lod_store.writePayload(testing.allocator, save_dir_path, stale_key, "stale", lod_store.DEFAULT_STORE_SIZE_CAP_MB); - - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - manager.cache_store.cache_dir_path = null; - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - try testing.expect((try lod_store.readPayload(testing.allocator, save_dir_path, stale_key)) == null); - const header = (try lod_store.readHeader(testing.allocator, save_dir_path)).?; - try testing.expectEqual(@as(u64, 42), header.seed); - try testing.expectEqual(@as(u64, 99), header.generator_identity_hash); - try testing.expectEqual(@as(u32, 7), header.generator_version); -} - -test "LODManager enableCache deletes stale data-version store" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - try lod_store.writeHeader(testing.allocator, save_dir_path, .{ - .seed = 42, - .generator_identity_hash = 99, - .generator_version = 7, - .lod_data_version = lod_cache.CACHE_VERSION - 1, - }); - const stale_key = lod_cache.Key{ .seed = 42, .generator_identity_hash = 99, .generator_version = 7, .rx = 0, .rz = 0, .lod = .lod1 }; - try lod_store.writePayload(testing.allocator, save_dir_path, stale_key, "stale", lod_store.DEFAULT_STORE_SIZE_CAP_MB); - - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - manager.cache_store.cache_dir_path = null; - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - try testing.expect((try lod_store.readPayload(testing.allocator, save_dir_path, stale_key)) == null); - const header = (try lod_store.readHeader(testing.allocator, save_dir_path)).?; - try testing.expectEqual(lod_cache.CACHE_VERSION, header.lod_data_version); -} - -test "LODManager queued generation applies source-store completion asynchronously" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var config = LODConfig{}; - const key = LODRegionKey{ .rx = 4, .rz = -2, .lod = .lod1 }; - - var source = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); - defer source.deinit(); - source.setColumn(2, 3, 81.0, .forest, .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }, 0xFF445566, .empty, .daylight, .empty); - - var writer = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer writer.cache_io.deinit(); - writer.config = config.interface(); - writer.cache_store.cache_dir_path = null; - try writer.enableCache(save_dir_path); - writer.saveCachedSourceData(key, &source); - writer.flushCacheIO(); - if (writer.cache_store.cache_dir_path) |path| testing.allocator.free(path); - writer.ingestion_queue.edit_dirty.deinit(); - - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - manager.config = config.interface(); - manager.cache_store.cache_dir_path = null; - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - defer manager.ingestion_queue.edit_dirty.deinit(); - defer manager.mesh_disposal.queue.deinit(testing.allocator); - - for (0..LODLevel.count) |i| { - manager.regions[i] = RegionMap.init(testing.allocator); - manager.meshes[i] = MeshMap.init(testing.allocator); - manager.upload_queues[i] = try RingBuffer(*LODChunk).init(testing.allocator, 4); - manager.job_dispatcher.queues[i] = try testing.allocator.create(JobQueue); - manager.job_dispatcher.queues[i].* = JobQueue.init(testing.allocator); - } - defer { - for (0..LODLevel.count) |i| { - var region_iter = manager.regions[i].iterator(); - while (region_iter.next()) |entry| { - entry.value_ptr.*.deinit(testing.allocator); - testing.allocator.destroy(entry.value_ptr.*); - } - manager.regions[i].deinit(); - manager.meshes[i].deinit(); - manager.upload_queues[i].deinit(); - manager.job_dispatcher.queues[i].deinit(); - testing.allocator.destroy(manager.job_dispatcher.queues[i]); - } - manager.generation_tokens.deinit(testing.allocator); - manager.transition_tokens.deinit(testing.allocator); - manager.fade_tokens.deinit(testing.allocator); - } - - const chunk = try testing.allocator.create(LODChunk); - chunk.* = LODChunk.init(key.rx, key.rz, key.lod); - chunk.state = .queued_for_generation; - chunk.job_token = 9; - try manager.regions[@intFromEnum(key.lod)].put(key, chunk); - _ = try manager.generation_tokens.push(testing.allocator, .{ .key = key, .job_token = chunk.job_token, .source_revision = chunk.source_revision, .priority = 0, .stage = .generation }); - - try manager.processQueuedGenerations(Vec3.zero); - - // The update-side call only enqueues the read; no source data has been - // deserialized or applied until the dedicated cache worker completes. - try testing.expectEqual(@as(u32, 0), manager.cache_hits); - try testing.expectEqual(LODState.queued_for_generation, chunk.state); - manager.flushCacheIO(); - - try testing.expectEqual(@as(u32, 1), manager.cache_hits); - try testing.expectEqual(@as(usize, 0), manager.job_dispatcher.queues[LODLevel.count - 1].count()); - try testing.expectEqual(LODState.generated, chunk.state); - switch (chunk.data) { - .simplified => |*loaded| { - const idx = 2 + 3 * loaded.width; - try testing.expectEqual(source.heightmap[idx], loaded.heightmap[idx]); - try testing.expectEqual(source.material_layers[idx].foundation, loaded.material_layers[idx].foundation); - }, - else => return error.ExpectedSimplifiedData, - } -} - -test "LODManager queued generation dispatches beyond cache read budget" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir_path = try dir.realpath(".", &path_buf); - - var config = LODConfig{}; - var manager = try LODManager.initCacheTestManager(testing.allocator, save_dir_path); - defer manager.cache_io.deinit(); - manager.config = config.interface(); - manager.cache_store.cache_dir_path = null; - try manager.enableCache(save_dir_path); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - defer manager.ingestion_queue.edit_dirty.deinit(); - defer manager.mesh_disposal.queue.deinit(testing.allocator); - - for (0..LODLevel.count) |i| { - manager.regions[i] = RegionMap.init(testing.allocator); - manager.meshes[i] = MeshMap.init(testing.allocator); - manager.upload_queues[i] = try RingBuffer(*LODChunk).init(testing.allocator, 4); - manager.job_dispatcher.queues[i] = try testing.allocator.create(JobQueue); - manager.job_dispatcher.queues[i].* = JobQueue.init(testing.allocator); - } - defer { - for (0..LODLevel.count) |i| { - var region_iter = manager.regions[i].iterator(); - while (region_iter.next()) |entry| { - entry.value_ptr.*.deinit(testing.allocator); - testing.allocator.destroy(entry.value_ptr.*); - } - manager.regions[i].deinit(); - manager.meshes[i].deinit(); - manager.upload_queues[i].deinit(); - manager.job_dispatcher.queues[i].deinit(); - testing.allocator.destroy(manager.job_dispatcher.queues[i]); - } - manager.generation_tokens.deinit(testing.allocator); - manager.transition_tokens.deinit(testing.allocator); - manager.fade_tokens.deinit(testing.allocator); - } - - const candidate_count = MAX_CACHE_LOADS_PER_UPDATE + 4; - for (0..candidate_count) |i| { - const key = LODRegionKey{ .rx = @intCast(i), .rz = 0, .lod = .lod1 }; - const chunk = try testing.allocator.create(LODChunk); - chunk.* = LODChunk.init(key.rx, key.rz, key.lod); - chunk.state = .queued_for_generation; - chunk.job_token = @intCast(i + 1); - try manager.regions[@intFromEnum(key.lod)].put(key, chunk); - _ = try manager.generation_tokens.push(testing.allocator, .{ .key = key, .job_token = chunk.job_token, .source_revision = chunk.source_revision, .priority = @intCast(i), .stage = .generation }); - } - - try manager.processQueuedGenerations(Vec3.zero); - - try testing.expectEqual(@as(u32, 0), manager.cache_misses); - try testing.expectEqual(candidate_count - MAX_CACHE_LOADS_PER_UPDATE, manager.job_dispatcher.queues[LODLevel.count - 1].count()); - manager.flushCacheIO(); - try testing.expectEqual(@as(u32, @intCast(MAX_CACHE_LOADS_PER_UPDATE)), manager.cache_misses); - try testing.expectEqual(candidate_count, manager.job_dispatcher.queues[LODLevel.count - 1].count()); -} - -fn initEvictionTestManager(allocator: std.mem.Allocator, config: *LODConfig) !LODManager { - var manager = try LODManager.initCacheTestManager(allocator, ""); - manager.config = config.interface(); - manager.job_dispatcher.worker_pool = null; - manager.job_dispatcher.next_token = 1; - manager.job_dispatcher.stop_flag = std.atomic.Value(bool).init(false); - for (0..LODLevel.count) |i| { - manager.regions[i] = RegionMap.init(allocator); - manager.meshes[i] = MeshMap.init(allocator); - manager.upload_queues[i] = try RingBuffer(*LODChunk).init(allocator, 4); - manager.job_dispatcher.queues[i] = try allocator.create(JobQueue); - manager.job_dispatcher.queues[i].* = JobQueue.init(allocator); - } - var bridge_ctx: u8 = 0; - manager.gpu_bridge = .{ - .on_upload = struct { - fn f(_: *LODMesh, _: *anyopaque) @import("engine-rhi").RhiError!void {} - }.f, - .on_destroy = struct { - fn f(_: *LODMesh, _: *anyopaque) void {} - }.f, - .on_wait_idle = struct { - fn f(_: *anyopaque) void {} - }.f, - .ctx = @ptrCast(&bridge_ctx), - }; - return manager; -} - -fn deinitEvictionTestManager(manager: *LODManager) void { - manager.cache_io.deinit(); - for (0..LODLevel.count) |i| { - var region_iter = manager.regions[i].iterator(); - while (region_iter.next()) |entry| { - entry.value_ptr.*.deinit(manager.allocator); - manager.allocator.destroy(entry.value_ptr.*); - } - manager.regions[i].deinit(); - - var mesh_iter = manager.meshes[i].iterator(); - while (mesh_iter.next()) |entry| { - entry.value_ptr.*.releasePendingCompactTile(); - if (entry.value_ptr.*.pending_vertices) |pending| { - manager.allocator.free(pending); - } - manager.allocator.destroy(entry.value_ptr.*); - } - manager.meshes[i].deinit(); - manager.upload_queues[i].deinit(); - manager.job_dispatcher.queues[i].deinit(); - manager.allocator.destroy(manager.job_dispatcher.queues[i]); - } - for (manager.mesh_disposal.queue.items) |mesh| { - manager.gpu_bridge.destroy(mesh); - manager.allocator.destroy(mesh); - } - manager.mesh_disposal.queue.deinit(manager.allocator); - manager.ingestion_queue.edit_dirty.deinit(); - manager.generation_tokens.deinit(manager.allocator); - manager.transition_tokens.deinit(manager.allocator); - manager.fade_tokens.deinit(manager.allocator); -} - -test "LODManager reports pool, direct, pending, deferred, and logical admission memory separately" { - var config = LODConfig{ .memory_budget_mb = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const RendererMemory = struct { - fn stats(_: *anyopaque) lod_gpu.LODRendererMemoryStats { - return .{ - .pool_gpu_capacity_bytes = 100, - .pool_gpu_allocated_bytes = 80, - .pool_gpu_slack_bytes = 20, - .pool_cpu_shadow_bytes = 100, - .compact_pool_capacity_bytes = 60, - .compact_pool_allocated_bytes = 40, - .compact_pool_free_bytes = 20, - }; - } - }; - manager.renderer = .{ - .render_fn = undefined, - .deinit_fn = undefined, - .ptr = undefined, - .memory_stats_fn = RendererMemory.stats, - }; - manager.profiling = .init(true); - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - _ = try putTestRegion(&manager, key, .uploading); - const direct_mesh = try putTestPendingMesh(&manager, key, 3); - direct_mesh.capacity = 2; - - const deferred = try testing.allocator.create(LODMesh); - deferred.* = LODMesh.init(testing.allocator, .lod1); - deferred.capacity = 4; - deferred.pending_vertices = try testing.allocator.alloc(Vertex, 5); - manager.queueMeshDeletion(deferred); - - manager.updateStats(); - const stats = manager.getStats(); - - const vertex_bytes = @sizeOf(Vertex); - const expected_direct_gpu = 6 * vertex_bytes; - const expected_pending = 8 * vertex_bytes; - const expected_known = 260 + expected_direct_gpu + expected_pending; - const expected_reservation = 1024 * 1024; - try testing.expectEqual(@as(u64, 100), stats.pool_gpu_capacity_bytes); - try testing.expectEqual(@as(u64, 80), stats.pool_gpu_allocated_bytes); - try testing.expectEqual(@as(u64, 20), stats.pool_gpu_slack_bytes); - try testing.expectEqual(@as(u64, 100), stats.pool_cpu_shadow_bytes); - try testing.expectEqual(@as(u64, 60), stats.compact_pool_capacity_bytes); - try testing.expectEqual(@as(u64, 40), stats.compact_pool_allocated_bytes); - try testing.expectEqual(@as(u64, expected_direct_gpu), stats.direct_mesh_gpu_bytes); - try testing.expectEqual(@as(u64, 3 * vertex_bytes), stats.pending_cpu_upload_bytes); - try testing.expectEqual(@as(u64, 4 * vertex_bytes), stats.deferred_deletion_gpu_bytes); - try testing.expectEqual(@as(u64, 5 * vertex_bytes), stats.deferred_deletion_cpu_bytes); - try testing.expectEqual(@as(u64, expected_known), stats.memory_used_bytes); - try testing.expectEqual(@as(u64, 1), stats.resident_region_count); - try testing.expectEqual(@as(u64, expected_reservation), stats.logical_admission_reservation_bytes); - try testing.expectEqual(@as(u64, expected_known + expected_reservation), stats.logical_admission_bytes); - try testing.expectEqual(@as(u64, expected_known), stats.profiling.known_memory_bytes); - try testing.expectEqual(@as(u64, 4 * vertex_bytes), stats.profiling.deferred_deletion_bytes); - try testing.expectEqual(@as(u64, 5 * vertex_bytes), stats.profiling.deferred_deletion_cpu_bytes); -} - -test "LODManager pool-exhaustion upload failure falls back to CPU heightfield and requeues LOD4" { - var config = LODConfig{ .max_uploads_per_frame = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var atlas: TextureAtlas = undefined; - @memset(std.mem.asBytes(&atlas.tile_mappings), 0); - atlas.tile_luminance = [_]TextureAtlas.BlockTileLuminance{TextureAtlas.BlockTileLuminance.uniform(1.0)} ** world_core.MAX_BLOCK_TYPES; - atlas.tile_colors = [_]TextureAtlas.BlockTileColor{TextureAtlas.BlockTileColor.uniform(0xffffff)} ** world_core.MAX_BLOCK_TYPES; - manager.atlas = &atlas; - - const key = LODRegionKey{ .rx = 96, .rz = -96, .lod = .lod4 }; - const chunk = try putTestRegion(&manager, key, .uploading); - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod4) }; - const mesh = try manager.getOrCreateMesh(key); - switch (chunk.data) { - .simplified => |*data| try mesh.buildCompactTile(data), - else => unreachable, - } - try manager.upload_queues[@intFromEnum(key.lod)].push(chunk); - - // This mirrors a compact pool exhaustion during a large teleport: compact - // upload fails, while the ordinary CPU mesh upload remains available. - manager.gpu_bridge.on_upload = struct { - fn f(candidate: *LODMesh, _: *anyopaque) @import("engine-rhi").RhiError!void { - if (candidate.isCompact()) return error.OutOfMemory; - } - }.f; - - manager.processUploadsWithBudget(std.math.maxInt(usize)); - try testing.expect(!mesh.isCompact()); - try testing.expect(mesh.pendingVerticesForTest() != null); - try testing.expectEqual(LODState.uploading, chunk.getState()); - try testing.expectEqual(@as(usize, 1), manager.upload_queues[@intFromEnum(key.lod)].count()); - - manager.processUploadsWithBudget(std.math.maxInt(usize)); - try testing.expectEqual(LODState.renderable, chunk.getState()); -} - -test "LODManager compact fallback keeps a renderable compact mesh through injected CPU allocation failure" { - const DestroyCounter = struct { - calls: u32 = 0, - - fn destroy(mesh: *LODMesh, ctx: *anyopaque) void { - const self: *@This() = @ptrCast(@alignCast(ctx)); - self.calls += 1; - // Match the production compact-pool callback, which clears all - // compact counts/state before LODGPUBridge clears pending vertices. - mesh.clearCompactState(); - } - }; - - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var atlas: TextureAtlas = undefined; - @memset(std.mem.asBytes(&atlas.tile_mappings), 0); - atlas.tile_luminance = [_]TextureAtlas.BlockTileLuminance{TextureAtlas.BlockTileLuminance.uniform(1.0)} ** world_core.MAX_BLOCK_TYPES; - atlas.tile_colors = [_]TextureAtlas.BlockTileColor{TextureAtlas.BlockTileColor.uniform(0xffffff)} ** world_core.MAX_BLOCK_TYPES; - manager.atlas = &atlas; - - var destroy_counter = DestroyCounter{}; - manager.gpu_bridge.ctx = &destroy_counter; - manager.gpu_bridge.on_destroy = DestroyCounter.destroy; - - var chunk = LODChunk.init(0, 0, .lod4); - defer chunk.deinit(testing.allocator); - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod4) }; - chunk.setState(.renderable); - - const backing = try testing.allocator.alloc(u8, 2 * 1024 * 1024); - defer testing.allocator.free(backing); - var failing_storage = std.heap.FixedBufferAllocator.init(backing); - var mesh = LODMesh.init(failing_storage.allocator(), .lod4); - defer mesh.clearRetiredState(); - switch (chunk.data) { - .simplified => |*data| try mesh.buildCompactTile(data), - else => unreachable, - } - mesh.ready = true; - mesh.vertex_count = mesh.compact_index_count; - const original_count = mesh.vertex_count; - - // Exhaust exactly the mesh allocator after compact setup. The CPU build - // must fail before retiring the only renderable representation. - const compact_end = failing_storage.end_index; - failing_storage.end_index = backing.len; - try testing.expectError(error.OutOfMemory, manager.fallbackCompactMeshToCpu(&mesh, &chunk)); - try testing.expectEqual(@as(u32, 0), destroy_counter.calls); - try testing.expect(chunk.isRenderable()); - try testing.expect(mesh.isCompact()); - try testing.expect(mesh.isRenderable()); - try testing.expectEqual(original_count, mesh.vertex_count); - try testing.expect(mesh.pendingVerticesForTest() == null); - - // Restore capacity and retry the same transition. This proves the failed - // attempt was not an empty .renderable state or a no-op retry. - failing_storage.end_index = compact_end; - try manager.fallbackCompactMeshToCpu(&mesh, &chunk); - try testing.expectEqual(@as(u32, 1), destroy_counter.calls); - try testing.expect(!mesh.isCompact()); - try testing.expect(!mesh.isRenderable()); - try testing.expect(mesh.pendingVerticesForTest() != null); -} - -test "LODManager recovers compact draw failure on update path without losing parent fallback" { - const RecoveryMock = struct { - allocator: std.mem.Allocator, - destroy_calls: u32 = 0, - upload_calls: u32 = 0, - - fn bridge(self: *@This()) LODGPUBridge { - return .{ - .on_upload = upload, - .on_destroy = destroy, - .on_wait_idle = waitIdle, - .ctx = self, - }; - } - - fn upload(mesh: *LODMesh, ctx: *anyopaque) @import("engine-rhi").RhiError!void { - const self: *@This() = @ptrCast(@alignCast(ctx)); - self.upload_calls += 1; - const pending = mesh.pending_vertices orelse return; - mesh.vertex_count = @intCast(pending.len); - mesh.opaque_vertex_count = @intCast(pending.len); - mesh.ready = true; - self.allocator.free(pending); - mesh.pending_vertices = null; - } - - fn destroy(_: *LODMesh, ctx: *anyopaque) void { - const self: *@This() = @ptrCast(@alignCast(ctx)); - self.destroy_calls += 1; - } - - fn waitIdle(_: *anyopaque) void {} - }; - - var config = LODConfig{ .max_uploads_per_frame = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var atlas: TextureAtlas = undefined; - @memset(std.mem.asBytes(&atlas.tile_mappings), 0); - atlas.tile_luminance = [_]TextureAtlas.BlockTileLuminance{TextureAtlas.BlockTileLuminance.uniform(1.0)} ** world_core.MAX_BLOCK_TYPES; - atlas.tile_colors = [_]TextureAtlas.BlockTileColor{TextureAtlas.BlockTileColor.uniform(0xffffff)} ** world_core.MAX_BLOCK_TYPES; - manager.atlas = &atlas; - - var mock = RecoveryMock{ .allocator = testing.allocator }; - manager.gpu_bridge = mock.bridge(); - - const parent_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod4 }; - const parent = try putTestRegion(&manager, parent_key, .renderable); - parent.ready_children = 4; - _ = try putTestMesh(&manager, parent_key, 3); - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod3 }; - const chunk = try putTestRegion(&manager, key, .renderable); - chunk.job_token = 17; - chunk.source_revision = 3; - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod3) }; - const mesh = try manager.getOrCreateMesh(key); - switch (chunk.data) { - .simplified => |*data| try mesh.buildCompactTile(data), - else => unreachable, - } - mesh.setSourceIdentity(chunk.job_token, chunk.source_revision); - mesh.ready = true; - mesh.vertex_count = mesh.compact_index_count; - - // The renderer's failed frame only suppresses the broken compact draw. It - // does not touch RHI or hierarchy state while it holds the shared lock. - try testing.expect(!mesh.noteCompactBackendDrawFailure()); - try testing.expect(mesh.isRenderable()); - var draw_attempt: u8 = 1; - while (draw_attempt < LODMesh.COMPACT_BACKEND_FAILURE_LIMIT) : (draw_attempt += 1) { - const terminal = mesh.noteCompactBackendDrawFailure(); - try testing.expectEqual(draw_attempt + 1 == LODMesh.COMPACT_BACKEND_FAILURE_LIMIT, terminal); - } - try testing.expect(!mesh.isRenderable()); - try testing.expectEqual(@as(u32, 0), mock.destroy_calls); - try testing.expectEqual(LODState.renderable, chunk.getState()); - try testing.expectEqual(@as(u8, 4), parent.readyChildren()); - - // The next update owns retirement and CPU fallback publication. Removing - // the child contribution immediately keeps the parent visible until upload. - manager.recoverCompactDrawFailures(); - try testing.expectEqual(@as(u32, 1), mock.destroy_calls); - try testing.expect(chunk.compact_disabled); - try testing.expect(!mesh.isCompact()); - try testing.expect(mesh.pendingVerticesForTest() != null); - try testing.expectEqual(LODState.uploading, chunk.getState()); - try testing.expectEqual(@as(usize, 1), manager.upload_queues[@intFromEnum(key.lod)].count()); - try testing.expectEqual(@as(u8, 3), parent.readyChildren()); - - manager.processUploadsWithBudget(std.math.maxInt(usize)); - try testing.expectEqual(@as(u32, 1), mock.upload_calls); - try testing.expectEqual(LODState.renderable, chunk.getState()); - try testing.expect(mesh.isRenderable()); - try testing.expectEqual(@as(u8, 4), parent.readyChildren()); -} - -test "LODManager ignores stale compact draw failures and retries on source change" { - const DestroyCounter = struct { - calls: u32 = 0, - - fn destroy(_: *LODMesh, ctx: *anyopaque) void { - const self: *@This() = @ptrCast(@alignCast(ctx)); - self.calls += 1; - } - }; - - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - var counter = DestroyCounter{}; - manager.gpu_bridge.ctx = &counter; - manager.gpu_bridge.on_destroy = DestroyCounter.destroy; - - const key = LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod3 }; - const chunk = try putTestRegion(&manager, key, .renderable); - chunk.job_token = 8; - chunk.source_revision = 4; - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod3) }; - const mesh = try manager.getOrCreateMesh(key); - switch (chunk.data) { - .simplified => |*data| try mesh.buildCompactTile(data), - else => unreachable, - } - mesh.setSourceIdentity(chunk.job_token, chunk.source_revision); - mesh.ready = true; - mesh.markCompactDrawFailed(); - - // A newer source has its own remesh lifecycle. A late failure from the old - // representation must neither retire it nor disable compact for the new - // source/device session. - chunk.markSourceDirty(); - manager.recoverCompactDrawFailures(); - try testing.expect(!chunk.compact_disabled); - try testing.expect(mesh.isCompact()); - try testing.expectEqual(LODState.renderable, chunk.getState()); - try testing.expectEqual(@as(u32, 0), counter.calls); - - // A stale job token is rejected by the same identity guard. - mesh.setSourceIdentity(chunk.job_token, chunk.source_revision); - chunk.job_token +%= 1; - manager.recoverCompactDrawFailures(); - try testing.expect(mesh.isCompact()); - try testing.expectEqual(@as(u32, 0), counter.calls); -} - -test "LODManager meshes reduced far grids through compact and expanded paths" { - const Cases = [_]struct { lod: LODLevel, width: u32, compact_capable: bool }{ - .{ .lod = .lod3, .width = 65, .compact_capable = true }, - .{ .lod = .lod4, .width = 65, .compact_capable = true }, - .{ .lod = .lod3, .width = 65, .compact_capable = false }, - .{ .lod = .lod4, .width = 65, .compact_capable = false }, - }; - - inline for (Cases) |case| { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var atlas: TextureAtlas = undefined; - @memset(std.mem.asBytes(&atlas.tile_mappings), 0); - atlas.tile_luminance = [_]TextureAtlas.BlockTileLuminance{TextureAtlas.BlockTileLuminance.uniform(1.0)} ** world_core.MAX_BLOCK_TYPES; - atlas.tile_colors = [_]TextureAtlas.BlockTileColor{TextureAtlas.BlockTileColor.uniform(0xffffff)} ** world_core.MAX_BLOCK_TYPES; - manager.atlas = &atlas; - - var capability_context: u8 = 0; - manager.gpu_bridge.ctx = &capability_context; - manager.gpu_bridge.on_supports_compact_gpu_culling = struct { - fn supports(_: *anyopaque) bool { - return case.compact_capable; - } - }.supports; - - manager.generator = .{ - .ptr = &capability_context, - .generate_heightmap_only = struct { - fn generate(_: *anyopaque, data: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void { - var z: u32 = 0; - while (z < data.width) : (z += 1) { - var x: u32 = 0; - while (x < data.width) : (x += 1) { - data.setGeneratedColumn(x, z, 64.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4f8c45, .empty, .daylight, .empty); - } - } - } - }.generate, - .maybe_recenter_cache = struct { - fn recenter(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - }.recenter, - .seed = 1, - .identity_hash = 1, - .version = 1, - }; - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = case.lod }; - const chunk = try putTestRegion(&manager, key, .generating); - chunk.job_token = 1; - generation_ops.processLODJob(&manager, .{ .type = .chunk_generation, .data = .{ .chunk = .{ .x = key.rx, .z = key.rz, .job_token = chunk.job_token, .lod_level = @intFromEnum(case.lod), .coord_scale = @intCast(case.lod.chunksPerSide()), .lod_radius = 4096 } } }); - - try testing.expectEqual(LODState.generated, chunk.getState()); - switch (chunk.data) { - .simplified => |*data| try testing.expectEqual(case.width, data.width), - else => return error.TestExpectedEqual, - } - - try manager.processStateTransitions(Vec3.zero); - const mesh_job: Job = manager.job_dispatcher.queues[LODLevel.count - 1].pop().?; - generation_ops.processLODJob(&manager, mesh_job); - - const mesh = manager.meshes[@intFromEnum(case.lod)].get(key) orelse return error.TestExpectedEqual; - try testing.expectEqual(case.compact_capable, mesh.isCompact()); - if (case.compact_capable) { - try testing.expectEqual((case.width - 1) * (case.width - 1) * 6, mesh.compact_index_count); - } else try testing.expect(mesh.pendingVerticesForTest() != null); - try testing.expectEqual(LODState.mesh_ready, chunk.getState()); - } -} - -test "stale generation completion preserves edited source published in flight" { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod4 }; - const chunk = try putTestRegion(&manager, key, .generating); - chunk.job_token = 1; - - const RaceContext = struct { - manager: *LODManager, - key: LODRegionKey, - }; - var context = RaceContext{ .manager = &manager, .key = key }; - manager.generator = .{ - .ptr = &context, - .generate_heightmap_only = struct { - fn generate(ptr: *anyopaque, data: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void { - const race: *RaceContext = @ptrCast(@alignCast(ptr)); - for (0..data.width) |z| for (0..data.width) |x| { - data.setGeneratedColumn(@intCast(x), @intCast(z), 64.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4f8c45, .empty, .daylight, .empty); - }; - - var edited = LODSimplifiedData.init(testing.allocator, .lod4) catch unreachable; - edited.setGeneratedColumn(0, 0, 20.0, .plains, .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, 0x808080, .empty, .daylight, .empty); - edited.setColumnProvenance(0, 0, .edited); - - race.manager.mutex.lock(); - defer race.manager.mutex.unlock(); - const region = race.manager.regions[@intFromEnum(race.key.lod)].get(race.key).?; - region.data = .{ .simplified = edited }; - region.markSourceDirty(); - } - }.generate, - .maybe_recenter_cache = struct { - fn recenter(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - }.recenter, - .seed = 1, - .identity_hash = 1, - .version = 1, - }; - - generation_ops.processLODJob(&manager, .{ .type = .chunk_generation, .data = .{ .chunk = .{ .x = key.rx, .z = key.rz, .job_token = chunk.job_token, .lod_level = @intFromEnum(key.lod), .coord_scale = @intCast(key.lod.chunksPerSide()), .lod_radius = 4096 } } }); - - try testing.expectEqual(LODState.generated, chunk.getState()); - try testing.expectEqual(@as(f32, 20.0), chunk.data.simplified.getHeight(0, 0)); - try testing.expectEqual(LODColumnProvenance.edited, chunk.data.simplified.getColumnProvenance(0, 0)); -} - -fn putTestRegion(manager: *LODManager, key: LODRegionKey, state: LODState) !*LODChunk { - const chunk = try manager.allocator.create(LODChunk); - chunk.* = LODChunk.init(key.rx, key.rz, key.lod); - chunk.state = state; - try manager.regions[@intFromEnum(key.lod)].put(key, chunk); - return chunk; -} - -fn putTestMesh(manager: *LODManager, key: LODRegionKey, capacity: u32) !*LODMesh { - const mesh = try manager.allocator.create(LODMesh); - mesh.* = LODMesh.init(manager.allocator, key.lod); - mesh.ready = true; - mesh.vertex_count = capacity; - mesh.capacity = capacity; - try manager.meshes[@intFromEnum(key.lod)].put(key, mesh); - return mesh; -} - -fn putTestPendingMesh(manager: *LODManager, key: LODRegionKey, vertex_count: usize) !*LODMesh { - const mesh = try manager.allocator.create(LODMesh); - mesh.* = LODMesh.init(manager.allocator, key.lod); - mesh.pending_vertices = try manager.allocator.alloc(Vertex, vertex_count); - try manager.meshes[@intFromEnum(key.lod)].put(key, mesh); - return mesh; -} - -test "LODManager backs off size-limited store writes until the cap changes" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - var config = LODConfig{ .lod_store_size_cap_mb = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - manager.cache_store.cache_dir_path = try testing.allocator.dupe(u8, save_dir); - defer testing.allocator.free(manager.cache_store.cache_dir_path.?); - manager.cache_store.use_config_store_size_cap = true; - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const chunk = try putTestRegion(&manager, key, .generated); - chunk.data = .{ .simplified = try LODSimplifiedData.init(testing.allocator, .lod1) }; - chunk.store_dirty = true; - chunk.store_size_limited = true; - chunk.store_size_limit_cap_mb = 1; - - manager.flushDirtyStores(); - try testing.expect(!chunk.store_write_queued); - try testing.expect(chunk.store_dirty); - - config.lod_store_size_cap_mb = 2; - manager.flushDirtyStores(); - try testing.expect(chunk.store_write_queued); -} - -const UploadMock = struct { - allocator: std.mem.Allocator, - calls: u32 = 0, - wait_idle_calls: u32 = 0, - fail_with_pressure: bool = false, - lod4_migration_bytes: usize = 0, - - fn bridge(self: *UploadMock) LODGPUBridge { - return .{ - .on_upload = upload, - .on_destroy = destroy, - .on_wait_idle = waitIdle, - .on_upload_cost = uploadCost, - .ctx = @ptrCast(self), - }; - } - - fn upload(mesh: *LODMesh, ctx: *anyopaque) @import("engine-rhi").RhiError!void { - const self: *UploadMock = @ptrCast(@alignCast(ctx)); - self.calls += 1; - if (self.fail_with_pressure) return error.OutOfMemory; - if (mesh.pending_vertices) |pending| { - mesh.vertex_count = @intCast(pending.len); - mesh.opaque_vertex_count = @intCast(pending.len); - mesh.ready = true; - self.allocator.free(pending); - mesh.pending_vertices = null; - } - } - - fn destroy(_: *LODMesh, _: *anyopaque) void {} - fn waitIdle(ctx: *anyopaque) void { - const self: *UploadMock = @ptrCast(@alignCast(ctx)); - self.wait_idle_calls += 1; - } - fn uploadCost(mesh: *LODMesh, ctx: *anyopaque) LODStagingCost { - const self: *UploadMock = @ptrCast(@alignCast(ctx)); - return .{ - .payload_bytes = mesh.pendingUploadBytes(), - .migration_bytes = if (mesh.lodLevel() == .lod4) self.lod4_migration_bytes else 0, - }; - } -}; - -test "LODManager upload budget defers remaining queued meshes" { - var config = LODConfig{ .max_uploads_per_frame = 8 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var mock = UploadMock{ .allocator = testing.allocator }; - manager.gpu_bridge = mock.bridge(); - - const first_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const second_key = LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod1 }; - const first = try putTestRegion(&manager, first_key, .uploading); - const second = try putTestRegion(&manager, second_key, .uploading); - _ = try putTestPendingMesh(&manager, first_key, 1); - _ = try putTestPendingMesh(&manager, second_key, 1); - try manager.upload_queues[1].push(first); - try manager.upload_queues[1].push(second); - - manager.processUploadsWithBudget(@sizeOf(Vertex)); - - try testing.expectEqual(@as(u32, 1), mock.calls); - try testing.expectEqual(LODState.renderable, first.state); - try testing.expectEqual(LODState.uploading, second.state); - try testing.expectEqual(@as(usize, 1), manager.upload_queues[1].count()); -} - -test "LODManager upload budget admits one oversized mesh to guarantee progress" { - var config = LODConfig{ .max_uploads_per_frame = 8 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var mock = UploadMock{ .allocator = testing.allocator }; - manager.gpu_bridge = mock.bridge(); - - const first_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const second_key = LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod1 }; - const first = try putTestRegion(&manager, first_key, .uploading); - const second = try putTestRegion(&manager, second_key, .uploading); - _ = try putTestPendingMesh(&manager, first_key, 1); - _ = try putTestPendingMesh(&manager, second_key, 1); - try manager.upload_queues[1].push(first); - try manager.upload_queues[1].push(second); - - manager.processUploadsWithBudget(@sizeOf(Vertex) - 1); - - try testing.expectEqual(@as(u32, 1), mock.calls); - try testing.expectEqual(LODState.renderable, first.state); - try testing.expectEqual(LODState.uploading, second.state); - try testing.expectEqual(@as(usize, 1), manager.upload_queues[1].count()); -} - -test "LODManager upload budget lets a near upload bypass a far pool migration" { - var config = LODConfig{ .max_uploads_per_frame = 8 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - // The far mesh itself is small, but its pool replacement would restage - // two additional vertices. The budget must preflight the full migration. - var mock = UploadMock{ .allocator = testing.allocator, .lod4_migration_bytes = 2 * @sizeOf(Vertex) }; - manager.gpu_bridge = mock.bridge(); - - const far_key = LODRegionKey{ .rx = 8, .rz = 0, .lod = .lod4 }; - const near_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const far = try putTestRegion(&manager, far_key, .uploading); - const near = try putTestRegion(&manager, near_key, .uploading); - _ = try putTestPendingMesh(&manager, far_key, 1); - _ = try putTestPendingMesh(&manager, near_key, 1); - try manager.upload_queues[@intFromEnum(far_key.lod)].push(far); - try manager.upload_queues[@intFromEnum(near_key.lod)].push(near); - - manager.processUploadsWithBudget(2 * @sizeOf(Vertex)); - - try testing.expectEqual(@as(u32, 1), mock.calls); - try testing.expectEqual(LODState.uploading, far.state); - try testing.expectEqual(LODState.renderable, near.state); - try testing.expectEqual(@as(usize, 1), manager.upload_queues[@intFromEnum(far_key.lod)].count()); - - // On the next frame no smaller work remains, so one over-budget pool - // migration must be allowed through instead of starving forever. - manager.processUploadsWithBudget(2 * @sizeOf(Vertex)); - try testing.expectEqual(@as(u32, 2), mock.calls); - try testing.expectEqual(LODState.renderable, far.state); - try testing.expectEqual(@as(usize, 0), manager.upload_queues[@intFromEnum(far_key.lod)].count()); -} - -test "LODManager routine upload and eviction record no streaming device waits" { - var config = LODConfig{ .max_uploads_per_frame = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var mock = UploadMock{ .allocator = testing.allocator }; - manager.gpu_bridge = mock.bridge(); - manager.profiling = .init(true); - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const chunk = try putTestRegion(&manager, key, .uploading); - const mesh = try putTestPendingMesh(&manager, key, 1); - try manager.upload_queues[@intFromEnum(key.lod)].push(chunk); - manager.processUploadsWithBudget(@sizeOf(Vertex)); - - _ = manager.meshes[@intFromEnum(key.lod)].remove(key); - manager.queueMeshDeletion(mesh); - manager.processMeshDeletions(1); - - try testing.expectEqual(@as(u32, 0), mock.wait_idle_calls); - try testing.expectEqual(@as(u64, 0), manager.profiling.snapshot().wait_idle_count); - - manager.waitIdleTracked(.shutdown); - const profile = manager.profiling.snapshot(); - try testing.expectEqual(@as(u32, 1), mock.wait_idle_calls); - try testing.expectEqual(@as(u64, 0), profile.wait_idle_count); - try testing.expectEqual(@as(u64, 1), profile.wait_idle_shutdown_count); -} - -test "LOD upload budget permits zero-pending and unlimited uploads" { - const pending_bytes = @sizeOf(Vertex); - - try testing.expect(!wouldExceedUploadBudget(0, 0, 1)); - try testing.expect(!wouldExceedUploadBudget(0, pending_bytes, 0)); - try testing.expect(!wouldExceedUploadBudget(0, pending_bytes, std.math.maxInt(usize))); -} - -test "LODManager staging pressure failure stops upload sweep" { - var config = LODConfig{ .max_uploads_per_frame = 8 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - var mock = UploadMock{ .allocator = testing.allocator, .fail_with_pressure = true }; - manager.gpu_bridge = mock.bridge(); - - const first_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - const second_key = LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod1 }; - const first = try putTestRegion(&manager, first_key, .uploading); - const second = try putTestRegion(&manager, second_key, .uploading); - _ = try putTestPendingMesh(&manager, first_key, 1); - _ = try putTestPendingMesh(&manager, second_key, 1); - try manager.upload_queues[1].push(first); - try manager.upload_queues[1].push(second); - - manager.processUploadsWithBudget(DEFAULT_LOD_UPLOAD_BUDGET_BYTES); - - try testing.expectEqual(@as(u32, 1), mock.calls); - try testing.expectEqual(LODState.uploading, first.state); - try testing.expectEqual(LODState.uploading, second.state); - try testing.expectEqual(@as(usize, 2), manager.upload_queues[1].count()); - try testing.expectEqual(@as(u32, 1), manager.stats.upload_failures); -} - -test "LODManager pause cancels queued and pinned stale work" { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const queued_generation = try putTestRegion(&manager, .{ .rx = 0, .rz = 0, .lod = .lod1 }, .generating); - queued_generation.job_token = 11; - const queued_mesh = try putTestRegion(&manager, .{ .rx = 1, .rz = 0, .lod = .lod1 }, .meshing); - queued_mesh.job_token = 12; - const running_generation = try putTestRegion(&manager, .{ .rx = 2, .rz = 0, .lod = .lod1 }, .generating); - running_generation.job_token = 13; - running_generation.pin(); - defer running_generation.unpin(); - const running_mesh = try putTestRegion(&manager, .{ .rx = 3, .rz = 0, .lod = .lod1 }, .meshing); - running_mesh.job_token = 14; - running_mesh.pin(); - defer running_mesh.unpin(); - manager.pending_region_count = 4; - - const queue = manager.job_dispatcher.queues[LODLevel.count - 1]; - try queue.push(.{ .type = .chunk_generation, .data = .{ .chunk = .{ .x = 0, .z = 0, .job_token = 11, .lod_level = 1 } } }); - try queue.push(.{ .type = .chunk_meshing, .data = .{ .chunk = .{ .x = 1, .z = 0, .job_token = 12, .lod_level = 1 } } }); - - manager.pause(); - - try testing.expectEqual(@as(usize, 0), queue.count()); - try testing.expectEqual(LODState.missing, queued_generation.state); - try testing.expectEqual(@as(u32, 12), queued_generation.job_token); - try testing.expectEqual(LODState.generated, queued_mesh.state); - try testing.expectEqual(@as(u32, 13), queued_mesh.job_token); - try testing.expectEqual(LODState.missing, running_generation.state); - try testing.expectEqual(@as(u32, 14), running_generation.job_token); - try testing.expect(running_generation.cancellationRequested()); - try testing.expectEqual(LODState.generated, running_mesh.state); - try testing.expectEqual(@as(u32, 15), running_mesh.job_token); - try testing.expect(running_mesh.cancellationRequested()); - try testing.expectEqual(@as(usize, 2), manager.pending_region_count); - try testing.expectEqual(@as(u32, 4), manager.cancelled_jobs); -} - -test "LODManager traversal cancels work outside its level horizon" { - var config = LODConfig{ .radii = .{ 8, 16, 32, 64, 128 } }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const near = try putTestRegion(&manager, .{ .rx = 0, .rz = 0, .lod = .lod1 }, .generating); - near.job_token = 7; - const stale = try putTestRegion(&manager, .{ .rx = 100, .rz = 100, .lod = .lod1 }, .generating); - stale.job_token = 9; - stale.cache_read_queued = true; - manager.pending_region_count = 2; - - cancelWorkOutsideHorizon(&manager, 0, 0); - - try testing.expectEqual(LODState.generating, near.state); - try testing.expectEqual(@as(u32, 7), near.job_token); - try testing.expect(!near.cancellationRequested()); - try testing.expectEqual(LODState.missing, stale.state); - try testing.expectEqual(@as(u32, 10), stale.job_token); - try testing.expect(stale.cancellationRequested()); - try testing.expect(!stale.cache_read_queued); - try testing.expectEqual(@as(usize, 1), manager.pending_region_count); - try testing.expectEqual(@as(u32, 1), manager.cancelled_jobs); -} - -test "LODManager ready child counters update on renderable transitions and removal" { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const child_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod1 }; - const child = try putTestRegion(&manager, child_key, .mesh_ready); - _ = try putTestMesh(&manager, child_key, 1); - manager.markRegionRenderable(child_key, child); - - const parent_key = child_key.parentKey().?; - const parent = try putTestRegion(&manager, parent_key, .mesh_ready); - manager.markRegionRenderable(parent_key, parent); - try testing.expectEqual(@as(u8, 1), parent.ready_children); - - manager.noteRegionRemoved(child_key, child); - try testing.expectEqual(@as(u8, 0), parent.ready_children); -} - -test "LODManager demoting renderable child clears parent coverage" { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const child_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod1 }; - const child = try putTestRegion(&manager, child_key, .mesh_ready); - _ = try putTestMesh(&manager, child_key, 1); - manager.markRegionRenderable(child_key, child); - - const parent_key = child_key.parentKey().?; - const parent = try putTestRegion(&manager, parent_key, .mesh_ready); - manager.markRegionRenderable(parent_key, parent); - try testing.expectEqual(@as(u8, 1), parent.ready_children); - - manager.demoteRegionForRemesh(child_key, child); - - try testing.expectEqual(LODState.generated, child.state); - try testing.expectEqual(@as(u8, 0), parent.ready_children); -} - -test "LODManager ready child counters ignore renderable children without geometry" { - var config = LODConfig{}; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const child_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod1 }; - const child = try putTestRegion(&manager, child_key, .mesh_ready); - _ = try putTestMesh(&manager, child_key, 0); - manager.markRegionRenderable(child_key, child); - - const parent_key = child_key.parentKey().?; - const parent = try putTestRegion(&manager, parent_key, .mesh_ready); - manager.markRegionRenderable(parent_key, parent); - - try testing.expectEqual(@as(u8, 0), parent.ready_children); -} - -test "LODManager memory budget eviction skips unsafe regions and evicts farthest first" { - var config = LODConfig{ .memory_budget_mb = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - - const budget_bytes = @as(usize, config.memory_budget_mb) * 1024 * 1024; - const eviction_bytes = 600 * 1024; - const mesh_capacity: u32 = @intCast(@max(eviction_bytes / @sizeOf(Vertex), 1)); - const mesh_bytes = @as(usize, mesh_capacity) * @sizeOf(Vertex); - - const near_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod1 }; - const far_key = LODRegionKey{ .rx = 20, .rz = 0, .lod = .lod1 }; - const pinned_key = LODRegionKey{ .rx = 40, .rz = 0, .lod = .lod1 }; - const no_parent_key = LODRegionKey{ .rx = 60, .rz = 0, .lod = .lod1 }; - const in_flight_key = LODRegionKey{ .rx = 80, .rz = 0, .lod = .lod1 }; - - _ = try putTestRegion(&manager, near_key.parentKey().?, .renderable); - _ = try putTestRegion(&manager, far_key.parentKey().?, .renderable); - _ = try putTestRegion(&manager, pinned_key.parentKey().?, .renderable); - _ = try putTestRegion(&manager, in_flight_key.parentKey().?, .renderable); - - _ = try putTestRegion(&manager, near_key, .renderable); - _ = try putTestRegion(&manager, far_key, .renderable); - const pinned_chunk = try putTestRegion(&manager, pinned_key, .renderable); - pinned_chunk.pin(); - _ = try putTestRegion(&manager, no_parent_key, .renderable); - _ = try putTestRegion(&manager, in_flight_key, .generating); - - _ = try putTestMesh(&manager, near_key, mesh_capacity); - _ = try putTestMesh(&manager, far_key, mesh_capacity); - _ = try putTestMesh(&manager, pinned_key, mesh_capacity); - _ = try putTestMesh(&manager, no_parent_key, mesh_capacity); - _ = try putTestMesh(&manager, in_flight_key, mesh_capacity); - - manager.memory_governor.used_bytes = budget_bytes + mesh_bytes; - try manager.enforceMemoryBudget(); - - try testing.expect(!manager.regions[1].contains(far_key)); - try testing.expect(!manager.meshes[1].contains(far_key)); - try testing.expect(manager.regions[1].contains(near_key)); - try testing.expect(manager.regions[1].contains(pinned_key)); - try testing.expect(manager.regions[1].contains(no_parent_key)); - try testing.expect(manager.regions[1].contains(in_flight_key)); - try testing.expect(manager.memory_governor.used_bytes <= budget_bytes); - try testing.expectEqual(@as(u32, 1), manager.stats.evictions); - try testing.expectEqual(@as(u32, 1), manager.mesh_disposal.queue.items.len); -} - -test "Phase 5 stress repeated teleport eviction cache recovery edits and upload pressure" { - // Counted cycles intentionally replace elapsed-time assertions. This makes - // the PR gate short while allowing the scheduled gate to exercise a much - // longer session with identical ordering and failure conditions. - const cycles = @min(@import("engine-core").envInt("ZIGCRAFT_PHASE5_STRESS_ITERATIONS", 64), 4096); - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - var config = LODConfig{ .memory_budget_mb = 1, .lod_store_size_cap_mb = 1 }; - var manager = try initEvictionTestManager(testing.allocator, &config); - defer deinitEvictionTestManager(&manager); - manager.profiling = .init(true); - manager.cache_store.cache_dir_path = null; - manager.cache_store.use_config_store_size_cap = true; - try manager.enableCache(save_dir); - defer if (manager.cache_store.cache_dir_path) |path| testing.allocator.free(path); - - var mock = UploadMock{ .allocator = testing.allocator }; - manager.gpu_bridge = mock.bridge(); - - // Seed an oversized valid container, then replace it through the async - // cache pipeline under the one-MiB cap. This takes the bounded compaction - // path instead of merely exercising a synchronous store helper. - const cache_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod2 }; - var source = try LODSimplifiedData.init(testing.allocator, .lod2); - defer source.deinit(); - source.setColumn(0, 0, 64.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4f8c45, .empty, .daylight, .empty); - const oversized = try testing.allocator.alloc(u8, 1300 * 1024); - defer testing.allocator.free(oversized); - @memset(oversized, 0x5a); - try lod_store.writePayload(testing.allocator, save_dir, manager.cacheKey(cache_key), oversized, lod_store.DEFAULT_STORE_SIZE_CAP_MB); - manager.saveCachedSourceData(cache_key, &source); - manager.flushCacheIO(); - const container_path = try lod_store.containerPath(testing.allocator, save_dir, manager.cacheKey(cache_key)); - defer testing.allocator.free(container_path); - const compacted_file = try fs.cwd().openFile(container_path, .{}); - const compacted_stat = try compacted_file.stat(); - compacted_file.close(); - try testing.expect(compacted_stat.size <= 1024 * 1024); - var cached = manager.loadCachedSourceData(cache_key) orelse return error.ExpectedCacheHit; - cached.deinit(); - - // A corrupt container must be discarded and subsequently recover through - // the asynchronous writer, rather than leaving a permanent cache miss. - const corrupt = try fs.cwd().createFile(container_path, .{ .truncate = true }); - try corrupt.writeAll("corrupt Phase 5 cache container"); - corrupt.close(); - try testing.expect(manager.loadCachedSourceData(cache_key) == null); - try testing.expectError(error.FileNotFound, fs.cwd().openFile(container_path, .{})); - manager.saveCachedSourceData(cache_key, &source); - manager.flushCacheIO(); - cached = manager.loadCachedSourceData(cache_key) orelse return error.ExpectedCacheRecovery; - cached.deinit(); - - const traveler_key = LODRegionKey{ .rx = 10_000, .rz = -10_000, .lod = .lod1 }; - const pausable_key = LODRegionKey{ .rx = 10_001, .rz = -10_000, .lod = .lod1 }; - const traveler = try putTestRegion(&manager, traveler_key, .missing); - const pausable = try putTestRegion(&manager, pausable_key, .missing); - const budget_bytes = @as(usize, config.memory_budget_mb) * 1024 * 1024; - const mesh_capacity: u32 = @intCast((600 * 1024) / @sizeOf(Vertex)); - const mesh_bytes = @as(usize, mesh_capacity) * @sizeOf(Vertex); - - for (0..cycles) |cycle| { - // Repeated long teleports must invalidate in-flight work without - // allowing a cancelled state to survive the next unpause. - traveler.resetCancellation(); - traveler.state = .generating; - traveler.job_token +%= 1; - manager.pending_region_count += 1; - cancelWorkOutsideHorizon(&manager, 0, 0); - try testing.expectEqual(LODState.missing, traveler.getState()); - try testing.expect(traveler.cancellationRequested()); - - pausable.resetCancellation(); - pausable.state = .meshing; - pausable.job_token +%= 1; - manager.pause(); - try testing.expectEqual(LODState.generated, pausable.getState()); - try testing.expect(pausable.cancellationRequested()); - manager.unpause(); - try testing.expect(!manager.paused); - - // Force a staging-pressure retry each cycle, then ensure the same - // upload completes once pressure clears. No streaming path may wait - // for the entire device to become idle. - const upload_key = LODRegionKey{ .rx = @intCast(20_000 + cycle), .rz = 0, .lod = .lod1 }; - const upload_chunk = try putTestRegion(&manager, upload_key, .uploading); - const upload_mesh = try putTestPendingMesh(&manager, upload_key, 1); - try manager.upload_queues[@intFromEnum(upload_key.lod)].push(upload_chunk); - mock.fail_with_pressure = true; - manager.processUploadsWithBudget(DEFAULT_LOD_UPLOAD_BUDGET_BYTES); - try testing.expectEqual(LODState.uploading, upload_chunk.getState()); - mock.fail_with_pressure = false; - manager.processUploadsWithBudget(DEFAULT_LOD_UPLOAD_BUDGET_BYTES); - try testing.expectEqual(LODState.renderable, upload_chunk.getState()); - _ = manager.meshes[@intFromEnum(upload_key.lod)].remove(upload_key); - manager.queueMeshDeletion(upload_mesh); - upload_chunk.deinit(testing.allocator); - testing.allocator.destroy(upload_chunk); - _ = manager.regions[@intFromEnum(upload_key.lod)].remove(upload_key); - manager.processMeshDeletions(1); - - // A parent fallback permits the finer child to be evicted under the - // one-MiB cap. Repeating the operation catches stale resident entries - // and deferred-deletion growth across a long teleport session. - const child_key = LODRegionKey{ .rx = @intCast(cycle * 2), .rz = 64, .lod = .lod1 }; - const parent_key = child_key.parentKey().?; - _ = try putTestRegion(&manager, parent_key, .renderable); - _ = try putTestRegion(&manager, child_key, .renderable); - _ = try putTestMesh(&manager, child_key, mesh_capacity); - manager.memory_governor.used_bytes = budget_bytes + mesh_bytes; - try manager.enforceMemoryBudget(); - try testing.expect(!manager.regions[@intFromEnum(child_key.lod)].contains(child_key)); - try testing.expect(manager.memory_governor.used_bytes <= budget_bytes); - manager.processMeshDeletions(1); - - // Edits are deliberately repeated across a small set of coordinates; - // the coalescing set must remain bounded even as other work churns. - manager.markChunkEdited(@intCast(cycle % 8), @intCast(-@as(i64, @intCast(cycle % 8)))); - try testing.expect(manager.ingestion_queue.edit_dirty.count() <= 8); - } - - // Saturate logical admission after the low-memory eviction loop. The - // scheduler must not add another resident region when its reservation - // would exceed the configured cap. - const resident_before = manager.regions[@intFromEnum(LODLevel.lod1)].count(); - manager.memory_governor.logical_admission_bytes = budget_bytes; - manager.cleanup_covered_regions = false; - try manager.queueLODRegions(.lod1, Vec3.zero, null, null); - try testing.expectEqual(resident_before, manager.regions[@intFromEnum(LODLevel.lod1)].count()); - - try testing.expect(mock.calls >= cycles * 2); - try testing.expect(manager.stats.upload_failures >= cycles); - try testing.expect(manager.cancelled_jobs >= cycles * 2); - try testing.expectEqual(@as(u32, 0), mock.wait_idle_calls); - try testing.expectEqual(@as(u64, 0), manager.profiling.snapshot().wait_idle_count); - for (0..LODLevel.count) |lod_idx| { - var regions = manager.regions[lod_idx].iterator(); - while (regions.next()) |entry| switch (entry.value_ptr.*.getState()) { - .queued_for_generation, .generating, .meshing, .uploading => return error.StuckLODState, - else => {}, - }; - try testing.expectEqual(@as(usize, 0), manager.upload_queues[lod_idx].count()); - } -} diff --git a/modules/world-lod/src/lod_manager_tests.zig b/modules/world-lod/src/lod_manager_tests.zig deleted file mode 100644 index f9d9032a..00000000 --- a/modules/world-lod/src/lod_manager_tests.zig +++ /dev/null @@ -1,679 +0,0 @@ -const std = @import("std"); - -const TextureAtlas = @import("engine-assets").TextureAtlas; -const Mat4 = @import("engine-math").Mat4; -const Vec3 = @import("engine-math").Vec3; -const rhi_types = @import("engine-rhi").rhi_types; -const Chunk = @import("world-core").Chunk; -const world_core = @import("world-core"); -const lod_manager = @import("lod_manager.zig"); -const LODManager = lod_manager.LODManager; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = lod_manager.LODStats; -const LODProfilingCollector = @import("lod_stats.zig").LODProfilingCollector; -const MAX_LOD_REGIONS = @import("lod_manager_context.zig").MAX_LOD_REGIONS; -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODConfig = lod_chunk.LODConfig; -const ILODConfig = lod_chunk.ILODConfig; -const LODChunk = lod_chunk.LODChunk; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; -const LODColumnProvenance = world_core.LODColumnProvenance; -const regionSizeBlocks = world_core.regionSizeBlocks; -const world_worldgen = @import("world-worldgen"); -const ColumnInfo = world_worldgen.ColumnInfo; -const Generator = world_worldgen.Generator; -const RegionInfo = world_worldgen.region.RegionInfo; - -fn lodGeneratorFromGenerator(generator: Generator) LODGenerator { - return .{ - .ptr = generator.ptr, - .generate_heightmap_only = generator.vtable.generateHeightmapOnly, - .maybe_recenter_cache = generator.vtable.maybeRecenterCache, - .seed = generator.getSeed(), - .identity_hash = std.hash.Wyhash.hash(0, generator.info.name), - .version = generator.info.version, - }; -} - -test "LODManager initialization" { - const allocator = std.testing.allocator; - - const MockState = struct { - buffer_created: bool = false, - buffer_destroyed: bool = false, - prepare_saw_stats: bool = false, - prepare_saw_profiling: bool = false, - }; - - const MockGenerator = struct { - fn generate(_: *anyopaque, _: *Chunk, _: ?*const bool) error{OutOfMemory}!void {} - fn generateHeightmapOnly(_: *anyopaque, _: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void {} - fn maybeRecenterCache(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - fn getSeed(_: *anyopaque) u64 { - return 0; - } - fn getRegionInfo(_: *anyopaque, _: i32, _: i32) RegionInfo { - return undefined; - } - fn getColumnInfo(_: *anyopaque, _: f32, _: f32) ColumnInfo { - return undefined; - } - fn deinit(_: *anyopaque, _: std.mem.Allocator) void {} - - const vtable = Generator.VTable{ - .generate = generate, - .generateHeightmapOnly = generateHeightmapOnly, - .maybeRecenterCache = maybeRecenterCache, - .getSeed = getSeed, - .getRegionInfo = getRegionInfo, - .getColumnInfo = getColumnInfo, - .deinit = deinit, - }; - }; - - var mock_gen_impl = MockGenerator{}; - const mock_gen = Generator{ - .ptr = &mock_gen_impl, - .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, - }; - - var config = LODConfig{ - .radii = .{ 8, 16, 32, 64, 128 }, - }; - - var mock_state = MockState{}; - const mock_bridge = LODGPUBridge{ - .on_upload = struct { - fn f(_: *LODMesh, _: *anyopaque) rhi_types.RhiError!void {} - }.f, - .on_destroy = struct { - fn f(_: *LODMesh, ctx: *anyopaque) void { - const state: *MockState = @ptrCast(@alignCast(ctx)); - state.buffer_destroyed = true; - } - }.f, - .on_wait_idle = struct { - fn f(_: *anyopaque) void {} - }.f, - .ctx = @ptrCast(&mock_state), - }; - - const mock_render = LODRenderInterface{ - .render_fn = struct { - fn f(_: *anyopaque, _: *const [LODLevel.count]MeshMap, _: *const [LODLevel.count]RegionMap, _: ILODConfig, _: Mat4, _: Vec3, _: ?LODManager.ChunkChecker, _: ?*anyopaque, _: bool, _: ?i32, _: lod_gpu.LODRenderLayer, _: ?*LODStats, _: ?*LODProfilingCollector) void {} - }.f, - .prepare_frame_fn = struct { - fn f(ctx: *anyopaque, _: u64, _: *const [LODLevel.count]MeshMap, _: *const [LODLevel.count]RegionMap, _: ILODConfig, _: Mat4, _: Vec3, _: ?LODManager.ChunkChecker, _: ?*anyopaque, _: ?i32, _: i32, stats: ?*LODStats, profiling: ?*LODProfilingCollector) void { - const state: *MockState = @ptrCast(@alignCast(ctx)); - state.prepare_saw_stats = stats != null; - state.prepare_saw_profiling = profiling != null; - } - }.f, - .deinit_fn = struct { - fn f(_: *anyopaque) void {} - }.f, - .ptr = @ptrCast(&mock_state), - }; - - const mock_atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - - var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, lodGeneratorFromGenerator(mock_gen), &mock_atlas); - mgr.cleanup_covered_regions = false; - - const stats = mgr.getStats(); - try std.testing.expectEqual(@as(u32, 0), stats.totalLoaded()); - try std.testing.expectEqual(@as(u32, 0), stats.totalGenerating()); - mgr.profiling.enabled = true; - mgr.prepareFrame(1, Mat4.identity, Vec3.zero, null, null, null, mgr.config.getChunkRenderRadius()); - try std.testing.expect(mock_state.prepare_saw_stats); - try std.testing.expect(mock_state.prepare_saw_profiling); - - mgr.deinit(); - - try std.testing.expectEqual(LODLevel.lod0, config.getLODForDistance(5)); - try std.testing.expectEqual(LODLevel.lod1, config.getLODForDistance(12)); - try std.testing.expectEqual(LODLevel.lod2, config.getLODForDistance(24)); - try std.testing.expectEqual(LODLevel.lod3, config.getLODForDistance(50)); -} - -test "LODManager end-to-end covered cleanup" { - const allocator = std.testing.allocator; - - const MockGenerator = struct { - fn generate(_: *anyopaque, _: *Chunk, _: ?*const bool) error{OutOfMemory}!void {} - fn generateHeightmapOnly(_: *anyopaque, _: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void {} - fn maybeRecenterCache(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - fn getSeed(_: *anyopaque) u64 { - return 0; - } - fn getRegionInfo(_: *anyopaque, _: i32, _: i32) RegionInfo { - return undefined; - } - fn getColumnInfo(_: *anyopaque, _: f32, _: f32) ColumnInfo { - return .{ .height = 0, .biome = .plains, .is_ocean = false, .temperature = 0, .humidity = 0, .continentalness = 0 }; - } - fn deinit(_: *anyopaque, _: std.mem.Allocator) void {} - - const vtable = Generator.VTable{ - .generate = generate, - .generateHeightmapOnly = generateHeightmapOnly, - .maybeRecenterCache = maybeRecenterCache, - .getSeed = getSeed, - .getRegionInfo = getRegionInfo, - .getColumnInfo = getColumnInfo, - .deinit = deinit, - }; - }; - - var mock_gen_impl = MockGenerator{}; - const mock_gen = Generator{ - .ptr = &mock_gen_impl, - .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, - }; - - var config = LODConfig{ - .radii = .{ 2, 4, 8, 16, 32 }, - }; - - var noop_ctx: u8 = 0; - const mock_bridge = LODGPUBridge{ - .on_upload = struct { - fn f(_: *LODMesh, _: *anyopaque) rhi_types.RhiError!void {} - }.f, - .on_destroy = struct { - fn f(_: *LODMesh, _: *anyopaque) void {} - }.f, - .on_wait_idle = struct { - fn f(_: *anyopaque) void {} - }.f, - .ctx = @ptrCast(&noop_ctx), - }; - - const mock_render = LODRenderInterface{ - .render_fn = struct { - fn f(_: *anyopaque, _: *const [LODLevel.count]MeshMap, _: *const [LODLevel.count]RegionMap, _: ILODConfig, _: Mat4, _: Vec3, _: ?LODManager.ChunkChecker, _: ?*anyopaque, _: bool, _: ?i32, _: lod_gpu.LODRenderLayer, _: ?*LODStats, _: ?*LODProfilingCollector) void {} - }.f, - .deinit_fn = struct { - fn f(_: *anyopaque) void {} - }.f, - .ptr = @ptrCast(&noop_ctx), - }; - - const mock_atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - - var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, lodGeneratorFromGenerator(mock_gen), &mock_atlas); - mgr.cleanup_covered_regions = false; - defer mgr.deinit(); - - try mgr.update(Vec3.zero, Vec3.zero, null, null); - - const Checker = struct { - pub fn isLoaded(_: i32, _: i32, _: *anyopaque) bool { - return false; - } - }; - - const key = lod_chunk.LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod1 }; - const chunk = try allocator.create(LODChunk); - chunk.* = LODChunk.init(1, 0, .lod1); - chunk.state = .renderable; - try mgr.regions[1].put(key, chunk); - - const mesh = try allocator.create(LODMesh); - mesh.* = LODMesh.init(allocator, .lod1); - mesh.ready = true; - mesh.vertex_count = 100; - try mgr.meshes[1].put(key, mesh); - - var dummy: u8 = 0; - try mgr.update(Vec3.zero, Vec3.zero, Checker.isLoaded, &dummy); - try std.testing.expect(mgr.regions[1].contains(key)); - - const FullChecker = struct { - pub fn isLoaded(_: i32, _: i32, _: *anyopaque) bool { - return true; - } - }; - - try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); - try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); - try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); - try mgr.update(Vec3.zero, Vec3.zero, FullChecker.isLoaded, &dummy); - - try std.testing.expect(mgr.regions[1].contains(key)); -} - -test "LODStats aggregation" { - var stats = LODStats{}; - stats.recordState(1, .renderable); - stats.recordState(1, .renderable); - stats.recordState(2, .generating); - - try std.testing.expectEqual(@as(u32, 2), stats.loaded[1]); - try std.testing.expectEqual(@as(u32, 1), stats.generating[2]); - try std.testing.expectEqual(@as(u32, 2), stats.totalLoaded()); - try std.testing.expectEqual(@as(u32, 1), stats.totalGenerating()); - - stats.addMemory(2 * 1024 * 1024); - try std.testing.expectEqual(@as(u32, 2), stats.memory_used_mb); - stats.profiling.enabled = true; - - stats.reset(); - try std.testing.expectEqual(@as(u32, 0), stats.totalLoaded()); - try std.testing.expectEqual(@as(u32, 0), stats.memory_used_mb); - try std.testing.expect(!stats.profiling.enabled); -} - -test "LODManager constants" { - try std.testing.expect(MAX_LOD_REGIONS > 0); - try std.testing.expect(LODLevel.count >= 2); -} - -test "LODManager preserves the CPU heightfield fallback for far LODs" { - var config = LODConfig{ .mesh_path = .qem }; - const manager = try buildIngestionManager(std.testing.allocator, &config); - defer manager.deinit(); - - // Far LODs must not inherit an optional mesh path. They retain the - // CPU-expanded heightfield route when a future renderer path is unavailable. - try std.testing.expectEqual(@import("engine-rhi").LODMeshPath.heightfield, manager.effectiveMeshPath(.lod3)); - try std.testing.expectEqual(@import("engine-rhi").LODMeshPath.heightfield, manager.effectiveMeshPath(.lod4)); -} - -// --------------------------------------------------------------------------- -// Chunk-derived LOD ingestion (issue #752 Phase 2) -// --------------------------------------------------------------------------- - -/// Minimal manager + mock glue shared by the ingestion tests. The caller owns -/// `config` (it must outlive the returned manager, since ILODConfig references it). -fn buildIngestionManager(allocator: std.mem.Allocator, config: *LODConfig) !*LODManager { - const MockGenerator = struct { - fn generate(_: *anyopaque, _: *Chunk, _: ?*const bool) error{OutOfMemory}!void {} - fn generateHeightmapOnly(_: *anyopaque, _: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void {} - fn maybeRecenterCache(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - fn getSeed(_: *anyopaque) u64 { - return 0; - } - fn getRegionInfo(_: *anyopaque, _: i32, _: i32) RegionInfo { - return undefined; - } - fn getColumnInfo(_: *anyopaque, _: f32, _: f32) ColumnInfo { - return .{ .height = 0, .biome = .plains, .is_ocean = false, .temperature = 0, .humidity = 0, .continentalness = 0 }; - } - fn deinit(_: *anyopaque, _: std.mem.Allocator) void {} - - const vtable = Generator.VTable{ - .generate = generate, - .generateHeightmapOnly = generateHeightmapOnly, - .maybeRecenterCache = maybeRecenterCache, - .getSeed = getSeed, - .getRegionInfo = getRegionInfo, - .getColumnInfo = getColumnInfo, - .deinit = deinit, - }; - }; - - var mock_gen_impl = MockGenerator{}; - const mock_gen = Generator{ - .ptr = &mock_gen_impl, - .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, - }; - - var noop_ctx: u8 = 0; - const mock_bridge = LODGPUBridge{ - .on_upload = struct { - fn f(_: *LODMesh, _: *anyopaque) rhi_types.RhiError!void {} - }.f, - .on_destroy = struct { - fn f(_: *LODMesh, _: *anyopaque) void {} - }.f, - .on_wait_idle = struct { - fn f(_: *anyopaque) void {} - }.f, - .ctx = @ptrCast(&noop_ctx), - }; - - const mock_render = LODRenderInterface{ - .render_fn = struct { - fn f(_: *anyopaque, _: *const [LODLevel.count]MeshMap, _: *const [LODLevel.count]RegionMap, _: ILODConfig, _: Mat4, _: Vec3, _: ?LODManager.ChunkChecker, _: ?*anyopaque, _: bool, _: ?i32, _: lod_gpu.LODRenderLayer, _: ?*LODStats, _: ?*LODProfilingCollector) void {} - }.f, - .deinit_fn = struct { - fn f(_: *anyopaque) void {} - }.f, - .ptr = @ptrCast(&noop_ctx), - }; - - const mock_atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - - const mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, lodGeneratorFromGenerator(mock_gen), &mock_atlas); - mgr.cleanup_covered_regions = false; - return mgr; -} - -/// Place a region at (rx,rz,lod) with simplified data, pre-seeding cell -/// (gx,gz) as worldgen so ingestion can be observed upgrading it. -fn placeSimplifiedRegion(mgr: *LODManager, allocator: std.mem.Allocator, rx: i32, rz: i32, lod: LODLevel) !*LODChunk { - const key = lod_chunk.LODRegionKey{ .rx = rx, .rz = rz, .lod = lod }; - const lchunk = try allocator.create(LODChunk); - lchunk.* = LODChunk.init(rx, rz, lod); - lchunk.data = .{ .simplified = try LODSimplifiedData.init(allocator, lod) }; - lchunk.state = .renderable; - try mgr.regions[@intFromEnum(lod)].put(key, lchunk); - return lchunk; -} - -test "ingestChunk upgrades worldgen region data and schedules a remesh" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - // Pre-seed cell (0,0) as worldgen at height 10. - lchunk.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .{ .is_surface = false, .surface_height = 0, .depth = 0, .coverage = 0 }, .{ .sky_light = 15, .block_light = 0, .ambient_occlusion = 1.0 }, .{ .tree_coverage = 0, .avg_tree_height = 0, .offset_x = 0, .offset_z = 0, .trunk = .air, .leaves = .air }); - try std.testing.expectEqual(LODColumnProvenance.worldgen, lchunk.data.simplified.getColumnProvenance(0, 0)); - - // Real chunk at (0,0) with terrain at y=64. - var chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 64) : (y += 1) { - const block: world_core.BlockType = if (y == 64) .grass else if (y > 60) .dirt else .stone; - chunk.setBlock(0, y, 0, block); - } - - mgr.ingestChunk(0, 0, &chunk, .chunk_derived); - - try std.testing.expectEqual(@as(f32, 64.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.chunk_derived, lchunk.data.simplified.getColumnProvenance(0, 0)); - // A renderable region is flipped back to .generated to trigger a remesh. - try std.testing.expectEqual(lod_chunk.LODState.generated, lchunk.state); - try std.testing.expect(lchunk.dirty); - try std.testing.expect(lchunk.store_dirty); -} - -test "ingestChunk defers while a cancelled worker still pins source data" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - lchunk.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .empty, .daylight, .empty); - lchunk.state = .generated; - lchunk.pin(); - defer lchunk.unpin(); - - var chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 64) : (y += 1) chunk.setBlock(0, y, 0, .stone); - - mgr.ingestChunk(0, 0, &chunk, .edited); - - try std.testing.expectEqual(@as(f32, 10.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.worldgen, lchunk.data.simplified.getColumnProvenance(0, 0)); - try std.testing.expect(mgr.ingestion_queue.pending_ingestions.items.len > 0); -} - -test "ingestChunk into generated source invalidates stale mesh transition" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - lchunk.state = .generated; - const old_token = lchunk.job_token; - - var chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 64) : (y += 1) chunk.setBlock(0, y, 0, .stone); - - mgr.ingestChunk(0, 0, &chunk, .edited); - - try std.testing.expectEqual(old_token + 1, lchunk.job_token); - const token = mgr.transition_tokens.pop() orelse return error.ExpectedMeshTransition; - try std.testing.expectEqual(@import("lod_manager_context.zig").LifecycleStage.mesh, token.stage); - try std.testing.expectEqual(lchunk.job_token, token.job_token); - try std.testing.expectEqual(lchunk.source_revision, token.source_revision); -} - -test "ingestChunk provenance authority: edited beats chunk_derived, worldgen cannot overwrite" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - - var derived_chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 50) : (y += 1) chunk_derived_setBlock(&derived_chunk, 0, y, 0, .stone); - - // 1. chunk_derived sets height to 50. - mgr.ingestChunk(0, 0, &derived_chunk, .chunk_derived); - try std.testing.expectEqual(@as(f32, 50.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.chunk_derived, lchunk.data.simplified.getColumnProvenance(0, 0)); - - // 2. An edit raises the column to y=80 with edited provenance. - var edited_chunk = Chunk.init(0, 0); - var ey: u32 = 0; - while (ey <= 80) : (ey += 1) chunk_derived_setBlock(&edited_chunk, 0, ey, 0, .stone); - mgr.ingestChunk(0, 0, &edited_chunk, .edited); - try std.testing.expectEqual(@as(f32, 80.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.edited, lchunk.data.simplified.getColumnProvenance(0, 0)); - - // 3. A later worldgen regeneration must NOT overwrite the edited column. - var regen_chunk = Chunk.init(0, 0); - var ry: u32 = 0; - while (ry <= 5) : (ry += 1) chunk_derived_setBlock(®en_chunk, 0, ry, 0, .stone); - mgr.ingestChunk(0, 0, ®en_chunk, .worldgen); - try std.testing.expectEqual(@as(f32, 80.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.edited, lchunk.data.simplified.getColumnProvenance(0, 0)); -} - -test "markChunkEdited coalesces and re-ingests via the resolver on update" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - - // Resolver returns a fixed edited chunk for any coordinate. - const ResolverState = struct { chunk: *const Chunk }; - var edited_chunk = Chunk.init(0, 0); - var ey: u32 = 0; - while (ey <= 90) : (ey += 1) chunk_derived_setBlock(&edited_chunk, 0, ey, 0, .stone); - var rstate = ResolverState{ .chunk = &edited_chunk }; - const resolve_fn = struct { - fn f(ptr: *anyopaque, cx: i32, cz: i32) ?*const Chunk { - _ = cx; - _ = cz; - const s: *ResolverState = @ptrCast(@alignCast(ptr)); - return s.chunk; - } - }.f; - mgr.setChunkResolver(.{ .ptr = @ptrCast(&rstate), .resolve_fn = resolve_fn }); - - // Queue several rapid edits to the same chunk; they should coalesce. - mgr.markChunkEdited(0, 0); - mgr.markChunkEdited(0, 0); - mgr.markChunkEdited(0, 0); - - // update() drains the edit queue on its cooldown (starts expired). - for (0..4) |_| try mgr.update(Vec3.zero, Vec3.zero, null, null); - - try std.testing.expectEqual(@as(f32, 90.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.edited, lchunk.data.simplified.getColumnProvenance(0, 0)); - - // Explicit save points bypass the coalescing cooldown and persist edits in - // the same transaction rather than losing them to a later frame. - edited_chunk.setBlock(0, 90, 0, .air); - lchunk.setState(.renderable); - if (lchunk.isPinned()) lchunk.unpin(); - mgr.markChunkEdited(0, 0); - mgr.ingestion_queue.edit_cooldown = 1.0; - mgr.flushEditedChunksNow(); - try std.testing.expectEqual(@as(f32, 89.0), lchunk.data.simplified.getHeight(0, 0)); -} - -test "edited chunk unload consumes queued work before resolver removal" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 }, .active_lod_count = 2 }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - lchunk.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .empty, .daylight, .empty); - - var edited_chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 72) : (y += 1) chunk_derived_setBlock(&edited_chunk, 0, y, 0, .stone); - mgr.markChunkEdited(0, 0); - - try std.testing.expectEqual(@as(u8, 0), mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, false)); - try std.testing.expectEqual(@as(f32, 72.0), lchunk.data.simplified.getHeight(0, 0)); - try std.testing.expectEqual(LODColumnProvenance.edited, lchunk.data.simplified.getColumnProvenance(0, 0)); - try std.testing.expectEqual(@as(usize, 0), mgr.ingestion_queue.edit_dirty.count()); - try std.testing.expectEqual(@as(u8, 0), mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, false)); -} - -test "edited chunk unload retains in-flight LOD work inside the horizon" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 }, .active_lod_count = 2 }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lchunk = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - lchunk.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .empty, .daylight, .empty); - lchunk.state = .meshing; - - var edited_chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 72) : (y += 1) chunk_derived_setBlock(&edited_chunk, 0, y, 0, .stone); - mgr.markChunkEdited(0, 0); - - const lod1_mask = @as(u8, 1) << @intFromEnum(LODLevel.lod1); - try std.testing.expectEqual(lod1_mask, mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, true)); - try std.testing.expectEqual(@as(usize, 1), mgr.ingestion_queue.pending_ingestions.items.len); - try std.testing.expectEqual(@as(f32, 10.0), lchunk.data.simplified.getHeight(0, 0)); - - lchunk.state = .renderable; - try std.testing.expectEqual(@as(u8, 0), mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, true)); - try std.testing.expectEqual(@as(usize, 0), mgr.ingestion_queue.pending_ingestions.items.len); - try std.testing.expectEqual(@as(f32, 72.0), lchunk.data.simplified.getHeight(0, 0)); -} - -test "deferred edited ingestion retries only levels still pending" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 }, .active_lod_count = 3 }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - const lod1 = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod1); - const lod2 = try placeSimplifiedRegion(mgr, allocator, 0, 0, .lod2); - lod1.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .empty, .daylight, .empty); - lod2.data.simplified.setColumn(0, 0, 10.0, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0x4D8033, .empty, .daylight, .empty); - lod2.state = .meshing; - - var edited_chunk = Chunk.init(0, 0); - var y: u32 = 0; - while (y <= 72) : (y += 1) chunk_derived_setBlock(&edited_chunk, 0, y, 0, .stone); - mgr.markChunkEdited(0, 0); - - const lod2_mask = @as(u8, 1) << @intFromEnum(LODLevel.lod2); - try std.testing.expectEqual(lod2_mask, mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, true)); - const lod1_revision = lod1.source_revision; - try std.testing.expectEqual(@as(f32, 72.0), lod1.data.simplified.getHeight(0, 0)); - - lod2.state = .renderable; - try std.testing.expectEqual(@as(u8, 0), mgr.flushEditedChunkForUnload(0, 0, &edited_chunk, true)); - try std.testing.expectEqual(lod1_revision, lod1.source_revision); - try std.testing.expectEqual(@as(f32, 72.0), lod2.data.simplified.getHeight(0, 0)); -} - -test "bounded edited chunk flush preserves work beyond the frame budget" { - const allocator = std.testing.allocator; - var config = LODConfig{ .active_lod_count = 2 }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - mgr.ingestion_queue.drain_per_frame = 2; - - mgr.markChunkEdited(0, 0); - mgr.markChunkEdited(1, 0); - mgr.markChunkEdited(2, 0); - mgr.flushEditedChunksBounded(); - - try std.testing.expectEqual(@as(usize, 1), mgr.ingestion_queue.edit_dirty.count()); - try std.testing.expectEqual(@as(usize, 2), mgr.ingestion_queue.pending_ingestions.items.len); -} - -fn chunk_derived_setBlock(chunk: *Chunk, x: u32, y: u32, z: u32, block: world_core.BlockType) void { - chunk.setBlock(x, y, z, block); -} - -test "enforceMemoryBudget shrinks finer radii under sustained pressure and spares the horizon" { - const allocator = std.testing.allocator; - var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 }, .memory_budget_mb = 1 }; - const mgr = try buildIngestionManager(allocator, &config); - defer mgr.deinit(); - - // Force sustained over-budget state with no evictable regions, so the - // hysteresis grow path must fire. - mgr.memory_governor.used_bytes = 50_000_000; - try mgr.enforceMemoryBudget(); - - // Finer levels (1..count-2) must have grown; coarsest (horizon) untouched. - try std.testing.expect(mgr.memory_governor.radius_shrink_chunks[1] > 0); - try std.testing.expectEqual(@as(i32, 0), mgr.memory_governor.radius_shrink_chunks[LODLevel.count - 1]); -} diff --git a/modules/world-lod/src/lod_manager_upload_ops.zig b/modules/world-lod/src/lod_manager_upload_ops.zig deleted file mode 100644 index b1fe578e..00000000 --- a/modules/world-lod/src/lod_manager_upload_ops.zig +++ /dev/null @@ -1,417 +0,0 @@ -const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; -const fs = @import("fs"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODConfig = lod_chunk.LODConfig; -const LODState = lod_chunk.LODState; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const ILODConfig = lod_chunk.ILODConfig; -const world_core = @import("world-core"); -const Chunk = world_core.Chunk; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; -const LODColumnProvenance = world_core.LODColumnProvenance; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const Vertex = @import("engine-rhi").Vertex; -const engine_core = @import("engine-core"); -const log = engine_core.log; -const JobSystem = engine_core.job_system; -const JobQueue = JobSystem.JobQueue; -const WorkerPool = JobSystem.WorkerPool; -const Job = JobSystem.Job; -const RingBuffer = engine_core.ring_buffer.RingBuffer; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const ChunkChecker = lod_gpu.ChunkChecker; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const lod_scheduler = @import("lod_scheduler.zig"); -const lod_cache = @import("lod_cache.zig"); -const lod_store = @import("lod_store.zig"); -const lod_ingest = @import("lod_ingest.zig"); -const TextureAtlas = @import("engine-assets").TextureAtlas; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const LODStats = @import("lod_stats.zig").LODStats; -const manager_ctx = @import("lod_manager_context.zig"); -const ChunkCoordKey = manager_ctx.ChunkCoordKey; -const ChunkCoordKeyContext = manager_ctx.ChunkCoordKeyContext; -const ChunkCoordSet = std.HashMap(ChunkCoordKey, void, ChunkCoordKeyContext, std.hash_map.default_max_load_percentage); -const ChunkResolver = manager_ctx.ChunkResolver; -const PendingIngestion = manager_ctx.PendingIngestion; -const PlayerChunkPos = manager_ctx.PlayerChunkPos; -const MAX_CACHE_LOADS_PER_UPDATE = manager_ctx.MAX_CACHE_LOADS_PER_UPDATE; -const MAX_MEMORY_EVICTIONS_PER_UPDATE = manager_ctx.MAX_MEMORY_EVICTIONS_PER_UPDATE; -const MAX_MESH_DELETIONS_PER_SWEEP = manager_ctx.MAX_MESH_DELETIONS_PER_SWEEP; -const DEFAULT_LOD_UPLOAD_BUDGET_BYTES = manager_ctx.DEFAULT_LOD_UPLOAD_BUDGET_BYTES; -const LOD_UPLOAD_BUDGET_ENV = manager_ctx.LOD_UPLOAD_BUDGET_ENV; -const LOD_UPDATE_DIVISOR = manager_ctx.LOD_UPDATE_DIVISOR; -const DELETION_SWEEP_SECONDS = manager_ctx.DELETION_SWEEP_SECONDS; -const CHUNK_COVERAGE_PADDING = manager_ctx.CHUNK_COVERAGE_PADDING; -const MIN_LOD_WORKERS = manager_ctx.MIN_LOD_WORKERS; -const MAX_LOD_WORKERS = manager_ctx.MAX_LOD_WORKERS; -const MAX_PENDING_INGESTIONS = manager_ctx.MAX_PENDING_INGESTIONS; -const PENDING_INGESTION_TTL = manager_ctx.PENDING_INGESTION_TTL; -const EDIT_FLUSH_COOLDOWN = manager_ctx.EDIT_FLUSH_COOLDOWN; -const LOD_FRAME_DT_APPROX = manager_ctx.LOD_FRAME_DT_APPROX; -const lodUploadBudgetBytes = manager_ctx.lodUploadBudgetBytes; -const wouldExceedUploadBudget = manager_ctx.wouldExceedUploadBudget; -const isUploadPressureError = manager_ctx.isUploadPressureError; -const MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE = manager_ctx.MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE; - -/// Rendering can discover a compact submission failure, but may not retire GPU -/// state while it holds the manager's render lock. Bound update-side recovery -/// so a pathological driver cannot monopolize one frame. -const MAX_COMPACT_DRAW_RECOVERIES_PER_UPDATE: usize = 4; - -/// Consumes compact draw failures recorded by the renderer. This runs only on -/// the manager update thread: it pins immutable source data, retires compact -/// GPU storage through the bridge, builds the expanded CPU mesh, then requeues -/// the normal bounded upload path. Workers never call the bridge. -pub fn recoverCompactDrawFailures(self: *Self) void { - const RecoveryTask = struct { - key: LODRegionKey, - chunk: *LODChunk, - mesh: *LODMesh, - lod_idx: usize, - job_token: u32, - source_revision: u32, - }; - - var recovered: usize = 0; - while (recovered < MAX_COMPACT_DRAW_RECOVERIES_PER_UPDATE) : (recovered += 1) { - var task: ?RecoveryTask = null; - self.mutex.lock(); - find_failure: for (0..LODLevel.count) |lod_idx| { - var mesh_iter = self.meshes[lod_idx].iterator(); - while (mesh_iter.next()) |entry| { - const key = entry.key_ptr.*; - const chunk = self.regions[lod_idx].get(key) orelse continue; - const mesh = entry.value_ptr.*; - // The source identity guards against a render failure from a - // representation superseded by ingestion, cache reload, or a - // cancelled lifecycle token. - if (!mesh.compactDrawFailureMatches(chunk.job_token, chunk.source_revision)) continue; - if (chunk.getState() != .renderable) continue; - - chunk.pin(); - chunk.compact_disabled = true; - self.profiling.addCompactDisabled(); - // `markCompactDrawFailed` already made the mesh non-renderable, - // so `noteRegionRemoved` cannot infer its former contribution. - // Account for it explicitly to keep the parent visible between - // the failed frame and successful expanded upload. - self.adjustParentReadyChildren(key, -1); - self.pending_region_count += 1; - chunk.setState(.meshing); - task = .{ - .key = key, - .chunk = chunk, - .mesh = mesh, - .lod_idx = lod_idx, - .job_token = chunk.job_token, - .source_revision = chunk.source_revision, - }; - break :find_failure; - } - } - self.mutex.unlock(); - - const recovery = task orelse break; - self.profiling.addCompactRecovery(); - self.fallbackCompactMeshToCpu(recovery.mesh, recovery.chunk) catch |err| { - log.log.warn("LOD{} compact draw recovery CPU build failed: {}", .{ recovery.lod_idx, err }); - self.mutex.lock(); - if (self.regions[recovery.lod_idx].get(recovery.key) == recovery.chunk and - recovery.chunk.job_token == recovery.job_token and - recovery.chunk.source_revision == recovery.source_revision and - recovery.chunk.getState() == .meshing) - { - // The compact range is already safely retired. Retry the - // regular CPU meshing path later; compact remains disabled for - // this source revision. - recovery.chunk.setState(.generated); - self.enqueueTransition(recovery.key, recovery.chunk, .mesh); - } - recovery.chunk.unpin(); - self.mutex.unlock(); - continue; - }; - - self.mutex.lock(); - if (self.regions[recovery.lod_idx].get(recovery.key) == recovery.chunk and - recovery.chunk.job_token == recovery.job_token and - recovery.chunk.source_revision == recovery.source_revision and - recovery.chunk.getState() == .meshing) - { - self.requeueUpload(recovery.lod_idx, recovery.chunk); - } - recovery.chunk.unpin(); - self.mutex.unlock(); - } -} - -/// Process GPU uploads (limited per frame) -pub fn processUploads(self: *Self) void { - self.processUploadsWithBudget(lodUploadBudgetBytes()); -} - -pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { - const UploadTask = struct { - key: LODRegionKey, - chunk: *LODChunk, - mesh: *LODMesh, - lod_idx: usize, - staging_bytes: usize, - }; - - self.mutex.lockShared(); - const max_uploads = self.config.getMaxUploadsPerFrame(); - self.mutex.unlockShared(); - - var uploads: u32 = 0; - var uploaded_bytes: usize = 0; - - while (uploads < max_uploads) { - const prep_timer = self.profiling.begin(); - var task: ?UploadTask = null; - var oversized_fallback: ?UploadTask = null; - var completed_without_upload = false; - var made_progress = false; - var deferred_for_budget = false; - - self.mutex.lock(); - const active_lod_count = lod_chunk.activeLODCount(self.config); - - // Inspect at most the queue depth observed for each level. An - // oversized far migration goes back at the tail, allowing a later - // near upload to use the remaining frame budget without looping on - // that deferred entry forever. - var order_idx: usize = 0; - while (order_idx < active_lod_count and task == null and !completed_without_upload) : (order_idx += 1) { - const i = lod_scheduler.priorityLevelIndex(order_idx, active_lod_count); - const attempts = self.upload_queues[i].count(); - var attempt: usize = 0; - while (attempt < attempts and task == null and !completed_without_upload) : (attempt += 1) if (self.upload_queues[i].pop()) |chunk| { - made_progress = true; - const key = chunk.key(); - if (self.meshes[i].get(key)) |mesh| { - const staging_bytes = self.gpu_bridge.uploadCost(mesh).total(); - if (wouldExceedUploadBudget(uploaded_bytes, staging_bytes, upload_budget_bytes)) { - self.profiling.addStagingPressure(); - deferred_for_budget = true; - // Preserve room for any smaller task later in the - // priority scan. If every queued task is oversized, - // admit one at the start of the frame so a pool - // migration cannot be deferred forever. - if (uploaded_bytes == 0 and oversized_fallback == null) { - oversized_fallback = .{ - .key = key, - .chunk = chunk, - .mesh = mesh, - .lod_idx = i, - .staging_bytes = staging_bytes, - }; - } else { - self.requeueUpload(i, chunk); - } - continue; - } - - chunk.pin(); - task = .{ - .key = key, - .chunk = chunk, - .mesh = mesh, - .lod_idx = i, - .staging_bytes = staging_bytes, - }; - } else { - self.markRegionRenderable(key, chunk); - uploads += 1; - completed_without_upload = true; - } - }; - } - if (oversized_fallback) |fallback| { - if (task == null and !completed_without_upload and uploaded_bytes == 0) { - fallback.chunk.pin(); - task = fallback; - } else { - self.requeueUpload(fallback.lod_idx, fallback.chunk); - } - } - self.mutex.unlock(); - - if (!made_progress) { - self.profiling.end(.upload_prep, prep_timer); - break; - } - if (completed_without_upload) { - self.profiling.end(.upload_prep, prep_timer); - continue; - } - - const upload_task = task orelse { - self.profiling.end(.upload_prep, prep_timer); - if (deferred_for_budget) break; - continue; - }; - self.profiling.end(.upload_prep, prep_timer); - const submission_timer = self.profiling.begin(); - self.gpu_bridge.upload(upload_task.mesh) catch |err| { - self.profiling.end(.upload_submission, submission_timer); - log.log.warn("LOD{} mesh upload failed (will retry): {}", .{ upload_task.lod_idx, err }); - // Compact allocation/update failures must not strand a far region - // in .mesh_ready. Rebuild its stable CPU heightfield while the - // upload task still pins the source, then put it straight back on - // the upload queue. This also covers LOD4. - if (upload_task.mesh.isCompact()) { - self.profiling.addCompactUploadFailure(); - self.fallbackCompactMeshToCpu(upload_task.mesh, upload_task.chunk) catch |fallback_err| { - log.log.warn("LOD{} compact fallback build failed; retaining retryable compact payload: {}", .{ upload_task.lod_idx, fallback_err }); - self.mutex.lock(); - self.stats.upload_failures += 1; - uploads += 1; - self.requeueUpload(upload_task.lod_idx, upload_task.chunk); - upload_task.chunk.unpin(); - self.mutex.unlock(); - return; - }; - self.mutex.lock(); - self.stats.upload_failures += 1; - uploads += 1; - self.requeueUpload(upload_task.lod_idx, upload_task.chunk); - upload_task.chunk.unpin(); - self.mutex.unlock(); - continue; - } - self.mutex.lock(); - self.stats.upload_failures += 1; - uploads += 1; - if (isUploadPressureError(err)) { - self.profiling.addStagingPressure(); - self.requeueUpload(upload_task.lod_idx, upload_task.chunk); - upload_task.chunk.unpin(); - self.mutex.unlock(); - return; - } - upload_task.chunk.setState(.mesh_ready); - upload_task.chunk.unpin(); - self.mutex.unlock(); - continue; - }; - self.profiling.end(.upload_submission, submission_timer); - - uploaded_bytes = std.math.add(usize, uploaded_bytes, upload_task.staging_bytes) catch std.math.maxInt(usize); - self.profiling.addUploadBytes(upload_task.staging_bytes); - // Count only ownership that reached the GPU bridge successfully. A - // requeued failure arrives here once on its eventual successful upload, - // never once per retry. LOD3/LOD4 are the compact far-path boundary; - // near pooled uploads remain intentionally outside this comparison. - if (upload_task.lod_idx >= @intFromEnum(LODLevel.lod3)) { - if (upload_task.mesh.isCompact()) { - self.profiling.addCompactUploadBytes(upload_task.staging_bytes); - } else { - self.profiling.addFarExpandedUploadBytes(upload_task.staging_bytes); - } - } - self.mutex.lock(); - self.markRegionRenderable(upload_task.key, upload_task.chunk); - uploads += 1; - upload_task.chunk.unpin(); - self.mutex.unlock(); - } -} - -pub fn requeueUpload(self: *Self, lod_idx: usize, chunk: *LODChunk) void { - chunk.setState(.uploading); - self.upload_queues[lod_idx].push(chunk) catch |err| { - log.log.warn("LOD{} upload requeue failed: {}", .{ lod_idx, err }); - self.stats.upload_failures += 1; - chunk.setState(.mesh_ready); - }; -} - -pub fn countRenderableChildren(self: *Self, key: LODRegionKey) u8 { - const children = key.childKeys() orelse return 0; - const child_idx = @intFromEnum(children[0].lod); - var count: u8 = 0; - for (children) |child_key| { - const child = self.regions[child_idx].get(child_key) orelse continue; - if (self.regionContributesGeometry(child_key, child)) count += 1; - } - return count; -} - -pub fn regionContributesGeometry(self: *Self, key: LODRegionKey, chunk: *const LODChunk) bool { - if (chunk.getState() != .renderable) return false; - const mesh = self.meshes[@intFromEnum(key.lod)].get(key) orelse return false; - return mesh.isRenderable(); -} - -pub fn adjustParentReadyChildren(self: *Self, key: LODRegionKey, delta: i8) void { - const parent = key.parentKey() orelse return; - const parent_chunk = self.regions[@intFromEnum(parent.lod)].get(parent) orelse return; - parent_chunk.adjustReadyChildren(delta); - self.enqueueFade(parent, parent_chunk); -} - -pub fn markRegionRenderable(self: *Self, key: LODRegionKey, chunk: *LODChunk) void { - if (chunk.isRenderable()) return; - chunk.markRenderable(self.countRenderableChildren(key)); - if (engine_core.envFlag("ZIGCRAFT_LOD_DIAG", false)) { - log.log.warn("LOD_REGION_RENDERABLE: lod={} region=({}, {})", .{ @intFromEnum(key.lod), key.rx, key.rz }); - } - self.enqueueFade(key, chunk); - if (self.pending_region_count > 0) self.pending_region_count -= 1; - if (self.regionContributesGeometry(key, chunk)) { - self.adjustParentReadyChildren(key, 1); - } -} - -pub fn decayTransitionFrames(self: *Self) void { - self.mutex.lock(); - defer self.mutex.unlock(); - var processed: usize = 0; - while (processed < MAX_LIFECYCLE_TRANSITIONS_PER_UPDATE) : (processed += 1) { - const token = self.fade_tokens.pop() orelse break; - if (token.stage != .fade) continue; - const chunk = self.regions[@intFromEnum(token.key.lod)].get(token.key) orelse continue; - if (!token.matches(chunk)) continue; - chunk.tickTransition(); - if (chunk.transition_frames_remaining > 0) { - _ = self.fade_tokens.push(self.allocator, token) catch false; - } - } -} - -pub fn noteRegionRemoved(self: *Self, key: LODRegionKey, chunk: *const LODChunk) void { - if (self.regionContributesGeometry(key, chunk)) { - self.adjustParentReadyChildren(key, -1); - } -} - -pub fn demoteRegionForRemesh(self: *Self, key: LODRegionKey, chunk: *LODChunk) void { - if (chunk.getState() == .renderable) { - self.noteRegionRemoved(key, chunk); - // Source revisions invalidate any queued worker/transition token before - // the edited data is allowed to publish a replacement mesh. - chunk.job_token +%= 1; - chunk.setState(.generated); - self.pending_region_count += 1; - self.enqueueTransition(key, chunk, .mesh); - } else if (chunk.getState() == .mesh_ready or chunk.getState() == .generated) { - // A queued transition captured the pre-edit source revision. Invalidate - // it and publish a mesh transition for the authoritative edited data. - chunk.job_token +%= 1; - chunk.setState(.generated); - self.enqueueTransition(key, chunk, .mesh); - } -} diff --git a/modules/world-lod/src/lod_materials.zig b/modules/world-lod/src/lod_materials.zig deleted file mode 100644 index 96b64158..00000000 --- a/modules/world-lod/src/lod_materials.zig +++ /dev/null @@ -1,23 +0,0 @@ -//! Material, tint, and color-selection helpers for LOD mesh generation. - -const geom = @import("lod_geometry.zig"); - -pub const LODTextureFace = geom.LODTextureFace; - -pub const ambientOcclusionForLOD = geom.ambientOcclusionForLOD; -pub const applyColorBrightness = geom.applyColorBrightness; -pub const applyTextureLuminance = geom.applyTextureLuminance; -pub const averageColor = geom.averageColor; -pub const blockForLODQuad = geom.blockForLODQuad; -pub const cellColorForLOD = geom.cellColorForLOD; -pub const getLodSideTile = geom.getLodSideTile; -pub const getLodTopColor = geom.getLodTopColor; -pub const getLodTopTile = geom.getLodTopTile; -pub const isLeafBlock = geom.isLeafBlock; -pub const packBlockDefaultColor = geom.packBlockDefaultColor; -pub const selectCellMaterial = geom.selectCellMaterial; -pub const terrainBlockForLODQuadForLOD = geom.terrainBlockForLODQuadForLOD; -pub const tintColorForLodFace = geom.tintColorForLodFace; -pub const unpackB = geom.unpackB; -pub const unpackG = geom.unpackG; -pub const unpackR = geom.unpackR; diff --git a/modules/world-lod/src/lod_mesh.zig b/modules/world-lod/src/lod_mesh.zig deleted file mode 100644 index 7e85bcd8..00000000 --- a/modules/world-lod/src/lod_mesh.zig +++ /dev/null @@ -1,1224 +0,0 @@ -//! LOD Mesh generation for distant terrain rendering. -//! -//! LOD meshes are simplified versions of chunk meshes. Region size, grid -//! detail, and span-vs-heightfield behavior are selected by runtime settings. -//! -//! Key simplifications: -//! - No greedy meshing (simple quads per grid cell) -//! - No lighting calculations -//! - Fluid vertices are split into a separate water range for WaterPass -//! - Biome colors averaged per cell - -const std = @import("std"); -const sync = @import("sync"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; -const world_core = @import("world-core"); -const BiomeId = world_core.BiomeId; -const biome_mod = @import("biome_color_provider.zig"); -const BlockType = world_core.BlockType; -const TextureAtlas = @import("engine-assets").TextureAtlas; -const rhi_types = @import("engine-rhi"); -const Vertex = rhi_types.Vertex; -const BufferHandle = rhi_types.BufferHandle; -const RhiError = rhi_types.RhiError; -const QuadricSimplifier = @import("world-meshing").meshing.quadric_simplifier.QuadricSimplifier; -const log = @import("engine-core").log; - -/// Chunk-derived and edited source columns can contain cave and overhang spans -/// surrounded by worldgen-only samples. Rendering those partial underground -/// intervals at a streaming boundary exposes a giant terrain cross-section. -/// Their authoritative surface height remains safe for the heightfield path. -pub fn canBuildColumnSpans(data: *const LODSimplifiedData) bool { - return data.hasVerticalSpans() and !data.hasNonWorldgenColumns(); -} -const lod_seam = @import("lod_seam.zig"); -const resources_mod = @import("lod_mesh_resources.zig"); -const geom = @import("lod_geometry.zig"); -const CompactLODTile = @import("lod_tile.zig").CompactLODTile; -const CompactTileEdge = @import("lod_tile.zig").TileEdge; - -pub const EdgeDir = lod_seam.EdgeDir; -pub const SeamConfig = lod_seam.SeamConfig; -pub const stitchEdge = lod_seam.stitchEdge; -pub const LODMeshResources = resources_mod.LODMeshResources; -pub const LODMeshRenderContext = resources_mod.LODMeshRenderContext; -pub const MAX_STAGING_UPDATE_BYTES = resources_mod.MAX_STAGING_UPDATE_BYTES; -pub const updateBufferChunked = resources_mod.updateBufferChunked; -pub const uploadBufferChunked = resources_mod.uploadBufferChunked; -const LODRenderLayer = @import("lod_upload_queue.zig").LODRenderLayer; - -const FullDetailMesh = geom.FullDetailMesh; -const buildFullDetailHeightmapMesh = geom.buildFullDetailHeightmapMesh; -const addTopFaceQuad = geom.addTopFaceQuad; -const addBottomFaceQuad = geom.addBottomFaceQuad; -const addSideFaceQuad = geom.addSideFaceQuad; -const addSteppedHeightfieldSides = geom.addSteppedHeightfieldSides; -const addExposedSpanFaces = geom.addExposedSpanFaces; -const addTreeColumn = geom.addTreeColumn; -const addTreeCanopyColumn = geom.addTreeCanopyColumn; -const averageColor = geom.averageColor; -const ambientOcclusionForLOD = geom.ambientOcclusionForLOD; -const applyColorBrightness = geom.applyColorBrightness; -const applyTextureLuminance = geom.applyTextureLuminance; -const blockForLODQuad = geom.blockForLODQuad; -const cellColorForLOD = geom.cellColorForLOD; -const collectColumnSpans = geom.collectColumnSpans; -const foldedCanopyColumnForLOD = geom.foldedCanopyColumnForLOD; -const getLodSideTile = geom.getLodSideTile; -const getLodTopColor = geom.getLodTopColor; -const getLodTopTile = geom.getLodTopTile; -const isLeafBlock = geom.isLeafBlock; -const isLODWaterCellForLOD = geom.isLODWaterCellForLOD; -const LODColumnSpan = geom.LODColumnSpan; -const LOD_TREE_COVERAGE_THRESHOLD = geom.LOD_TREE_COVERAGE_THRESHOLD; -const packBlockDefaultColor = geom.packBlockDefaultColor; -const quantizedCellVisualTerrainHeightForLOD = geom.quantizedCellVisualTerrainHeightForLOD; -const quantizedWaterSurfaceHeightForCell = geom.quantizedWaterSurfaceHeightForCell; -const quantizedWaterSurfaceHeightForSpan = geom.quantizedWaterSurfaceHeightForSpan; -const representativeVegetationForLOD = geom.representativeVegetationForLOD; -const selectCellMaterial = geom.selectCellMaterial; -const shouldRenderLODTree = geom.shouldRenderLODTree; -const terrainBlockForLODQuadForLOD = geom.terrainBlockForLODQuadForLOD; -const tintColorForLodFace = geom.tintColorForLodFace; -const unpackB = geom.unpackB; -const unpackG = geom.unpackG; -const unpackR = geom.unpackR; - -/// Size of each LOD mesh grid cell in blocks -pub fn getCellSize(lod: LODLevel) u32 { - return LODSimplifiedData.getCellSizeBlocks(lod); -} - -/// LOD Mesh for a single LOD region -pub const LODMesh = struct { - /// `drawCompactLOD` currently reports a bool, so transient render-graph - /// readiness and a rejected backend submission share one result. Retrying - /// several frames preserves compact residency during pipeline/descriptor - /// warmup while still bounding a genuine persistent failure. - pub const COMPACT_BACKEND_FAILURE_LIMIT: u8 = 8; - pub const DrawRange = struct { - offset: usize, - count: u32, - }; - - pub const DrawState = struct { - buffer_handle: BufferHandle = 0, - vertex_offset: usize = 0, - vertex_count: u32 = 0, - capacity: u32 = 0, - pooled: bool = false, - ready: bool = false, - - pub const empty: DrawState = .{}; - }; - - /// CPU geometry prepared for an expanded upload. Keeping this payload - /// separately movable lets a compact-to-CPU transition retain the compact - /// representation until CPU generation has definitely succeeded. - pub const PendingCpuBuild = struct { - vertices: ?[]Vertex = null, - opaque_vertex_count: u32 = 0, - water_vertex_offset: usize = 0, - water_vertex_count: u32 = 0, - }; - - pub const MemorySnapshot = struct { - capacity_bytes: usize, - pending_upload_bytes: usize, - pooled: bool, - compact: bool, - vertex_count: u32, - }; - - /// GPU buffer handle - buffer_handle: BufferHandle = 0, - /// Number of vertices - vertex_count: u32 = 0, - /// Number of opaque terrain vertices at the start of the buffer. - opaque_vertex_count: u32 = 0, - /// Byte offset from `vertex_offset` to translucent LOD water vertices. - water_vertex_offset: usize = 0, - /// Number of translucent LOD water vertices. - water_vertex_count: u32 = 0, - /// Buffer capacity (vertices) - capacity: u32 = 0, - /// Byte offset inside the vertex buffer. Non-zero when backed by a shared LOD pool. - vertex_offset: usize = 0, - /// True when buffer_handle is owned by a shared LOD vertex pool. - pooled: bool = false, - /// Pending vertices to upload - pending_vertices: ?[]Vertex = null, - /// Present only while a compact tile awaits GPU upload. The pool owns the - /// post-upload representation, so this is never an expanded Vertex array. - compact_tile: ?CompactLODTile = null, - /// Same-level authoritative compact aprons. This metadata survives release - /// of `compact_tile` after upload, so a resident tile never claims a - /// seamless edge merely because its fallback apron happened to be local. - compact_neighbor_apron_mask: u8 = 0, - compact: bool = false, - compact_sample_offset: u32 = 0, - compact_sample_bytes: usize = 0, - compact_index_count: u32 = 0, - compact_tile_width: u32 = 0, - compact_has_water: bool = false, - compact_draw_failed: bool = false, - compact_backend_draw_failures: u8 = 0, - /// Immutable source identity captured when this representation was built. - /// Recovery uses it to reject a late render failure from a superseded mesh. - source_job_token: u32 = 0, - source_revision: u32 = 0, - /// Allocator - allocator: std.mem.Allocator, - /// Mutex for thread safety - mutex: sync.Mutex = .{}, - /// LOD level - lod_level: LODLevel, - /// Ready for rendering - ready: bool = false, - - pub fn init(allocator: std.mem.Allocator, lod: LODLevel) LODMesh { - return .{ - .allocator = allocator, - .lod_level = lod, - }; - } - - pub fn isRenderable(self: *const LODMesh) bool { - return self.ready and self.vertex_count > 0; - } - - pub fn isReady(self: *const LODMesh) bool { - return self.ready; - } - - pub fn isPooled(self: *const LODMesh) bool { - return self.pooled; - } - /// Representation selection is serialized by the LOD manager/renderer. - /// Code outside that invariant must hold `mutex` before reading `compact`. - pub fn isCompact(self: *const LODMesh) bool { - return self.compact; - } - - pub fn compactDrawFailed(self: *LODMesh) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.compact_draw_failed; - } - - pub fn markCompactDrawFailed(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.compact_draw_failed = true; - self.ready = false; - } - - /// A backend failure is only terminal after a bounded retry budget. - /// Resource/descriptor availability is handled separately by the renderer - /// and never calls this method. - pub fn noteCompactBackendDrawFailure(self: *LODMesh) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - self.compact_backend_draw_failures +|= 1; - if (self.compact_backend_draw_failures < COMPACT_BACKEND_FAILURE_LIMIT) return false; - self.compact_draw_failed = true; - self.ready = false; - return true; - } - - /// A confirmed backend submission makes earlier failures irrelevant: only - /// consecutive failures may retire an otherwise healthy compact mesh. - /// Transient resource unavailability intentionally does not call this. - pub fn resetCompactBackendDrawFailures(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.compact_backend_draw_failures = 0; - } - - /// A manager-owned mesh build records the source identity before it can be - /// rendered. The render thread only marks failure; it never owns recovery. - pub fn setSourceIdentity(self: *LODMesh, job_token: u32, source_revision: u32) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.source_job_token = job_token; - self.source_revision = source_revision; - } - - pub fn compactDrawFailureMatches(self: *LODMesh, job_token: u32, source_revision: u32) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.compact_draw_failed and self.compact and - self.source_job_token == job_token and self.source_revision == source_revision; - } - - /// A bit is set only when the corresponding same-level apron was copied - /// before upload. Missing and cross-LOD neighbors intentionally remain - /// invalid; the shader uses the complementary skirt mask for those edges. - pub fn compactApronMask(self: *LODMesh) u8 { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.compact_neighbor_apron_mask; - } - - pub fn compactSkirtMask(self: *LODMesh) u8 { - self.mutex.lock(); - defer self.mutex.unlock(); - return (~self.compact_neighbor_apron_mask) & @import("lod_tile.zig").TILE_EDGE_MASK; - } - - pub fn compactEdgeIsSeamless(self: *LODMesh, edge: CompactTileEdge) bool { - return (self.compactApronMask() & @import("lod_tile.zig").edgeMask(edge)) != 0; - } - - pub fn patchCompactNeighbor(self: *LODMesh, edge: CompactTileEdge, neighbor: *LODMesh) bool { - if (self == neighbor) return false; - const self_first = @intFromPtr(self) < @intFromPtr(neighbor); - const first = if (self_first) self else neighbor; - const second = if (self_first) neighbor else self; - first.mutex.lock(); - defer first.mutex.unlock(); - second.mutex.lock(); - defer second.mutex.unlock(); - const tile = if (self.compact_tile) |*value| value else return false; - const neighbor_tile = if (neighbor.compact_tile) |*value| value else return false; - tile.applyNeighborApron(edge, neighbor_tile) catch return false; - const opposite: CompactTileEdge = switch (edge) { - .west => .east, - .east => .west, - .north => .south, - .south => .north, - }; - neighbor_tile.applyNeighborApron(opposite, tile) catch return false; - self.compact_neighbor_apron_mask = tile.neighbor_apron_mask; - neighbor.compact_neighbor_apron_mask = neighbor_tile.neighbor_apron_mask; - return true; - } - - /// True when replacing this mesh must first retire renderer-owned storage. - /// A remesh may replace either an uploaded representation or source data - /// awaiting upload; both cases must discard the old representation first. - pub fn hasRepresentation(self: *LODMesh) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.compact or self.buffer_handle != 0 or self.pooled or - self.pending_vertices != null or self.capacity != 0 or self.ready; - } - - pub fn bufferHandle(self: *const LODMesh) BufferHandle { - return self.buffer_handle; - } - - pub fn vertexOffset(self: *const LODMesh) usize { - return self.vertex_offset; - } - - pub fn vertexCount(self: *const LODMesh) u32 { - return self.vertex_count; - } - - pub fn lodLevel(self: *const LODMesh) LODLevel { - return self.lod_level; - } - - pub fn byteSize(self: *const LODMesh) usize { - return @as(usize, self.capacity) * @sizeOf(Vertex); - } - - pub fn drawRange(self: *const LODMesh, layer: LODRenderLayer) ?DrawRange { - if (!self.ready) return null; - if (self.compact) return switch (layer) { - .terrain => if (self.compact_index_count > 0) .{ .offset = 0, .count = self.compact_index_count } else null, - .fluid => if (self.compact_has_water and self.compact_index_count > 0) .{ .offset = 0, .count = self.compact_index_count } else null, - }; - if (self.buffer_handle == 0) return null; - return switch (layer) { - .terrain => if (self.opaque_vertex_count > 0) - .{ .offset = 0, .count = self.opaque_vertex_count } - else if (self.water_vertex_count == 0 and self.vertex_count > 0) - .{ .offset = 0, .count = self.vertex_count } - else - null, - .fluid => if (self.water_vertex_count > 0) - .{ .offset = self.water_vertex_offset, .count = self.water_vertex_count } - else - null, - }; - } - - pub fn firstVertex(self: *const LODMesh, range: DrawRange) u32 { - return @intCast((self.vertex_offset + range.offset) / @sizeOf(Vertex)); - } - - pub fn setDrawStateUnlocked(self: *LODMesh, state: DrawState) void { - self.buffer_handle = state.buffer_handle; - self.vertex_offset = state.vertex_offset; - self.vertex_count = state.vertex_count; - self.capacity = state.capacity; - self.pooled = state.pooled; - self.ready = state.ready; - } - - pub fn setDrawState(self: *LODMesh, state: DrawState) void { - self.mutex.lock(); - defer self.mutex.unlock(); - self.setDrawStateUnlocked(state); - } - - pub fn clearDrawStateUnlocked(self: *LODMesh) void { - self.setDrawStateUnlocked(.empty); - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - } - - pub fn markEmptyUploadedUnlocked(self: *LODMesh) void { - self.clearDrawStateUnlocked(); - self.ready = true; - } - - pub fn setBufferHandleUnlocked(self: *LODMesh, handle: BufferHandle) void { - self.buffer_handle = handle; - } - - pub fn setPoolLocationUnlocked(self: *LODMesh, handle: BufferHandle, offset: usize) void { - self.buffer_handle = handle; - self.vertex_offset = offset; - } - - pub fn setUploaded(self: *LODMesh, vertex_count: u32, opaque_vertex_count: u32) void { - self.vertex_count = vertex_count; - self.opaque_vertex_count = opaque_vertex_count; - self.ready = true; - } - - pub fn pendingVerticesForTest(self: *const LODMesh) ?[]Vertex { - return self.pending_vertices; - } - - /// Detaches a successfully built expanded payload before compact GPU - /// storage is retired. `LODGPUBridge.destroy` deliberately clears pending - /// vertices, so the caller must carry this payload across that callback. - pub fn takePendingCpuBuild(self: *LODMesh) PendingCpuBuild { - self.mutex.lock(); - defer self.mutex.unlock(); - const result = PendingCpuBuild{ - .vertices = self.pending_vertices, - .opaque_vertex_count = self.opaque_vertex_count, - .water_vertex_offset = self.water_vertex_offset, - .water_vertex_count = self.water_vertex_count, - }; - self.pending_vertices = null; - return result; - } - - /// Publishes a detached expanded payload after the former compact storage - /// has been retired. This cannot allocate, so a successful CPU build never - /// becomes an empty mesh during retirement. - pub fn restorePendingCpuBuild(self: *LODMesh, build: *PendingCpuBuild) void { - self.mutex.lock(); - defer self.mutex.unlock(); - std.debug.assert(!self.compact); - std.debug.assert(self.pending_vertices == null); - self.opaque_vertex_count = build.opaque_vertex_count; - self.water_vertex_offset = build.water_vertex_offset; - self.water_vertex_count = build.water_vertex_count; - self.pending_vertices = build.vertices; - build.* = .{}; - } - - pub fn deinit(self: *LODMesh, resources: LODMeshResources) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pooled) { - std.debug.assert(false); - return; - } - - if (self.buffer_handle != 0 and !self.pooled) { - resources.destroyBuffer(self.buffer_handle); - } - self.buffer_handle = 0; - self.vertex_offset = 0; - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - self.pooled = false; - if (self.pending_vertices) |p| { - self.allocator.free(p); - self.pending_vertices = null; - } - if (self.compact_tile) |*tile| tile.deinit(); - self.compact_tile = null; - self.compact_neighbor_apron_mask = 0; - self.compact = false; - self.compact_sample_offset = 0; - self.compact_sample_bytes = 0; - self.compact_index_count = 0; - self.compact_tile_width = 0; - self.compact_has_water = false; - self.compact_draw_failed = false; - self.compact_backend_draw_failures = 0; - self.ready = false; - } - - pub fn buildCompactTile(self: *LODMesh, data: *const LODSimplifiedData) !void { - const tile = try CompactLODTile.initFromSimplified(self.allocator, self.lod_level, data); - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.compact_tile) |*old| old.deinit(); - self.compact_tile = tile; - self.compact_neighbor_apron_mask = 0; - self.compact = true; - const cells = (data.width - 1) * (data.width - 1); - self.compact_index_count = cells * 6; - self.compact_tile_width = data.width; - self.compact_has_water = false; - for (data.water) |water| { - if (water.is_surface and water.coverage > 0.001) { - self.compact_has_water = true; - break; - } - } - self.compact_draw_failed = false; - self.compact_backend_draw_failures = 0; - self.ready = false; - } - - /// Releases a compact source tile once its samples have been uploaded. - /// `compact_neighbor_apron_mask` deliberately remains: resident data has no - /// mutable CPU payload, so a later neighbor must remain a skirted, - /// non-seamless edge rather than triggering an unsafe in-place GPU patch. - pub fn releasePendingCompactTile(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.compact_tile) |*tile| tile.deinit(); - self.compact_tile = null; - } - - /// Clears compact-only state after its range has been retired by the - /// renderer, or when it never reached the renderer. This is deliberately - /// separate from `releasePendingCompactTile`: a CPU fallback must not leave - /// `compact` set, otherwise the normal vertex uploader will re-enter the - /// compact path with stale offsets. - pub fn clearCompactState(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.compact_tile) |*tile| tile.deinit(); - self.compact_tile = null; - self.compact_neighbor_apron_mask = 0; - self.compact = false; - self.compact_sample_offset = 0; - self.compact_sample_bytes = 0; - self.compact_index_count = 0; - self.compact_tile_width = 0; - self.compact_has_water = false; - self.compact_draw_failed = false; - self.compact_backend_draw_failures = 0; - self.buffer_handle = 0; - self.vertex_count = 0; - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - self.vertex_offset = 0; - self.capacity = 0; - self.pooled = false; - self.ready = false; - } - - /// Idempotent cleanup after the renderer has retired/destroyed the GPU - /// object. This also makes lightweight test bridges safe: they need not - /// duplicate representation bookkeeping merely to exercise a remesh. - pub fn clearRetiredState(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.pending_vertices) |pending| self.allocator.free(pending); - self.pending_vertices = null; - if (self.compact_tile) |*tile| tile.deinit(); - self.compact_tile = null; - self.compact_neighbor_apron_mask = 0; - self.clearDrawStateUnlocked(); - self.compact = false; - self.compact_sample_offset = 0; - self.compact_sample_bytes = 0; - self.compact_index_count = 0; - self.compact_tile_width = 0; - self.compact_has_water = false; - self.compact_draw_failed = false; - self.compact_backend_draw_failures = 0; - } - - pub fn clearPendingVertices(self: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pending_vertices) |p| { - self.allocator.free(p); - self.pending_vertices = null; - } - } - - pub fn pendingUploadBytes(self: *LODMesh) usize { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.compact_tile) |tile| return std.mem.sliceAsBytes(tile.samples).len; - const pending = self.pending_vertices orelse return 0; - return std.mem.sliceAsBytes(pending).len; - } - - /// Atomically snapshots the storage that this mesh currently owns or uses. - /// Pooled capacity is an allocation within the renderer-owned pool, not a - /// separate GPU buffer. - pub fn memorySnapshot(self: *LODMesh) MemorySnapshot { - self.mutex.lock(); - defer self.mutex.unlock(); - return .{ - .capacity_bytes = self.byteSize(), - .pending_upload_bytes = if (self.compact_tile) |tile| - std.mem.sliceAsBytes(tile.samples).len - else if (self.pending_vertices) |pending| - std.mem.sliceAsBytes(pending).len - else - 0, - .pooled = self.pooled, - .compact = self.compact, - .vertex_count = self.vertex_count, - }; - } - - /// Build mesh from simplified LOD data (heightmap-based) - pub fn buildFromSimplifiedData(self: *LODMesh, data: *const LODSimplifiedData, world_x: i32, world_z: i32, atlas: *const TextureAtlas) !void { - if (data.width < 2) return error.EmptyData; - - const region_size: f32 = @floatFromInt(lod_chunk.regionSizeBlocks(self.lod_level)); - const cell_size = region_size / @as(f32, @floatFromInt(data.width - 1)); - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(self.allocator); - var water_vertices = std.ArrayListUnmanaged(Vertex).empty; - defer water_vertices.deinit(self.allocator); - - var gz: u32 = 0; - while (gz + 1 < data.width) : (gz += 1) { - var gx: u32 = 0; - while (gx + 1 < data.width) : (gx += 1) { - const cell_color = cellColorForLOD(data, gx, gz, self.lod_level); - const lit_cell_color = applyColorBrightness(cell_color, ambientOcclusionForLOD(data, gx, gz, self.lod_level)); - const wx = @as(f32, @floatFromInt(gx)) * cell_size; - const wz = @as(f32, @floatFromInt(gz)) * cell_size; - const size = cell_size; - - const is_water_cell = isLODWaterCellForLOD(data, gx, gz, self.lod_level); - const base_block = terrainBlockForLODQuadForLOD(data, gx, gz, is_water_cell, self.lod_level); - const base_height = quantizedCellVisualTerrainHeightForLOD(data, gx, gz, self.lod_level, is_water_cell); - const folded_canopy = foldedCanopyColumnForLOD(data, gx, gz, self.lod_level, base_height, base_block, is_water_cell); - const top_block = if (folded_canopy) |folded| folded.block else base_block; - const column_height = if (folded_canopy) |folded| folded.height else base_height; - const top_tile = if (folded_canopy != null) Vertex.LOD_TILE_ID else getLodTopTile(top_block, atlas); - const side_tile = if (folded_canopy != null) Vertex.LOD_TILE_ID else getLodSideTile(top_block, atlas); - const base_top_color = if (folded_canopy) |folded| - applyColorBrightness(folded.color, ambientOcclusionForLOD(data, gx, gz, self.lod_level)) - else - getLodTopColor(top_block, top_tile, lit_cell_color); - const top_color = applyTextureLuminance(tintColorForLodFace(data, gx, gz, self.lod_level, top_block, .top, base_top_color), top_block, .top, atlas); - const side_color = applyTextureLuminance(tintColorForLodFace(data, gx, gz, self.lod_level, top_block, .side, base_top_color), top_block, .side, atlas); - - try addTopFaceQuad(self.allocator, &vertices, wx, column_height, wz, size, unpackR(top_color), unpackG(top_color), unpackB(top_color), top_tile, world_x, world_z); - try addSteppedHeightfieldSides(self.allocator, &vertices, data, gx, gz, self.lod_level, wx, wz, size, column_height, side_color, side_tile, world_x, world_z); - - if (is_water_cell) { - const water_height = quantizedWaterSurfaceHeightForCell(data, gx, gz, self.lod_level); - if (water_height > column_height + 0.01) { - const water_tile = getLodTopTile(.water, atlas); - const water_color = tintColorForLodFace(data, gx, gz, self.lod_level, .water, .top, packBlockDefaultColor(.water, 0x3366CC)); - try addTopFaceQuad(self.allocator, &water_vertices, wx, water_height, wz, size, unpackR(water_color), unpackG(water_color), unpackB(water_color), water_tile, world_x, world_z); - } - } - - if (folded_canopy == null and !is_water_cell and shouldRenderLODTree(top_block)) { - const vegetation = representativeVegetationForLOD(data, gx, gz, self.lod_level); - if (vegetation.tree_coverage >= LOD_TREE_COVERAGE_THRESHOLD) { - try addTreeColumn(self.allocator, &vertices, data, gx, gz, self.lod_level, wx, wz, size, column_height, vegetation, atlas, world_x, world_z); - } - } - } - } - - const opaque_count = vertices.items.len; - const water_count = water_vertices.items.len; - const total_count = opaque_count + water_count; - var pending: ?[]Vertex = null; - if (total_count > 0) { - const allocated = try self.allocator.alloc(Vertex, total_count); - pending = allocated; - errdefer self.allocator.free(allocated); - @memcpy(allocated[0..opaque_count], vertices.items); - @memcpy(allocated[opaque_count..total_count], water_vertices.items); - } - - // Allocate and populate before touching the live mesh. A failed CPU - // fallback must leave its compact representation exactly retryable. - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.pending_vertices) |old| self.allocator.free(old); - self.opaque_vertex_count = @intCast(opaque_count); - self.water_vertex_offset = opaque_count * @sizeOf(Vertex); - self.water_vertex_count = @intCast(water_count); - self.pending_vertices = pending; - } - - /// Build mesh from rich LOD column/span data, falling back to the stable heightfield path - /// when spans are not available. This is intentionally exposed as a test/config hook. - pub fn buildFromColumnSpans(self: *LODMesh, data: *const LODSimplifiedData, world_x: i32, world_z: i32, atlas: *const TextureAtlas) !void { - if (data.width < 2) return error.EmptyData; - if (!canBuildColumnSpans(data)) return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - - const region_size: f32 = @floatFromInt(lod_chunk.regionSizeBlocks(self.lod_level)); - const cell_size = region_size / @as(f32, @floatFromInt(data.width - 1)); - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(self.allocator); - var water_vertices = std.ArrayListUnmanaged(Vertex).empty; - defer water_vertices.deinit(self.allocator); - - var found_span = false; - var gz: u32 = 0; - while (gz + 1 < data.width) : (gz += 1) { - var gx: u32 = 0; - while (gx + 1 < data.width) : (gx += 1) { - var spans_buf: [world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan = undefined; - const span_count = collectColumnSpans(data, gx, gz, self.lod_level, &spans_buf); - if (span_count == 0) continue; - found_span = true; - - const wx = @as(f32, @floatFromInt(gx)) * cell_size; - const wz = @as(f32, @floatFromInt(gz)) * cell_size; - - var span_index: usize = 0; - while (span_index < span_count) : (span_index += 1) { - const span = spans_buf[span_index]; - const top_tile = getLodTopTile(span.block, atlas); - const side_tile = getLodSideTile(span.block, atlas); - const span_color = applyColorBrightness(span.color, span.ambient_occlusion); - const lit_color = if (span.block == .water) - tintColorForLodFace(data, gx, gz, self.lod_level, .water, .side, span_color) - else - applyTextureLuminance(tintColorForLodFace(data, gx, gz, self.lod_level, span.block, .side, span_color), span.block, .side, atlas); - const top_color = if (span.block == .water) - tintColorForLodFace(data, gx, gz, self.lod_level, .water, .top, packBlockDefaultColor(.water, 0x3366CC)) - else - getLodTopColor(span.block, top_tile, applyTextureLuminance(tintColorForLodFace(data, gx, gz, self.lod_level, span.block, .top, span_color), span.block, .top, atlas)); - - if (span.block == .water) { - const water_height = quantizedWaterSurfaceHeightForSpan(data, gx, gz, self.lod_level, span.max_height); - try addTopFaceQuad(self.allocator, &water_vertices, wx, water_height, wz, cell_size, unpackR(top_color), unpackG(top_color), unpackB(top_color), top_tile, world_x, world_z); - continue; - } - - if (isLeafBlock(span.block)) { - const tree_is_water_cell = isLODWaterCellForLOD(data, gx, gz, self.lod_level); - if (tree_is_water_cell) continue; - var vegetation = representativeVegetationForLOD(data, gx, gz, self.lod_level); - if (vegetation.leaves == .air) vegetation.leaves = span.block; - if (vegetation.tree_coverage < LOD_TREE_COVERAGE_THRESHOLD) vegetation.tree_coverage = LOD_TREE_COVERAGE_THRESHOLD; - if (vegetation.avg_tree_height < 2.0) vegetation.avg_tree_height = @max(2.0, span.max_height - span.min_height); - const tree_base_height = quantizedCellVisualTerrainHeightForLOD(data, gx, gz, self.lod_level, tree_is_water_cell); - try addTreeCanopyColumn(self.allocator, &vertices, data, gx, gz, self.lod_level, wx, wz, cell_size, tree_base_height, span.min_height, span.max_height, vegetation, atlas, world_x, world_z); - continue; - } - - try addTopFaceQuad(self.allocator, &vertices, wx, span.max_height, wz, cell_size, unpackR(top_color), unpackG(top_color), unpackB(top_color), top_tile, world_x, world_z); - try addExposedSpanFaces(self.allocator, &vertices, data, gx, gz, self.lod_level, span, wx, wz, cell_size, lit_color, side_tile, world_x, world_z); - - // Floating span (overhang): there is open air below this - // span's floor, so add a downward-facing bottom quad. - const supported_from_below = if (span_index == 0) - span.min_height <= 0.01 - else - spans_buf[span_index - 1].max_height >= span.min_height - 0.01; - if (!supported_from_below) { - try addBottomFaceQuad(self.allocator, &vertices, wx, span.min_height, wz, cell_size, unpackR(lit_color) * 0.5, unpackG(lit_color) * 0.5, unpackB(lit_color) * 0.5, side_tile, world_x, world_z); - } - } - } - } - - if (!found_span) return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pending_vertices) |p| { - self.allocator.free(p); - } - - const opaque_count = vertices.items.len; - const water_count = water_vertices.items.len; - const total_count = opaque_count + water_count; - self.opaque_vertex_count = @intCast(opaque_count); - self.water_vertex_offset = opaque_count * @sizeOf(Vertex); - self.water_vertex_count = @intCast(water_count); - - if (total_count > 0) { - const pending = try self.allocator.alloc(Vertex, total_count); - @memcpy(pending[0..opaque_count], vertices.items); - @memcpy(pending[opaque_count..total_count], water_vertices.items); - self.pending_vertices = pending; - } else { - self.pending_vertices = null; - } - } - - /// Build mesh from simplified LOD data using QEM decimation. - /// Generates a full-detail heightmap mesh first, then simplifies via quadric error metrics. - /// Falls back to naive `buildFromSimplifiedData` if QEM input is too small or fails. - pub fn buildFromSimplifiedDataWithQEM( - self: *LODMesh, - data: *const LODSimplifiedData, - world_x: i32, - world_z: i32, - target_triangles: u32, - min_input_triangles: u32, - atlas: *const TextureAtlas, - ) !void { - const full_mesh = buildFullDetailHeightmapMesh(self.allocator, self.lod_level, data, world_x, world_z, atlas) catch |err| { - log.log.warn("LOD{} full-detail mesh build failed, falling back: {}", .{ @intFromEnum(self.lod_level), err }); - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - }; - defer { - self.allocator.free(full_mesh.vertices); - self.allocator.free(full_mesh.indices); - } - - if (full_mesh.indices.len % 3 != 0) { - log.log.warn("LOD{} mesh has invalid index count {}, falling back", .{ @intFromEnum(self.lod_level), full_mesh.indices.len }); - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - } - const input_triangles: u32 = @intCast(full_mesh.indices.len / 3); - if (input_triangles < min_input_triangles) { - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - } - - // No simplification needed — target already meets or exceeds input - if (target_triangles >= input_triangles) { - try self.setPendingFromIndexed(full_mesh.vertices, full_mesh.indices); - return; - } - const effective_target = target_triangles; - - const simplified = QuadricSimplifier.simplify( - self.allocator, - full_mesh.vertices, - full_mesh.indices, - effective_target, - ) catch |err| { - log.log.warn("LOD{} QEM simplification failed, falling back to naive: {}", .{ @intFromEnum(self.lod_level), err }); - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - }; - defer { - self.allocator.free(simplified.vertices); - self.allocator.free(simplified.indices); - } - - if (simplified.indices.len == 0) { - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - } - - log.log.trace("LOD{} QEM: {} -> {} triangles (error={d:.2})", .{ - @intFromEnum(self.lod_level), - simplified.original_triangle_count, - simplified.simplified_triangle_count, - simplified.error_estimate, - }); - - self.setPendingFromIndexed(simplified.vertices, simplified.indices) catch |err| { - log.log.warn("LOD{} failed to expand simplified mesh, falling back: {}", .{ @intFromEnum(self.lod_level), err }); - return self.buildFromSimplifiedData(data, world_x, world_z, atlas); - }; - } - - /// Convert indexed triangle mesh to non-indexed vertex list and store as pending. - fn setPendingFromIndexed(self: *LODMesh, vertices: []const Vertex, indices: []const u32) !void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pending_vertices) |p| { - self.allocator.free(p); - self.pending_vertices = null; - } - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - - if (indices.len == 0) return; - - const expanded = try self.allocator.alloc(Vertex, indices.len); - errdefer self.allocator.free(expanded); - for (expanded, 0..) |*dst, i| { - const idx = indices[i]; - if (idx >= vertices.len) return error.InvalidIndex; - dst.* = vertices[idx]; - } - self.pending_vertices = expanded; - self.opaque_vertex_count = @intCast(expanded.len); - self.water_vertex_offset = expanded.len * @sizeOf(Vertex); - self.water_vertex_count = 0; - } - - /// Build mesh from full chunk heightmap data - pub fn buildFromHeightmap( - self: *LODMesh, - heightmap: []const f32, - biomes: []const BiomeId, - width: u32, - world_x: i32, - world_z: i32, - atlas: *const TextureAtlas, - ) !void { - const cell_size = getCellSize(self.lod_level); - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(self.allocator); - - var gz: u32 = 0; - while (gz < width) : (gz += 1) { - var gx: u32 = 0; - while (gx < width) : (gx += 1) { - const idx = gx + gz * width; - const height = heightmap[idx]; - const biome = biomes[idx]; - const surface_block = biome.getSurfaceBlock(); - const color = applyTextureLuminance(biome_mod.getBiomeColor(biome), surface_block, .top, atlas); - const side_color = applyTextureLuminance(biome_mod.getBiomeColor(biome), surface_block, .side, atlas); - - const r: f32 = @as(f32, @floatFromInt((color >> 16) & 0xFF)) / 255.0; - const g: f32 = @as(f32, @floatFromInt((color >> 8) & 0xFF)) / 255.0; - const b: f32 = @as(f32, @floatFromInt(color & 0xFF)) / 255.0; - const sr: f32 = @as(f32, @floatFromInt((side_color >> 16) & 0xFF)) / 255.0; - const sg: f32 = @as(f32, @floatFromInt((side_color >> 8) & 0xFF)) / 255.0; - const sb: f32 = @as(f32, @floatFromInt(side_color & 0xFF)) / 255.0; - - const wx: f32 = @floatFromInt(gx * cell_size); - const wz: f32 = @floatFromInt(gz * cell_size); - const wy: f32 = height; - const size: f32 = @floatFromInt(cell_size); - - const tiles = atlas.getTilesForBlock(@intFromEnum(surface_block)); - - try addTopFaceQuad(self.allocator, &vertices, wx, wy, wz, size, r, g, b, tiles.top, world_x, world_z); - - // Add skirts - const skirt_depth = size * 4.0; - if (gx == 0) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, sr * 0.6, sg * 0.6, sb * 0.6, .west, tiles.side, world_x, world_z); - } - if (gx == width - 1) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, sr * 0.6, sg * 0.6, sb * 0.6, .east, tiles.side, world_x, world_z); - } - if (gz == 0) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, sr * 0.7, sg * 0.7, sb * 0.7, .north, tiles.side, world_x, world_z); - } - if (gz == width - 1) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, sr * 0.7, sg * 0.7, sb * 0.7, .south, tiles.side, world_x, world_z); - } - - // Side faces for height differences - if (gx > 0) { - const nh = heightmap[(gx - 1) + gz * width]; - if (height > nh + 2) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, nh, sr * 0.7, sg * 0.7, sb * 0.7, .west, tiles.side, world_x, world_z); - } - } - if (gz > 0) { - const nh = heightmap[gx + (gz - 1) * width]; - if (height > nh + 2) { - try addSideFaceQuad(self.allocator, &vertices, wx, wy, wz, size, nh, sr * 0.8, sg * 0.8, sb * 0.8, .north, tiles.side, world_x, world_z); - } - } - } - } - - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pending_vertices) |p| { - self.allocator.free(p); - } - - if (vertices.items.len > 0) { - self.pending_vertices = try self.allocator.dupe(Vertex, vertices.items); - self.opaque_vertex_count = @intCast(vertices.items.len); - self.water_vertex_offset = self.opaque_vertex_count * @sizeOf(Vertex); - self.water_vertex_count = 0; - } else { - self.pending_vertices = null; - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - } - } - - /// Upload pending vertices to GPU - pub fn upload(self: *LODMesh, resources: LODMeshResources) RhiError!void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.pooled) return error.InvalidState; - - const pending = self.pending_vertices orelse { - self.ready = self.buffer_handle != 0; - return; - }; - - if (pending.len == 0) { - if (self.buffer_handle != 0 and !self.pooled) { - resources.destroyBuffer(self.buffer_handle); - } - self.buffer_handle = 0; - self.vertex_count = 0; - self.opaque_vertex_count = 0; - self.water_vertex_offset = 0; - self.water_vertex_count = 0; - self.capacity = 0; - self.vertex_offset = 0; - self.pooled = false; - self.allocator.free(pending); - self.pending_vertices = null; - self.ready = true; - return; - } - - const data_size = pending.len * @sizeOf(Vertex); - const needed_capacity = @max(1024, std.math.ceilPowerOfTwo(usize, data_size) catch data_size); - - var upload_handle = self.buffer_handle; - var new_handle: BufferHandle = 0; - - // Create or resize buffer. Keep the old buffer renderable until the - // replacement upload succeeds, then retire it through the RHI. - if (self.buffer_handle == 0 or needed_capacity > self.capacity * @sizeOf(Vertex)) { - new_handle = try resources.createBuffer(needed_capacity, .vertex); - upload_handle = new_handle; - } - errdefer if (new_handle != 0) resources.destroyBuffer(new_handle); - - // Upload data - try uploadBufferChunked(resources, upload_handle, std.mem.sliceAsBytes(pending)); - if (new_handle != 0) { - const old_handle = self.buffer_handle; - self.buffer_handle = new_handle; - self.capacity = @intCast(needed_capacity / @sizeOf(Vertex)); - self.vertex_offset = 0; - self.pooled = false; - if (old_handle != 0 and !self.pooled) { - resources.destroyBuffer(old_handle); - } - } - self.vertex_count = @intCast(pending.len); - if (self.opaque_vertex_count == 0 and self.water_vertex_count == 0) { - self.opaque_vertex_count = self.vertex_count; - } - - self.allocator.free(pending); - self.pending_vertices = null; - self.ready = true; - } - - /// Draw the LOD mesh - pub fn draw(self: *const LODMesh, render_ctx: LODMeshRenderContext) void { - if (!self.ready or self.buffer_handle == 0 or self.vertex_count == 0) return; - render_ctx.drawOffset(self.buffer_handle, self.vertex_count, .triangles, self.vertex_offset); - } -}; - -test "chunk-derived span sources use the stable heightfield fallback" { - var data = try LODSimplifiedData.initWithVerticalSpans(std.testing.allocator, .lod2); - defer data.deinit(); - - try std.testing.expect(canBuildColumnSpans(&data)); - data.setColumnProvenance(0, 0, .chunk_derived); - try std.testing.expect(!canBuildColumnSpans(&data)); -} - -/// LOD Mesh Builder - builds meshes for LOD regions -pub const LODMeshBuilder = struct { - allocator: std.mem.Allocator, - - pub fn init(allocator: std.mem.Allocator) LODMeshBuilder { - return .{ .allocator = allocator }; - } - - /// Build LOD1 mesh from 2x2 chunk heightmaps - pub fn buildLOD1( - self: *LODMeshBuilder, - mesh: *LODMesh, - heightmaps: [4][]const f32, // NW, NE, SW, SE chunks - biomes: [4][]const BiomeId, - world_x: i32, - world_z: i32, - atlas: *const TextureAtlas, - ) !void { - _ = self; - const chunk_size: u32 = 16; - const cell_size: u32 = 2; // LOD1 = 2x scale - const grid_per_chunk = chunk_size / cell_size; // 8 cells per chunk - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(mesh.allocator); - - // Process each of the 4 chunks - const chunk_offsets = [4][2]i32{ - .{ 0, 0 }, // NW - .{ 16, 0 }, // NE - .{ 0, 16 }, // SW - .{ 16, 16 }, // SE - }; - - for (chunk_offsets, 0..) |offset, chunk_idx| { - const heightmap = heightmaps[chunk_idx]; - const biome_data = biomes[chunk_idx]; - - var gz: u32 = 0; - while (gz < grid_per_chunk) : (gz += 1) { - var gx: u32 = 0; - while (gx < grid_per_chunk) : (gx += 1) { - // Sample center of each cell - const sample_x = gx * cell_size + cell_size / 2; - const sample_z = gz * cell_size + cell_size / 2; - const idx = sample_x + sample_z * chunk_size; - - if (idx >= heightmap.len) continue; - - const height = heightmap[idx]; - const biome = biome_data[idx]; - const color = biome_mod.getBiomeColor(biome); - const tiles = atlas.getTilesForBlock(@intFromEnum(biome.getSurfaceBlock())); - - const r: f32 = @as(f32, @floatFromInt((color >> 16) & 0xFF)) / 255.0; - const g: f32 = @as(f32, @floatFromInt((color >> 8) & 0xFF)) / 255.0; - const b: f32 = @as(f32, @floatFromInt(color & 0xFF)) / 255.0; - - const wx: f32 = @floatFromInt(offset[0] + @as(i32, @intCast(gx * cell_size))); - const wz: f32 = @floatFromInt(offset[1] + @as(i32, @intCast(gz * cell_size))); - const wy: f32 = @floatFromInt(height); - const size: f32 = @floatFromInt(cell_size); - - try addTopFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, r, g, b, tiles.top, world_x, world_z); - - // Skirts - const skirt_depth = size * 4.0; - if (gx == 0) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.6, g * 0.6, b * 0.6, .west, tiles.side, world_x, world_z); - if (gx == grid_per_chunk - 1) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.6, g * 0.6, b * 0.6, .east, tiles.side, world_x, world_z); - if (gz == 0) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.7, g * 0.7, b * 0.7, .north, tiles.side, world_x, world_z); - if (gz == grid_per_chunk - 1) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.7, g * 0.7, b * 0.7, .south, tiles.side, world_x, world_z); - } - } - } - - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - if (mesh.pending_vertices) |p| { - mesh.allocator.free(p); - } - - if (vertices.items.len > 0) { - mesh.pending_vertices = try mesh.allocator.dupe(Vertex, vertices.items); - } else { - mesh.pending_vertices = null; - } - } - - /// Build LOD2 mesh from 4x4 chunk heightmaps - pub fn buildLOD2( - self: *LODMeshBuilder, - mesh: *LODMesh, - heightmaps: [16][]const f32, - biomes_data: [16][]const BiomeId, - world_x: i32, - world_z: i32, - atlas: *const TextureAtlas, - ) !void { - _ = self; - const chunk_size: u32 = 16; - const cell_size: u32 = 4; // LOD2 = 4x scale - const grid_per_chunk = chunk_size / cell_size; // 4 cells per chunk - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(mesh.allocator); - - // 4x4 grid of chunks - for (0..16) |chunk_idx| { - const cx: i32 = @intCast(chunk_idx % 4); - const cz: i32 = @intCast(chunk_idx / 4); - const offset_x = cx * @as(i32, chunk_size); - const offset_z = cz * @as(i32, chunk_size); - - const heightmap = heightmaps[chunk_idx]; - const biome_data = biomes_data[chunk_idx]; - - var gz: u32 = 0; - while (gz < grid_per_chunk) : (gz += 1) { - var gx: u32 = 0; - while (gx < grid_per_chunk) : (gx += 1) { - const sample_x = gx * cell_size + cell_size / 2; - const sample_z = gz * cell_size + cell_size / 2; - const idx = sample_x + sample_z * chunk_size; - - if (idx >= heightmap.len) continue; - - const height = heightmap[idx]; - const biome = biome_data[idx]; - const color = biome_mod.getBiomeColor(biome); - const tiles = atlas.getTilesForBlock(@intFromEnum(biome.getSurfaceBlock())); - - const r: f32 = @as(f32, @floatFromInt((color >> 16) & 0xFF)) / 255.0; - const g: f32 = @as(f32, @floatFromInt((color >> 8) & 0xFF)) / 255.0; - const b: f32 = @as(f32, @floatFromInt(color & 0xFF)) / 255.0; - - const wx: f32 = @floatFromInt(offset_x + @as(i32, @intCast(gx * cell_size))); - const wz: f32 = @floatFromInt(offset_z + @as(i32, @intCast(gz * cell_size))); - const wy: f32 = @floatFromInt(height); - const size: f32 = @floatFromInt(cell_size); - - try addTopFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, r, g, b, tiles.top, world_x, world_z); - - // Skirts - const skirt_depth = size * 4.0; - if (gx == 0) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.6, g * 0.6, b * 0.6, .west, tiles.side, world_x, world_z); - if (gx == grid_per_chunk - 1) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.6, g * 0.6, b * 0.6, .east, tiles.side, world_x, world_z); - if (gz == 0) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.7, g * 0.7, b * 0.7, .north, tiles.side, world_x, world_z); - if (gz == grid_per_chunk - 1) try addSideFaceQuad(mesh.allocator, &vertices, wx, wy, wz, size, wy - skirt_depth, r * 0.7, g * 0.7, b * 0.7, .south, tiles.side, world_x, world_z); - } - } - } - - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - if (mesh.pending_vertices) |p| { - mesh.allocator.free(p); - } - - if (vertices.items.len > 0) { - mesh.pending_vertices = try mesh.allocator.dupe(Vertex, vertices.items); - } else { - mesh.pending_vertices = null; - } - } - - /// Build LOD3 mesh from simplified heightmap data - pub fn buildLOD3( - self: *LODMeshBuilder, - mesh: *LODMesh, - data: *const LODSimplifiedData, - region_world_x: i32, - region_world_z: i32, - atlas: *const TextureAtlas, - ) !void { - _ = self; - try mesh.buildFromSimplifiedData(data, region_world_x, region_world_z, atlas); - } -}; diff --git a/modules/world-lod/src/lod_mesh_resources.zig b/modules/world-lod/src/lod_mesh_resources.zig deleted file mode 100644 index ca4c6b4c..00000000 --- a/modules/world-lod/src/lod_mesh_resources.zig +++ /dev/null @@ -1,196 +0,0 @@ -//! RHI resource adapter for LOD mesh GPU buffers. - -const std = @import("std"); -const rhi_pkg = @import("engine-rhi").rhi; -const rhi_types = @import("engine-rhi"); -const BufferHandle = rhi_types.BufferHandle; -const BufferUsage = rhi_types.BufferUsage; -const RhiError = rhi_types.RhiError; - -pub const LODMeshResources = struct { - ptr: *anyopaque, - vtable: *const VTable, - - pub const VTable = struct { - createBuffer: *const fn (ptr: *anyopaque, size: usize, usage: BufferUsage) RhiError!BufferHandle, - uploadBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle, data: []const u8) RhiError!void, - updateBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void, - destroyBuffer: *const fn (ptr: *anyopaque, handle: BufferHandle) void, - waitIdle: *const fn (ptr: *anyopaque) void, - }; - - pub fn fromRHI(rhi: *rhi_pkg.RHI) LODMeshResources { - return .{ .ptr = rhi, .vtable = &rhi_vtable }; - } - - pub fn fromProvider(comptime Provider: type, provider: *Provider) LODMeshResources { - const Adapter = struct { - fn createBuffer(ptr: *anyopaque, size: usize, usage: BufferUsage) RhiError!BufferHandle { - const typed: *Provider = @ptrCast(@alignCast(ptr)); - if (@hasDecl(Provider, "resourceManager")) { - return typed.resourceManager().createBuffer(size, usage); - } - return typed.createBuffer(size, usage); - } - - fn uploadBuffer(ptr: *anyopaque, handle: BufferHandle, data: []const u8) RhiError!void { - const typed: *Provider = @ptrCast(@alignCast(ptr)); - if (@hasDecl(Provider, "resourceManager")) { - return typed.resourceManager().uploadBuffer(handle, data); - } - if (@hasDecl(Provider, "uploadBuffer")) { - return typed.uploadBuffer(handle, data); - } - if (@hasDecl(Provider, "updateBuffer")) { - return typed.updateBuffer(handle, 0, data); - } - return error.InvalidState; - } - - fn updateBuffer(ptr: *anyopaque, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void { - const typed: *Provider = @ptrCast(@alignCast(ptr)); - if (@hasDecl(Provider, "resourceManager")) { - return typed.resourceManager().updateBuffer(handle, offset, data); - } - if (@hasDecl(Provider, "updateBuffer")) { - return typed.updateBuffer(handle, offset, data); - } - if (offset == 0 and @hasDecl(Provider, "uploadBuffer")) { - return typed.uploadBuffer(handle, data); - } - return error.InvalidState; - } - - fn destroyBuffer(ptr: *anyopaque, handle: BufferHandle) void { - const typed: *Provider = @ptrCast(@alignCast(ptr)); - if (@hasDecl(Provider, "resourceManager")) { - typed.resourceManager().destroyBuffer(handle); - return; - } - typed.destroyBuffer(handle); - } - - fn waitIdle(ptr: *anyopaque) void { - const typed: *Provider = @ptrCast(@alignCast(ptr)); - if (@hasDecl(Provider, "waitIdle")) { - typed.waitIdle(); - } - } - - const vtable = VTable{ - .createBuffer = @This().createBuffer, - .uploadBuffer = @This().uploadBuffer, - .updateBuffer = @This().updateBuffer, - .destroyBuffer = @This().destroyBuffer, - .waitIdle = @This().waitIdle, - }; - }; - - return .{ .ptr = provider, .vtable = &Adapter.vtable }; - } - - pub fn createBuffer(self: LODMeshResources, size: usize, usage: BufferUsage) RhiError!BufferHandle { - return self.vtable.createBuffer(self.ptr, size, usage); - } - - pub fn uploadBuffer(self: LODMeshResources, handle: BufferHandle, data: []const u8) RhiError!void { - return self.vtable.uploadBuffer(self.ptr, handle, data); - } - - pub fn updateBuffer(self: LODMeshResources, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void { - return self.vtable.updateBuffer(self.ptr, handle, offset, data); - } - - pub fn destroyBuffer(self: LODMeshResources, handle: BufferHandle) void { - self.vtable.destroyBuffer(self.ptr, handle); - } - - pub fn waitIdle(self: LODMeshResources) void { - self.vtable.waitIdle(self.ptr); - } - - const rhi_vtable = VTable{ - .createBuffer = struct { - fn f(ptr: *anyopaque, size: usize, usage: BufferUsage) RhiError!BufferHandle { - const rhi: *rhi_pkg.RHI = @ptrCast(@alignCast(ptr)); - return rhi.resourceManager().createBuffer(size, usage); - } - }.f, - .uploadBuffer = struct { - fn f(ptr: *anyopaque, handle: BufferHandle, data: []const u8) RhiError!void { - const rhi: *rhi_pkg.RHI = @ptrCast(@alignCast(ptr)); - return rhi.resourceManager().uploadBuffer(handle, data); - } - }.f, - .updateBuffer = struct { - fn f(ptr: *anyopaque, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void { - const rhi: *rhi_pkg.RHI = @ptrCast(@alignCast(ptr)); - return rhi.resourceManager().updateBuffer(handle, offset, data); - } - }.f, - .destroyBuffer = struct { - fn f(ptr: *anyopaque, handle: BufferHandle) void { - const rhi: *rhi_pkg.RHI = @ptrCast(@alignCast(ptr)); - rhi.resourceManager().destroyBuffer(handle); - } - }.f, - .waitIdle = struct { - fn f(ptr: *anyopaque) void { - const rhi: *rhi_pkg.RHI = @ptrCast(@alignCast(ptr)); - rhi.waitIdle(); - } - }.f, - }; -}; - -/// The staging work an upload will record this frame. Pool replacement copies -/// are deliberately separate from the payload so a frame budget cannot hide a -/// full-pool migration behind a small incoming mesh. -pub const LODStagingCost = struct { - payload_bytes: usize = 0, - migration_bytes: usize = 0, - - pub fn total(self: LODStagingCost) usize { - return std.math.add(usize, self.payload_bytes, self.migration_bytes) catch std.math.maxInt(usize); - } -}; - -/// Keep individual Vulkan staging-ring allocations bounded. Large LOD meshes -/// can exceed the remaining per-frame staging space even when the frame-level -/// upload budget is respected; splitting avoids one oversized allocation. -pub const MAX_STAGING_UPDATE_BYTES: usize = 8 * 1024 * 1024; - -pub fn updateBufferChunked(resources: LODMeshResources, handle: BufferHandle, offset: usize, data: []const u8) RhiError!void { - var cursor: usize = 0; - while (cursor < data.len) { - const chunk_len = @min(MAX_STAGING_UPDATE_BYTES, data.len - cursor); - try resources.updateBuffer(handle, offset + cursor, data[cursor .. cursor + chunk_len]); - cursor += chunk_len; - } -} - -pub fn uploadBufferChunked(resources: LODMeshResources, handle: BufferHandle, data: []const u8) RhiError!void { - if (data.len <= MAX_STAGING_UPDATE_BYTES) { - try resources.uploadBuffer(handle, data); - return; - } - try updateBufferChunked(resources, handle, 0, data); -} - -pub const LODMeshRenderContext = struct { - ptr: *anyopaque, - draw_fn: *const fn (ptr: *anyopaque, handle: BufferHandle, count: u32, mode: rhi_types.DrawMode) void, - draw_offset_fn: ?*const fn (ptr: *anyopaque, handle: BufferHandle, count: u32, mode: rhi_types.DrawMode, offset: usize) void = null, - - pub fn draw(self: LODMeshRenderContext, handle: BufferHandle, count: u32, mode: rhi_types.DrawMode) void { - self.draw_fn(self.ptr, handle, count, mode); - } - - pub fn drawOffset(self: LODMeshRenderContext, handle: BufferHandle, count: u32, mode: rhi_types.DrawMode, offset: usize) void { - if (self.draw_offset_fn) |draw_offset| { - draw_offset(self.ptr, handle, count, mode, offset); - return; - } - self.draw(handle, count, mode); - } -}; diff --git a/modules/world-lod/src/lod_mesh_tests.zig b/modules/world-lod/src/lod_mesh_tests.zig deleted file mode 100644 index c60b4003..00000000 --- a/modules/world-lod/src/lod_mesh_tests.zig +++ /dev/null @@ -1,1557 +0,0 @@ -//! Tests for LOD mesh generation. - -const std = @import("std"); -const mesh_mod = @import("lod_mesh.zig"); -const geom = @import("lod_geometry.zig"); -const world_core = @import("world-core"); -const biome_mod = @import("biome_color_provider.zig"); -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; -const TextureAtlas = @import("engine-assets").TextureAtlas; -const rhi_types = @import("engine-rhi"); -const Vertex = rhi_types.Vertex; -const BufferHandle = rhi_types.BufferHandle; -const BufferUsage = rhi_types.BufferUsage; -const RhiError = rhi_types.RhiError; -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODSimplifiedData = lod_chunk.LODSimplifiedData; - -const LODMesh = mesh_mod.LODMesh; -const LODMeshResources = mesh_mod.LODMeshResources; -const CompactLODPool = @import("lod_compact_pool.zig").CompactLODPool; -const TileEdge = @import("lod_tile.zig").TileEdge; -const TILE_EDGE_MASK = @import("lod_tile.zig").TILE_EDGE_MASK; -const MAX_STAGING_UPDATE_BYTES = mesh_mod.MAX_STAGING_UPDATE_BYTES; -const getCellSize = mesh_mod.getCellSize; -const updateBufferChunked = mesh_mod.updateBufferChunked; - -const LODColumnSpan = geom.LODColumnSpan; -const LODTextureFace = geom.LODTextureFace; -const addExposedSpanFace = geom.addExposedSpanFace; -const addTreeCanopyColumn = geom.addTreeCanopyColumn; -const applyTextureLuminance = geom.applyTextureLuminance; -const blockForLODQuad = geom.blockForLODQuad; -const buildFullDetailHeightmapMesh = geom.buildFullDetailHeightmapMesh; -const collectColumnSpans = geom.collectColumnSpans; -const makeLODVertex = geom.makeLODVertex; -const quantizedCellSurfaceHeight = geom.quantizedCellSurfaceHeight; -const quantizedCellTerrainHeight = geom.quantizedCellTerrainHeight; -const quantizedWaterSurfaceHeightForCell = geom.quantizedWaterSurfaceHeightForCell; -const representativeVegetationForLOD = geom.representativeVegetationForLOD; - -// Tests -fn testResources() LODMeshResources { - const Mock = struct { - fn createBuffer(_: *anyopaque, _: usize, _: BufferUsage) RhiError!BufferHandle { - return 1; - } - fn uploadBuffer(_: *anyopaque, _: BufferHandle, _: []const u8) RhiError!void {} - fn updateBuffer(_: *anyopaque, _: BufferHandle, _: usize, _: []const u8) RhiError!void {} - fn destroyBuffer(_: *anyopaque, _: BufferHandle) void {} - fn waitIdle(_: *anyopaque) void {} - - const vtable = LODMeshResources.VTable{ - .createBuffer = createBuffer, - .uploadBuffer = uploadBuffer, - .updateBuffer = updateBuffer, - .destroyBuffer = destroyBuffer, - .waitIdle = waitIdle, - }; - }; - return .{ .ptr = undefined, .vtable = &Mock.vtable }; -} - -test "LODMesh initialization" { - const allocator = std.testing.allocator; - var mesh = LODMesh.init(allocator, .lod1); - defer mesh.deinit(testResources()); - - try std.testing.expectEqual(LODLevel.lod1, mesh.lod_level); - try std.testing.expectEqual(@as(u32, 0), mesh.vertex_count); - try std.testing.expect(!mesh.ready); -} - -test "compact late neighbor keeps a resident fallback edge skirted" { - const allocator = std.testing.allocator; - var left_source = try LODSimplifiedData.init(allocator, .lod4); - defer left_source.deinit(); - var right_source = try LODSimplifiedData.init(allocator, .lod4); - defer right_source.deinit(); - right_source.setHeight(0, 3, 88.0); - - var left = LODMesh.init(allocator, .lod4); - var pool = CompactLODPool.init(allocator); - defer { - // `CompactLODPool` owns the shared storage handle. Do not call the - // generic mesh deinit while that handle is still attached. - left.clearRetiredState(); - pool.deinit(testResources()); - } - try left.buildCompactTile(&left_source); - - // The pool owns the immutable GPU range and releases the CPU payload. A - // neighbor arriving later must not make the old fallback apron seamless. - try pool.upload(&left, testResources()); - try std.testing.expect(left.compact_tile == null); - var late_right = LODMesh.init(allocator, .lod4); - defer late_right.deinit(testResources()); - try late_right.buildCompactTile(&right_source); - - try std.testing.expect(!left.patchCompactNeighbor(.east, &late_right)); - try std.testing.expect(!left.compactEdgeIsSeamless(.east)); - try std.testing.expectEqual(TILE_EDGE_MASK, left.compactSkirtMask()); -} - -test "compact eviction reload does not retain a stale seam claim" { - const allocator = std.testing.allocator; - var left_source = try LODSimplifiedData.init(allocator, .lod4); - defer left_source.deinit(); - var right_source = try LODSimplifiedData.init(allocator, .lod4); - defer right_source.deinit(); - - var left = LODMesh.init(allocator, .lod4); - defer left.deinit(testResources()); - var right = LODMesh.init(allocator, .lod4); - defer right.deinit(testResources()); - try left.buildCompactTile(&left_source); - try right.buildCompactTile(&right_source); - try std.testing.expect(left.patchCompactNeighbor(.east, &right)); - try std.testing.expect(left.compactEdgeIsSeamless(.east)); - - // Retiring the old range and rebuilding from a reloaded source starts with - // no authority. The edge becomes seamless only after a fresh pre-upload - // neighbor patch, never by inheriting the former resident metadata. - left.clearRetiredState(); - try left.buildCompactTile(&left_source); - try std.testing.expect(!left.compactEdgeIsSeamless(.east)); - try std.testing.expectEqual(TILE_EDGE_MASK, left.compactSkirtMask()); - try std.testing.expect(left.patchCompactNeighbor(.east, &right)); - try std.testing.expect(left.compactEdgeIsSeamless(.east)); -} - -test "compact differing LOD edges remain non-seamless and skirted" { - const allocator = std.testing.allocator; - var fine_source = try LODSimplifiedData.init(allocator, .lod3); - defer fine_source.deinit(); - var coarse_source = try LODSimplifiedData.init(allocator, .lod4); - defer coarse_source.deinit(); - - var fine = LODMesh.init(allocator, .lod3); - defer fine.deinit(testResources()); - var coarse = LODMesh.init(allocator, .lod4); - defer coarse.deinit(testResources()); - try fine.buildCompactTile(&fine_source); - try coarse.buildCompactTile(&coarse_source); - - try std.testing.expect(!fine.patchCompactNeighbor(TileEdge.east, &coarse)); - try std.testing.expect(!fine.compactEdgeIsSeamless(.east)); - try std.testing.expectEqual(TILE_EDGE_MASK, fine.compactSkirtMask()); -} - -test "getCellSize" { - try std.testing.expectEqual(@as(u32, 1), getCellSize(.lod0)); - try std.testing.expectEqual(@as(u32, 1), getCellSize(.lod1)); - try std.testing.expectEqual(@as(u32, 2), getCellSize(.lod2)); - try std.testing.expectEqual(@as(u32, 2), getCellSize(.lod3)); - try std.testing.expectEqual(@as(u32, 4), getCellSize(.lod4)); -} - -test "buildFromSimplifiedData keeps distant heightfields voxel stepped" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod3); - defer data.deinit(); - fillColumnSpanData(&data, .grass, 64.0, 0x3A7D42); - data.setHeight(0, 0, 64.0); - data.setHeight(1, 0, 70.0); - data.setHeight(0, 1, 66.0); - data.setHeight(1, 1, 72.0); - - var mesh = LODMesh.init(allocator, .lod3); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len >= 6); - - var top_vertices_at_base_height: usize = 0; - for (verts[0..6]) |v| { - if (v.pos[1] == 64.0) top_vertices_at_base_height += 1; - } - - try std.testing.expectEqual(@as(usize, 6), top_vertices_at_base_height); -} - -test "buildFullDetailHeightmapMesh spans full LOD region" { - const allocator = std.testing.allocator; - - var atlas: TextureAtlas = undefined; - @memset(std.mem.asBytes(&atlas.tile_mappings), 0); - atlas.tile_luminance = [_]TextureAtlas.BlockTileLuminance{TextureAtlas.BlockTileLuminance.uniform(1.0)} ** world_core.MAX_BLOCK_TYPES; - atlas.tile_colors = [_]TextureAtlas.BlockTileColor{TextureAtlas.BlockTileColor.uniform(0xFFFFFF)} ** world_core.MAX_BLOCK_TYPES; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - const cell_count: usize = @intCast(data.width * data.width); - var i: usize = 0; - while (i < cell_count) : (i += 1) { - data.heightmap[i] = 0.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .air; - data.colors[i] = 0; - } - - const mesh = try buildFullDetailHeightmapMesh(allocator, .lod3, &data, 32, 64, &atlas); - defer { - allocator.free(mesh.vertices); - allocator.free(mesh.indices); - } - - var max_x: f32 = 0.0; - var max_z: f32 = 0.0; - for (mesh.vertices) |v| { - max_x = @max(max_x, v.pos[0]); - max_z = @max(max_z, v.pos[2]); - } - - try std.testing.expectEqual(@as(f32, 256.0), max_x); - try std.testing.expectEqual(@as(f32, 256.0), max_z); - try std.testing.expectEqual(@as(f32, 32.0), @as(f32, mesh.vertices[0].uv[0])); - try std.testing.expectEqual(@as(f32, 64.0), @as(f32, mesh.vertices[0].uv[1])); -} - -test "setPendingFromIndexed resets stale LOD draw ranges" { - const allocator = std.testing.allocator; - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - mesh.opaque_vertex_count = 24; - mesh.water_vertex_offset = 24 * @sizeOf(Vertex); - mesh.water_vertex_count = 6; - - const source = [_]Vertex{ - makeLODVertex(.{ 0.0, 1.0, 0.0 }, .{ 1.0, 1.0, 1.0 }, .{ 0.0, 1.0, 0.0 }, .{ 0.0, 0.0 }, Vertex.LOD_TILE_ID), - makeLODVertex(.{ 1.0, 1.0, 0.0 }, .{ 1.0, 1.0, 1.0 }, .{ 0.0, 1.0, 0.0 }, .{ 1.0, 0.0 }, Vertex.LOD_TILE_ID), - makeLODVertex(.{ 0.0, 1.0, 1.0 }, .{ 1.0, 1.0, 1.0 }, .{ 0.0, 1.0, 0.0 }, .{ 0.0, 1.0 }, Vertex.LOD_TILE_ID), - }; - const indices = [_]u32{ 0, 1, 2 }; - - try mesh.setPendingFromIndexed(&source, &indices); - - const pending = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(usize, 3), pending.len); - try std.testing.expectEqual(@as(u32, 3), mesh.opaque_vertex_count); - try std.testing.expectEqual(@as(usize, 3 * @sizeOf(Vertex)), mesh.water_vertex_offset); - try std.testing.expectEqual(@as(u32, 0), mesh.water_vertex_count); -} - -fn vertexTileId(v: Vertex) u16 { - return @intCast(v.packed_meta & 0xFFFF); -} - -fn vertexRgb(v: Vertex) u32 { - const r = v.color & 0xFF; - const g = (v.color >> 8) & 0xFF; - const b = (v.color >> 16) & 0xFF; - return (r << 16) | (g << 8) | b; -} - -fn linearizeTestRgb(color: u32) u32 { - const r = linearizeTestChannel(@intCast((color >> 16) & 0xFF)); - const g = linearizeTestChannel(@intCast((color >> 8) & 0xFF)); - const b = linearizeTestChannel(@intCast(color & 0xFF)); - return (r << 16) | (g << 8) | b; -} - -fn linearizeTestChannel(value: u8) u32 { - const c = @as(f32, @floatFromInt(value)) / 255.0; - const linear = if (c <= 0.04045) c / 12.92 else std.math.pow(f32, (c + 0.055) / 1.055, 2.4); - return @intFromFloat(@round(std.math.clamp(linear * 255.0, 0.0, 255.0))); -} - -test "updateBufferChunked bounds individual staging updates" { - const Mock = struct { - calls: u32 = 0, - max_len: usize = 0, - total_len: usize = 0, - - fn createBuffer(_: *anyopaque, _: usize, _: BufferUsage) RhiError!BufferHandle { - return 1; - } - - fn uploadBuffer(_: *anyopaque, _: BufferHandle, _: []const u8) RhiError!void {} - - fn updateBuffer(ptr: *anyopaque, _: BufferHandle, _: usize, data: []const u8) RhiError!void { - const self: *@This() = @ptrCast(@alignCast(ptr)); - self.calls += 1; - self.max_len = @max(self.max_len, data.len); - self.total_len += data.len; - } - - fn destroyBuffer(_: *anyopaque, _: BufferHandle) void {} - fn waitIdle(_: *anyopaque) void {} - - const vtable = LODMeshResources.VTable{ - .createBuffer = createBuffer, - .uploadBuffer = uploadBuffer, - .updateBuffer = updateBuffer, - .destroyBuffer = destroyBuffer, - .waitIdle = waitIdle, - }; - }; - - const allocator = std.testing.allocator; - const len = MAX_STAGING_UPDATE_BYTES + 17; - const data = try allocator.alloc(u8, len); - defer allocator.free(data); - @memset(data, 0xAB); - - var mock = Mock{}; - try updateBufferChunked(.{ .ptr = &mock, .vtable = &Mock.vtable }, 1, 128, data); - - try std.testing.expectEqual(@as(u32, 2), mock.calls); - try std.testing.expect(mock.max_len <= MAX_STAGING_UPDATE_BYTES); - try std.testing.expectEqual(len, mock.total_len); -} - -fn testAtlas(allocator: std.mem.Allocator) TextureAtlas { - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** world_core.MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - atlas.tile_mappings[@intFromEnum(BlockType.dirt)] = .{ .top = 25, .bottom = 25, .side = 26 }; - atlas.tile_mappings[@intFromEnum(BlockType.stone)] = .{ .top = 31, .bottom = 31, .side = 32 }; - atlas.tile_mappings[@intFromEnum(BlockType.sand)] = .{ .top = 51, .bottom = 52, .side = 55 }; - atlas.tile_mappings[@intFromEnum(BlockType.water)] = .{ .top = 41, .bottom = 41, .side = 42 }; - atlas.tile_mappings[@intFromEnum(BlockType.wood)] = .{ .top = 62, .bottom = 62, .side = 63 }; - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - atlas.tile_colors[@intFromEnum(BlockType.grass)] = .{ .top = linearizeTestRgb(0x7FBF5A), .bottom = linearizeTestRgb(0x8A5A35), .side = linearizeTestRgb(0x6A8F42) }; - atlas.tile_colors[@intFromEnum(BlockType.dirt)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0x8A5A35)); - atlas.tile_colors[@intFromEnum(BlockType.stone)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0x777777)); - atlas.tile_colors[@intFromEnum(BlockType.sand)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0xD8C76D)); - atlas.tile_colors[@intFromEnum(BlockType.water)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0x3366CC)); - atlas.tile_colors[@intFromEnum(BlockType.wood)] = .{ .top = linearizeTestRgb(0x7B5A32), .bottom = linearizeTestRgb(0x7B5A32), .side = linearizeTestRgb(0x6B4428) }; - atlas.tile_colors[@intFromEnum(BlockType.leaves)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0x4C8F38)); - return atlas; -} - -fn fillColumnSpanData(data: *LODSimplifiedData, block: BlockType, height: f32, color: u32) void { - for (0..data.width * data.width) |i| { - data.heightmap[i] = height; - data.biomes[i] = .plains; - data.top_blocks[i] = block; - data.colors[i] = color; - data.material_layers[i] = .{ .surface = block, .subsurface = block, .foundation = block }; - data.lighting[i] = world_core.LODLightingHint.daylight; - } -} - -fn testSpan(min_height: f32, max_height: f32, block: BlockType, color: u32) world_core.LODVerticalSpan { - return .{ - .min_height = min_height, - .max_height = max_height, - .biome = .plains, - .material_layers = .{ .surface = block, .subsurface = block, .foundation = block }, - .color = color, - .water = world_core.LODWaterState.empty, - .lighting = world_core.LODLightingHint.daylight, - .vegetation = world_core.LODVegetationHint.empty, - }; -} - -test "buildFromColumnSpans falls back to heightfield without span data" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .grass, 64.0, 0x3A7D42); - - var heightfield_mesh = LODMesh.init(allocator, .lod2); - defer heightfield_mesh.deinit(testResources()); - try heightfield_mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - var span_mesh = LODMesh.init(allocator, .lod2); - defer span_mesh.deinit(testResources()); - try span_mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const heightfield_verts = heightfield_mesh.pending_vertices orelse return error.TestExpectedEqual; - const span_verts = span_mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expectEqual(heightfield_verts.len, span_verts.len); -} - -test "buildFromColumnSpans emits side faces for steep span terrain" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .stone, 64.0, 0x808080); - data.clearVerticalSpans(0, 0); - data.clearVerticalSpans(1, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(50.0, 96.0, .stone, 0x808080))); - try std.testing.expect(data.setVerticalSpan(1, 0, 0, testSpan(50.0, 64.0, .stone, 0x808080))); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_cliff_side = false; - for (verts) |v| { - if (vertexTileId(v) == 32 and v.pos[1] >= 95.0) { - found_cliff_side = true; - break; - } - } - try std.testing.expect(found_cliff_side); -} - -test "addExposedSpanFace suppresses unknown region border walls" { - const allocator = std.testing.allocator; - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(allocator); - - try addExposedSpanFace( - allocator, - &vertices, - &data, - 0, - 0, - null, - 0, - .lod2, - .{ .min_height = 0.0, .max_height = 64.0, .block = .stone, .color = 0x808080, .ambient_occlusion = 1.0 }, - 0.0, - 0.0, - 8.0, - 0x808080, - 32, - .west, - 0, - 0, - ); - - try std.testing.expectEqual(@as(usize, 0), vertices.items.len); -} - -test "buildFromColumnSpans adds water as a separate span" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 60.0, 0xD8C76D); - data.clearVerticalSpans(0, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(45.0, 60.0, .sand, 0xD8C76D))); - data.water[0] = .{ .is_surface = true, .surface_height = 63.0, .depth = 3.0, .coverage = 1.0 }; - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(mesh.opaque_vertex_count > 0); - try std.testing.expect(mesh.water_vertex_count > 0); - try std.testing.expectEqual(@as(u32, 6), mesh.water_vertex_count); - try std.testing.expectEqual(mesh.opaque_vertex_count * @sizeOf(Vertex), mesh.water_vertex_offset); - - for (verts[0..mesh.opaque_vertex_count]) |v| { - try std.testing.expect(vertexTileId(v) != 41); - } - - var found_water = false; - const water_start: usize = @intCast(mesh.opaque_vertex_count); - for (verts[water_start..]) |v| { - if (vertexTileId(v) == 41 and v.pos[1] == 63.0) { - found_water = true; - break; - } - } - try std.testing.expect(found_water); -} - -test "collectColumnSpans synthesizes local material seafloor under ocean water" { - const allocator = std.testing.allocator; - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 58.0; - data.biomes[i] = .ocean; - data.top_blocks[i] = .water; - data.colors[i] = 0x3355AA; - data.material_layers[i] = .{ .surface = .water, .subsurface = .sand, .foundation = .stone }; - data.water[i] = .{ .is_surface = true, .surface_height = 64.0, .depth = 6.0, .coverage = 1.0 }; - data.lighting[i] = world_core.LODLightingHint.daylight; - } - - var spans: [world_core.MAX_LOD_VERTICAL_SPANS + 1]LODColumnSpan = undefined; - const count = collectColumnSpans(&data, 0, 0, .lod2, &spans); - - var found_seafloor = false; - var found_water = false; - var i: usize = 0; - while (i < count) : (i += 1) { - if (spans[i].block == .sand) { - found_seafloor = true; - try std.testing.expectEqual(@as(f32, 50.0), spans[i].min_height); - try std.testing.expectEqual(@as(f32, 58.0), spans[i].max_height); - try std.testing.expect(spans[i].color != 0x3355AA); - } - if (spans[i].block == .water) found_water = true; - } - - try std.testing.expect(found_seafloor); - try std.testing.expect(found_water); -} - -test "buildFromSimplifiedData separates heightfield water from opaque terrain" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 60.0, 0xD8C76D); - - const water_points = [_]u32{ 0, 1, data.width, data.width + 1 }; - for (water_points) |idx| { - data.water[idx] = .{ .is_surface = true, .surface_height = 63.0, .depth = 3.0, .coverage = 1.0 }; - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(mesh.opaque_vertex_count > 0); - try std.testing.expect(mesh.water_vertex_count > 0); - try std.testing.expectEqual(mesh.opaque_vertex_count * @sizeOf(Vertex), mesh.water_vertex_offset); - - var found_seafloor = false; - for (verts[0..mesh.opaque_vertex_count]) |v| { - try std.testing.expect(vertexTileId(v) != 41); - if (vertexTileId(v) == 51 and v.pos[1] == 60.0) found_seafloor = true; - } - try std.testing.expect(found_seafloor); - - var found_water = false; - const water_start: usize = @intCast(mesh.opaque_vertex_count); - for (verts[water_start..]) |v| { - if (vertexTileId(v) == 41 and v.pos[1] == 63.0) { - found_water = true; - break; - } - } - try std.testing.expect(found_water); -} - -test "coarse heightfield water ignores isolated deep wet corner" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 60.0, 0xD8C76D); - data.water[0] = .{ .is_surface = true, .surface_height = 63.0, .depth = 8.0, .coverage = 1.0 }; - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expectEqual(@as(u32, 0), mesh.water_vertex_count); - - var found_surface_sand = false; - var found_depressed_sand = false; - for (verts[0..mesh.opaque_vertex_count]) |v| { - try std.testing.expect(vertexTileId(v) != 41); - if (vertexTileId(v) == 51 and v.pos[1] == 60.0) found_surface_sand = true; - if (vertexTileId(v) == 51 and v.pos[1] < 60.0) found_depressed_sand = true; - } - - try std.testing.expect(found_surface_sand); - try std.testing.expect(!found_depressed_sand); -} - -test "coarse span water ignores isolated low-coverage water span" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 60.0, 0xD8C76D); - data.clearVerticalSpans(0, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(45.0, 60.0, .sand, 0xD8C76D))); - - const water_state = world_core.LODWaterState{ .is_surface = true, .surface_height = 63.0, .depth = 3.0, .coverage = 0.2 }; - data.water[0] = water_state; - try std.testing.expect(data.setVerticalSpan(0, 0, 1, .{ - .min_height = 60.0, - .max_height = 63.0, - .biome = .plains, - .material_layers = .{ .surface = .water, .subsurface = .water, .foundation = .sand }, - .color = 0x3366CC, - .water = water_state, - .lighting = world_core.LODLightingHint.daylight, - .vegetation = world_core.LODVegetationHint.empty, - })); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(mesh.opaque_vertex_count > 0); - try std.testing.expectEqual(@as(u32, 0), mesh.water_vertex_count); - for (verts[0..mesh.opaque_vertex_count]) |v| { - try std.testing.expect(vertexTileId(v) != 41); - } -} - -test "buildFromColumnSpans skips empty columns while exposing neighbors" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .grass, 64.0, 0x3A7D42); - data.clearVerticalSpans(0, 0); - data.clearVerticalSpans(1, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(50.0, 64.0, .grass, 0x3A7D42))); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var side_count: usize = 0; - for (verts) |v| { - if (vertexTileId(v) == 24) side_count += 1; - } - try std.testing.expect(side_count >= 6); -} - -test "buildFromColumnSpans sorts representative spans by height" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .stone, 64.0, 0x808080); - data.clearVerticalSpans(0, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(80.0, 90.0, .stone, 0x808080))); - try std.testing.expect(data.setVerticalSpan(0, 0, 1, testSpan(40.0, 55.0, .dirt, 0x6B4A2B))); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_lower_top = false; - var found_upper_top = false; - for (verts) |v| { - if (vertexTileId(v) == 25 and v.pos[1] == 55.0) found_lower_top = true; - if (vertexTileId(v) == 31 and v.pos[1] == 90.0) found_upper_top = true; - } - try std.testing.expect(found_lower_top); - try std.testing.expect(found_upper_top); -} - -test "buildFromColumnSpans keeps LOD2 canopy spans detached" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .grass, 64.0, 0x2D591A); - - for (0..data.width * data.width) |i| { - data.vegetation[i] = .{ - .tree_coverage = 0.6, - .avg_tree_height = 7.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - } - data.clearVerticalSpans(0, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(0.0, 64.0, .grass, 0x2D591A))); - try std.testing.expect(data.setVerticalSpan(0, 0, 1, testSpan(66.0, 71.0, .leaves, 0x24941F))); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_folded = false; - var found_detached = false; - var found_compact_canopy = false; - for (verts) |v| { - if (v.pos[1] == 68.0 and vertexTileId(v) == Vertex.LOD_TILE_ID) found_folded = true; - if (v.pos[1] == 71.0) found_detached = true; - if (v.pos[1] == 71.0 and vertexTileId(v) == Vertex.LOD_TILE_ID and v.pos[0] > 0.0 and v.pos[0] < 4.0) found_compact_canopy = true; - } - try std.testing.expect(!found_folded); - try std.testing.expect(found_detached); - try std.testing.expect(found_compact_canopy); -} - -test "buildFromColumnSpans omits canopy from water cells" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - - var data = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 60.0, 0xD8C76D); - - const water_points = [_]u32{ 0, 1, data.width, data.width + 1 }; - for (water_points) |idx| { - data.water[idx] = .{ .is_surface = true, .surface_height = 63.0, .depth = 3.0, .coverage = 1.0 }; - } - data.clearVerticalSpans(0, 0); - try std.testing.expect(data.setVerticalSpan(0, 0, 0, testSpan(0.0, 60.0, .sand, 0xD8C76D))); - try std.testing.expect(data.setVerticalSpan(0, 0, 1, testSpan(62.0, 68.0, .leaves, 0x24941F))); - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromColumnSpans(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(mesh.water_vertex_count > 0); - for (verts[0..mesh.opaque_vertex_count]) |v| { - try std.testing.expect(vertexTileId(v) != 70); - } -} - -test "buildFromSimplifiedData renders LOD3 tree columns" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_mappings[@intFromEnum(BlockType.wood)] = .{ .top = 62, .bottom = 62, .side = 63 }; - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - - var data = try LODSimplifiedData.init(allocator, .lod3); - defer data.deinit(); - fillColumnSpanData(&data, .grass, 64.0, 0x2D591A); - data.vegetation[0] = .{ - .tree_coverage = 1.0, - .avg_tree_height = 8.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - - var mesh = LODMesh.init(allocator, .lod3); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_trunk_side = false; - var found_canopy = false; - var found_compact_canopy = false; - for (verts[0..mesh.opaque_vertex_count]) |v| { - if (vertexTileId(v) == 63 and v.pos[1] > 64.0) found_trunk_side = true; - if (vertexTileId(v) == Vertex.LOD_TILE_ID and v.pos[1] >= 72.0) found_canopy = true; - if (vertexTileId(v) == Vertex.LOD_TILE_ID and v.pos[1] >= 72.0 and v.pos[0] > 0.0 and v.pos[0] < 8.0) found_compact_canopy = true; - } - - try std.testing.expect(found_trunk_side); - try std.testing.expect(found_canopy); - try std.testing.expect(found_compact_canopy); -} - -test "buildFromSimplifiedData uses atlas tiles and world-scaled UVs" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(7)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = biome_mod.getBiomeColor(.plains); - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 32, 64, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - - try std.testing.expectEqual(@as(u16, 23), vertexTileId(verts[0])); - try std.testing.expectEqual(@as(f32, 32.0), @as(f32, verts[0].uv[0])); - try std.testing.expectEqual(@as(f32, 64.0), @as(f32, verts[0].uv[1])); - - var found_top_tile = false; - var found_side_tile = false; - for (verts) |v| { - const tile_id = vertexTileId(v); - try std.testing.expect(tile_id == 23 or tile_id == 24); - if (tile_id == 23) found_top_tile = true; - if (tile_id == 24) found_side_tile = true; - } - try std.testing.expect(found_top_tile); - try std.testing.expect(found_side_tile); -} - -test "buildFromSimplifiedData uses texture average for non-tint LOD tops" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.sand)] = .{ .top = 31, .bottom = 0, .side = 0 }; - atlas.tile_colors[@intFromEnum(BlockType.sand)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0xC2A85E)); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .beach; - data.top_blocks[i] = .sand; - data.colors[i] = 0xD8C76D; - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - - var top_tile_count: usize = 0; - for (verts) |v| { - const tile_id = vertexTileId(v); - try std.testing.expect(tile_id == 31 or tile_id == Vertex.LOD_TILE_ID); - if (tile_id == 31) { - top_tile_count += 1; - try std.testing.expectEqual(linearizeTestRgb(0xC2A85E), vertexRgb(v)); - } - } - try std.testing.expect(top_tile_count > 0); -} - -test "buildFromSimplifiedData keeps texture averages stable across world origins" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - fillColumnSpanData(&data, .sand, 64.0, 0xD8C76D); - - const expected = linearizeTestRgb(0xD8C76D); - const origins = [_][2]i32{ - .{ 0, 0 }, - .{ 37, -91 }, - .{ -256, 513 }, - .{ 1024, 33 }, - }; - - for (origins) |origin| { - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, origin[0], origin[1], &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var checked_top = false; - for (verts) |v| { - if (vertexTileId(v) == 51) { - checked_top = true; - try std.testing.expectEqual(expected, vertexRgb(v)); - } - } - try std.testing.expect(checked_top); - } -} - -test "buildFromSimplifiedData falls back to LOD tile for unmapped top blocks" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = biome_mod.getBiomeColor(.plains); - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - - for (verts) |v| try std.testing.expectEqual(@as(u16, Vertex.LOD_TILE_ID), vertexTileId(v)); -} - -test "buildFromSimplifiedData promotes mixed water cells to water material" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - atlas.tile_mappings[@intFromEnum(BlockType.water)] = .{ .top = 41, .bottom = 41, .side = 42 }; - atlas.tile_mappings[@intFromEnum(BlockType.sand)] = .{ .top = 51, .bottom = 52, .side = 55 }; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 63.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = biome_mod.getBiomeColor(.plains); - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - data.water[0] = .{ - .is_surface = true, - .surface_height = 63.0, - .depth = 8.0, - .coverage = 1.0, - }; - data.material_layers[0] = .{ - .surface = .water, - .subsurface = .sand, - .foundation = .stone, - }; - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - - var found_seafloor_top = false; - const water_start: usize = @intCast(mesh.opaque_vertex_count); - for (verts[0..mesh.opaque_vertex_count]) |v| { - if (vertexTileId(v) == 51) found_seafloor_top = true; - try std.testing.expect(vertexTileId(v) != 41); - } - try std.testing.expect(found_seafloor_top); - - var found_water_top = false; - for (verts[water_start..]) |v| { - if (vertexTileId(v) == 41 and v.pos[1] == 63.0) found_water_top = true; - } - try std.testing.expect(found_water_top); - - var found_floor_side = false; - for (verts[0..mesh.opaque_vertex_count]) |v| { - if (vertexTileId(v) == 55) { - found_floor_side = true; - break; - } - } - try std.testing.expect(found_floor_side); -} - -test "blockForLODQuad uses representative non-water surface" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod1); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - - data.material_layers[1].surface = .stone; - data.material_layers[data.width].surface = .stone; - data.material_layers[data.width + 1].surface = .stone; - - try std.testing.expectEqual(BlockType.stone, blockForLODQuad(&data, 0, 0)); -} - -test "coarse cell terrain height uses source sample instead of corner mean" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 96.0; - } - data.setHeight(0, 0, 64.0); - - try std.testing.expectEqual(@as(f32, 64.0), quantizedCellTerrainHeight(&data, 0, 0)); - try std.testing.expectEqual(@as(f32, 64.0), quantizedCellSurfaceHeight(&data, 0, 0)); -} - -test "LOD boundary height retains its authoritative source sample" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod1); - defer data.deinit(); - - for (0..data.width * data.width) |i| data.heightmap[i] = 10.0; - data.setHeight(0, 1, 100.0); - - try std.testing.expectEqual(@as(f32, 100.0), quantizedCellSurfaceHeight(&data, 0, 1)); -} - -test "representativeVegetationForLOD aggregates sparse coarse coverage" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - data.vegetation[0] = .{ - .tree_coverage = 0.1, - .avg_tree_height = 7.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - - const vegetation = representativeVegetationForLOD(&data, 0, 0, .lod2); - try std.testing.expectEqual(@as(f32, 0.025), vegetation.tree_coverage); - try std.testing.expect(vegetation.avg_tree_height >= 7.0); -} - -test "representativeVegetationForLOD suppresses subpixel horizon foliage" { - const allocator = std.testing.allocator; - var data = try LODSimplifiedData.init(allocator, .lod4); - defer data.deinit(); - data.vegetation[0] = .{ .tree_coverage = 0.1, .avg_tree_height = 8.0, .offset_x = 0.0, .offset_z = 0.0, .trunk = .wood, .leaves = .leaves }; - - const vegetation = representativeVegetationForLOD(&data, 0, 0, .lod4); - try std.testing.expectEqual(@as(f32, 0.0), vegetation.tree_coverage); - try std.testing.expectEqual(BlockType.air, vegetation.leaves); -} - -test "buildFromSimplifiedData uses averaged color tile for far LOD tops" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(7)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - atlas.tile_colors[@intFromEnum(BlockType.grass)] = .{ .top = linearizeTestRgb(0x80C060), .bottom = linearizeTestRgb(0x805030), .side = linearizeTestRgb(0x607040) }; - - var data = try LODSimplifiedData.init(allocator, .lod3); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = 0x3A7D42; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - - var mesh = LODMesh.init(allocator, .lod3); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - try std.testing.expectEqual(@as(u16, Vertex.LOD_TILE_ID), vertexTileId(verts[0])); - try std.testing.expect(vertexRgb(verts[0]) != 0x3A7D42); -} - -test "buildFromSimplifiedData tints atlas average for grass tops" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_colors[@intFromEnum(BlockType.grass)] = .{ .top = linearizeTestRgb(0x80C060), .bottom = linearizeTestRgb(0x805030), .side = linearizeTestRgb(0x607040) }; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = 0x3A7D42; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - const rgb = vertexRgb(verts[0]); - try std.testing.expect(rgb != 0x3A7D42); - try std.testing.expect(((rgb >> 8) & 0xFF) > ((rgb >> 16) & 0xFF)); -} - -test "buildFromSimplifiedData uses chunk grass tint for grass tops" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_colors[@intFromEnum(BlockType.grass)] = .{ .top = linearizeTestRgb(0x80C060), .bottom = linearizeTestRgb(0x805030), .side = linearizeTestRgb(0x607040) }; - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = 0xFFFFFF; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - const expected = applyTextureLuminance(biome_mod.getGrassTintColor(.plains), .grass, .top, &atlas); - try std.testing.expectEqual(expected, vertexRgb(verts[0])); -} - -test "addTreeCanopyColumn uses chunk foliage tint" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_colors[@intFromEnum(BlockType.leaves)] = TextureAtlas.BlockTileColor.uniform(linearizeTestRgb(0x4C8F38)); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .forest; - } - - var vertices = std.ArrayListUnmanaged(Vertex).empty; - defer vertices.deinit(allocator); - const vegetation: world_core.LODVegetationHint = .{ - .tree_coverage = 0.35, - .avg_tree_height = 8.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - - try addTreeCanopyColumn(allocator, &vertices, &data, 0, 0, .lod2, 0.0, 0.0, 8.0, 64.0, 67.0, 72.0, vegetation, &atlas, 0, 0); - - try std.testing.expect(vertices.items.len >= 6); - const expected = applyTextureLuminance(biome_mod.getFoliageTintColor(.forest), .leaves, .top, &atlas); - for (vertices.items[0..6]) |v| { - try std.testing.expectEqual(expected, vertexRgb(v)); - } -} - -test "buildFromSimplifiedData uses single source sample for fine LOD tops" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(7)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - - var data = try LODSimplifiedData.init(allocator, .lod1); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 96.0; - data.biomes[i] = .plains; - data.top_blocks[i] = .grass; - data.colors[i] = 0xFFFFFF; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - } - data.heightmap[0] = 64.0; - data.colors[0] = 0x123456; - - var mesh = LODMesh.init(allocator, .lod1); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - try std.testing.expectEqual(@as(f32, 64.0), verts[0].pos[1]); - const expected = applyTextureLuminance(biome_mod.getGrassTintColor(.plains), .grass, .top, &atlas); - try std.testing.expectEqual(expected, vertexRgb(verts[0])); -} - -test "buildFromSimplifiedData keeps mixed water cells on one flat surface" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - - var data = try LODSimplifiedData.init(allocator, .lod2); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 58.0; - data.biomes[i] = .ocean; - data.top_blocks[i] = .sand; - data.colors[i] = 0x3355AA; - data.material_layers[i] = .{ .surface = .sand, .subsurface = .sand, .foundation = .stone }; - data.water[i] = .{ .is_surface = true, .surface_height = 63.0, .depth = 5.0, .coverage = 1.0 }; - } - data.water[0].surface_height = 62.0; - data.water[1].surface_height = 64.0; - data.water[data.width].surface_height = 63.0; - data.water[data.width + 1].surface_height = 63.0; - - var mesh = LODMesh.init(allocator, .lod2); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - const water_start: usize = @intCast(mesh.opaque_vertex_count); - try std.testing.expect(water_start < verts.len); - for (verts[water_start .. water_start + 6]) |v| { - try std.testing.expectEqual(@as(f32, 64.0), v.pos[1]); - } -} - -test "adjacent ocean regions normalize water tops to shared sea level" { - const allocator = std.testing.allocator; - - var left = try LODSimplifiedData.init(allocator, .lod2); - defer left.deinit(); - var right = try LODSimplifiedData.init(allocator, .lod2); - defer right.deinit(); - - for (0..left.width * left.width) |i| { - left.heightmap[i] = 58.0; - left.biomes[i] = .ocean; - left.top_blocks[i] = .water; - left.material_layers[i] = .{ .surface = .water, .subsurface = .sand, .foundation = .stone }; - left.water[i] = .{ .is_surface = true, .surface_height = 62.0, .depth = 4.0, .coverage = 1.0 }; - - right.heightmap[i] = 58.0; - right.biomes[i] = .ocean; - right.top_blocks[i] = .water; - right.material_layers[i] = .{ .surface = .water, .subsurface = .sand, .foundation = .stone }; - right.water[i] = .{ .is_surface = true, .surface_height = 63.0, .depth = 5.0, .coverage = 1.0 }; - } - - try std.testing.expectEqual(@as(f32, 64.0), quantizedWaterSurfaceHeightForCell(&left, left.width - 2, 0, .lod2)); - try std.testing.expectEqual(@as(f32, 64.0), quantizedWaterSurfaceHeightForCell(&right, 0, 0, .lod2)); -} - -test "buildFromSimplifiedData renders voxel tree columns for fine vegetation hints" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - atlas.tile_mappings[@intFromEnum(BlockType.wood)] = .{ .top = 71, .bottom = 71, .side = 72 }; - - var data = try LODSimplifiedData.init(allocator, .lod1); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .forest; - data.top_blocks[i] = .grass; - data.colors[i] = 0x2D591A; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - data.vegetation[i] = .{ - .tree_coverage = 0.6, - .avg_tree_height = 7.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - } - - var mesh = LODMesh.init(allocator, .lod1); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_leaf_column_top = false; - for (verts) |v| { - if (vertexTileId(v) == Vertex.LOD_TILE_ID and v.pos[1] == 71.0 and vertexRgb(v) != 0xFFFFFF) { - found_leaf_column_top = true; - break; - } - } - try std.testing.expect(found_leaf_column_top); -} - -test "buildFromSimplifiedData renders far vegetation as separate tree silhouettes" { - const allocator = std.testing.allocator; - var atlas = testAtlas(allocator); - atlas.tile_mappings[@intFromEnum(BlockType.leaves)] = .{ .top = 70, .bottom = 70, .side = 70 }; - - var data = try LODSimplifiedData.init(allocator, .lod4); - defer data.deinit(); - - for (0..data.width * data.width) |i| { - data.heightmap[i] = 64.0; - data.biomes[i] = .forest; - data.top_blocks[i] = .grass; - data.colors[i] = 0x2D591A; - data.material_layers[i] = .{ - .surface = .grass, - .subsurface = .dirt, - .foundation = .stone, - }; - data.vegetation[i] = .{ - .tree_coverage = 0.6, - .avg_tree_height = 7.0, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .wood, - .leaves = .leaves, - }; - } - - var mesh = LODMesh.init(allocator, .lod4); - defer mesh.deinit(testResources()); - - try mesh.buildFromSimplifiedData(&data, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - var found_ground_top = false; - var found_canopy_top = false; - var found_compact_canopy = false; - for (verts) |v| { - if (v.pos[1] == 64.0 and vertexRgb(v) == 0x2D591A) found_ground_top = true; - if (v.pos[1] == 71.0 and vertexTileId(v) == Vertex.LOD_TILE_ID and vertexRgb(v) != 0x2D591A) found_canopy_top = true; - if (v.pos[1] == 71.0 and vertexTileId(v) == Vertex.LOD_TILE_ID and v.pos[0] > 0.0 and v.pos[0] < 8.0) found_compact_canopy = true; - } - try std.testing.expect(found_ground_top); - try std.testing.expect(found_canopy_top); - try std.testing.expect(found_compact_canopy); -} - -test "buildFromSimplifiedData adds internal faces for steep LOD height deltas" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.stone)] = .{ .top = 11, .bottom = 12, .side = 13 }; - - var flat_data = try LODSimplifiedData.init(allocator, .lod3); - defer flat_data.deinit(); - var cliff_data = try LODSimplifiedData.init(allocator, .lod3); - defer cliff_data.deinit(); - - for (0..flat_data.width * flat_data.width) |i| { - flat_data.heightmap[i] = 64.0; - flat_data.biomes[i] = .mountains; - flat_data.top_blocks[i] = .stone; - flat_data.colors[i] = 0x808080; - flat_data.material_layers[i] = .{ - .surface = .stone, - .subsurface = .stone, - .foundation = .stone, - }; - cliff_data.heightmap[i] = flat_data.heightmap[i]; - cliff_data.biomes[i] = flat_data.biomes[i]; - cliff_data.top_blocks[i] = flat_data.top_blocks[i]; - cliff_data.colors[i] = flat_data.colors[i]; - cliff_data.material_layers[i] = flat_data.material_layers[i]; - } - - cliff_data.setHeight(15, 15, 96.0); - cliff_data.setHeight(16, 15, 96.0); - cliff_data.setHeight(15, 16, 96.0); - cliff_data.setHeight(16, 16, 96.0); - - var flat_mesh = LODMesh.init(allocator, .lod3); - defer flat_mesh.deinit(testResources()); - try flat_mesh.buildFromSimplifiedData(&flat_data, 0, 0, &atlas); - const flat_verts = flat_mesh.pending_vertices orelse return error.TestExpectedEqual; - - var cliff_mesh = LODMesh.init(allocator, .lod3); - defer cliff_mesh.deinit(testResources()); - try cliff_mesh.buildFromSimplifiedData(&cliff_data, 0, 0, &atlas); - const cliff_verts = cliff_mesh.pending_vertices orelse return error.TestExpectedEqual; - - try std.testing.expect(cliff_verts.len > flat_verts.len); -} - -test "buildFromHeightmap uses biome atlas tiles" { - const allocator = std.testing.allocator; - const MAX_BLOCK_TYPES = world_core.MAX_BLOCK_TYPES; - - var atlas = TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(7)} ** MAX_BLOCK_TYPES, - }; - atlas.tile_mappings[@intFromEnum(BlockType.grass)] = .{ .top = 23, .bottom = 2, .side = 24 }; - - const width: u32 = 4; - const count = width * width; - const heightmap = [_]f32{64.0} ** count; - const biomes = [_]BiomeId{.plains} ** count; - - var mesh = LODMesh.init(allocator, .lod1); - defer mesh.deinit(testResources()); - - try mesh.buildFromHeightmap(&heightmap, &biomes, width, 0, 0, &atlas); - - const verts = mesh.pending_vertices orelse return error.TestExpectedEqual; - try std.testing.expect(verts.len > 0); - - var found_top_tile = false; - for (verts) |v| { - if (vertexTileId(v) == 23) { - found_top_tile = true; - break; - } - } - try std.testing.expect(found_top_tile); -} diff --git a/modules/world-lod/src/lod_renderer.zig b/modules/world-lod/src/lod_renderer.zig deleted file mode 100644 index 686ba069..00000000 --- a/modules/world-lod/src/lod_renderer.zig +++ /dev/null @@ -1,3319 +0,0 @@ -//! LOD Renderer - handles culling and drawing of LOD meshes using Multi-Draw Indirect. -//! -//! This module is responsible for rendering distant terrain chunks (LOD1-LOD3) -//! using an instanced rendering approach. It receives prepared mesh and region -//! data from LODManager via the LODGPUBridge abstraction. -//! -//! ## Multi-Draw Indirect (MDI) Rendering -//! -//! The renderer uses instance buffers to batch-draw multiple LOD regions: -//! - Per-frame instance data (position, color, mask radius) is accumulated -//! - Data is uploaded to GPU storage buffers (double-buffered per frame) -//! - RHI renders all visible regions in minimal draw calls -//! -//! ## GPU Data Flow -//! -//! ``` -//! LODManager -> MeshMap/RegionMap -> LODRenderer.render() -> InstanceBuffer -> GPU -//! ``` -//! -//! The LODGPUBridge and LODRenderInterface types abstract the data transfer, -//! allowing LODManager to remain decoupled from rendering concerns. -//! -//! ## Frustum Culling -//! -//! Visible regions are filtered by frustum culling before adding to the draw -//! list. Each LODChunk has conservative bounds that are tested against the camera frustum. -//! Optional ChunkChecker callback allows additional visibility filtering. - -const std = @import("std"); -const lod_chunk = @import("lod_chunk.zig"); -const ChunkBounds = lod_chunk.ChunkBounds; -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODConfig = lod_chunk.LODConfig; -const ILODConfig = lod_chunk.ILODConfig; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODRegionKeyContext = lod_chunk.LODRegionKeyContext; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const LODMeshResources = @import("lod_mesh.zig").LODMeshResources; -const LODStagingCost = @import("lod_mesh_resources.zig").LODStagingCost; -const LODVertexPool = @import("lod_vertex_pool.zig").LODVertexPool; -const CompactLODPool = @import("lod_compact_pool.zig").CompactLODPool; -const CHUNK_SIZE_X = @import("world-core").CHUNK_SIZE_X; -const CHUNK_SIZE_Z = @import("world-core").CHUNK_SIZE_Z; -const worldToChunkFromFloat = @import("world-core").worldToChunkFromFloat; - -const lod_gpu = @import("lod_upload_queue.zig"); -const LODGPUBridge = lod_gpu.LODGPUBridge; -const LODRenderInterface = lod_gpu.LODRenderInterface; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const LODRendererMemoryStats = lod_gpu.LODRendererMemoryStats; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; -const ChunkChecker = lod_gpu.ChunkChecker; -const LODStats = @import("lod_stats.zig").LODStats; -const LODProfilingCollector = @import("lod_stats.zig").LODProfilingCollector; - -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const Frustum = @import("engine-math").Frustum; -const rhi_types = @import("engine-rhi"); -const engine_core = @import("engine-core"); -const log = engine_core.log; -const build_options = @import("world_lod_options"); -const LODCullCandidate = rhi_types.LODCullCandidate; -const LODCullDispatch = rhi_types.LODCullDispatch; -const ILODCullingSystem = rhi_types.ILODCullingSystem; - -const CHUNK_COVERAGE_PADDING: i32 = 1; -const LOD_UNMASKED_SENTINEL: f32 = 0.5; -// Positive radii retain the legacy two-chunk overlap. A negative radius marks -// an exact contiguous ready-detail disk without consuming float precision in a -// fractional tag at large render distances. -const COMPACT_GRID_WIDTHS = [_]u32{ 5, 9, 17, 33, 65, 129 }; - -fn conservativeChunkDiskMaskRadius(mask_radius: f32) f32 { - return if (mask_radius >= 1.0) -mask_radius else LOD_UNMASKED_SENTINEL; -} - -fn readyDiskMaskRadius(ready_radius: i32) f32 { - if (ready_radius < 0) return LOD_UNMASKED_SENTINEL; - const radius_blocks = @as(f32, @floatFromInt(@as(i64, ready_radius) * CHUNK_SIZE_X)); - return -@max(radius_blocks, 1.0); -} - -fn contiguousReadyDiskRadius(checker: ?ChunkChecker, checker_ctx: ?*anyopaque, camera_chunk_x: i32, camera_chunk_z: i32, max_radius: i32) i32 { - return expandContiguousReadyDiskRadius(checker, checker_ctx, camera_chunk_x, camera_chunk_z, -1, max_radius); -} - -fn readyDetailChunkAtOffset(check: ChunkChecker, ctx: *anyopaque, camera_chunk_x: i32, camera_chunk_z: i32, dx: i64, dz: i64) bool { - const cx = @as(i64, camera_chunk_x) + dx; - const cz = @as(i64, camera_chunk_z) + dz; - if (cx < std.math.minInt(i32) or cx > std.math.maxInt(i32) or cz < std.math.minInt(i32) or cz > std.math.maxInt(i32)) return false; - return check(@intCast(cx), @intCast(cz), ctx); -} - -fn expandContiguousReadyDiskRadius(checker: ?ChunkChecker, checker_ctx: ?*anyopaque, camera_chunk_x: i32, camera_chunk_z: i32, known_ready_radius: i32, max_radius: i32) i32 { - const check = checker orelse return -1; - const ctx = checker_ctx orelse return -1; - if (known_ready_radius >= max_radius) return max_radius; - - var radius = @as(i64, @max(known_ready_radius + 1, 0)); - const max_radius_i64 = @as(i64, @max(max_radius, 0)); - while (radius <= max_radius_i64) : (radius += 1) { - const radius_sq = radius * radius; - const previous_radius = radius - 1; - const previous_sq = previous_radius * previous_radius; - var max_dx = radius; - var previous_max_dx = previous_radius; - var abs_dz: i64 = 0; - while (abs_dz <= radius) : (abs_dz += 1) { - while (max_dx * max_dx + abs_dz * abs_dz > radius_sq) max_dx -= 1; - if (abs_dz <= previous_radius) { - while (previous_max_dx * previous_max_dx + abs_dz * abs_dz > previous_sq) previous_max_dx -= 1; - } else { - previous_max_dx = -1; - } - - const first_new_x = previous_max_dx + 1; - var abs_dx = first_new_x; - while (abs_dx <= max_dx) : (abs_dx += 1) { - if (!readyDetailChunkAtOffset(check, ctx, camera_chunk_x, camera_chunk_z, abs_dx, abs_dz)) return @intCast(radius - 1); - if (abs_dx > 0 and !readyDetailChunkAtOffset(check, ctx, camera_chunk_x, camera_chunk_z, -abs_dx, abs_dz)) return @intCast(radius - 1); - if (abs_dz > 0) { - if (!readyDetailChunkAtOffset(check, ctx, camera_chunk_x, camera_chunk_z, abs_dx, -abs_dz)) return @intCast(radius - 1); - if (abs_dx > 0 and !readyDetailChunkAtOffset(check, ctx, camera_chunk_x, camera_chunk_z, -abs_dx, -abs_dz)) return @intCast(radius - 1); - } - } - } - } - return @max(max_radius, 0); -} - -fn selectLODDescriptorStream(render_ctx: anytype, layer: LODRenderLayer, compact: bool, gpu: bool) void { - if (comptime !@hasDecl(@TypeOf(render_ctx), "setLODDescriptorStream")) return; - const stream: rhi_types.LODDescriptorStream = switch (layer) { - .terrain => if (compact) if (gpu) .terrain_compact_gpu else .terrain_compact_direct else if (gpu) .terrain_standard_gpu else .terrain_standard_direct, - .fluid => if (compact) if (gpu) .water_compact_gpu else .water_compact_direct else if (gpu) .water_standard_gpu else .water_standard_direct, - }; - render_ctx.setLODDescriptorStream(stream); -} - -fn compactGridIndexCount(width: u32, include_skirts: bool) usize { - if (width < 2) return 0; - const top = @as(usize, width - 1) * @as(usize, width - 1) * 6; - const skirts = if (include_skirts) @as(usize, width - 1) * 4 * 6 else 0; - return top + skirts; -} - -fn compactGridIndices(allocator: std.mem.Allocator, width: u32, include_skirts: bool) ![]u32 { - if (width < 2) return error.InvalidGrid; - const result = try allocator.alloc(u32, compactGridIndexCount(width, include_skirts)); - var out: usize = 0; - var z: u32 = 0; - while (z + 1 < width) : (z += 1) { - var x: u32 = 0; - while (x + 1 < width) : (x += 1) { - const a = z * width + x; - const b = a + 1; - const c = a + width; - const d = c + 1; - result[out + 0] = a; - result[out + 1] = c; - result[out + 2] = b; - result[out + 3] = b; - result[out + 4] = c; - result[out + 5] = d; - out += 6; - } - } - - if (include_skirts) { - const top_vertex_count = width * width; - const Edge = struct { - fn append(result_slice: []u32, output: *usize, top_a: u32, top_b: u32, bottom_a: u32, bottom_b: u32) void { - result_slice[output.* + 0] = top_a; - result_slice[output.* + 1] = bottom_a; - result_slice[output.* + 2] = top_b; - result_slice[output.* + 3] = top_b; - result_slice[output.* + 4] = bottom_a; - result_slice[output.* + 5] = bottom_b; - output.* += 6; - } - }; - for (0..width - 1) |edge_index| { - const i: u32 = @intCast(edge_index); - // North and south. - Edge.append(result, &out, i, i + 1, top_vertex_count + i, top_vertex_count + i + 1); - const south_top = (width - 1) * width + i; - const south_bottom = top_vertex_count + width + i; - Edge.append(result, &out, south_top + 1, south_top, south_bottom + 1, south_bottom); - // West and east. - const west_top = i * width; - const west_bottom = top_vertex_count + width * 2 + i; - Edge.append(result, &out, west_top + width, west_top, west_bottom + 1, west_bottom); - const east_top = i * width + width - 1; - const east_bottom = top_vertex_count + width * 3 + i; - Edge.append(result, &out, east_top, east_top + width, east_bottom, east_bottom + 1); - } - } - std.debug.assert(out == result.len); - return result; -} - -fn compactGridVariant(width: u32) ?usize { - for (COMPACT_GRID_WIDTHS, 0..) |candidate, index| { - if (candidate == width) return index; - } - return null; -} - -const RenderDiag = struct { - meshes_seen: u32 = 0, - missing_region: u32 = 0, - not_ready: u32 = 0, - bad_state: u32 = 0, - covered_finer_lod: u32 = 0, - out_of_range: u32 = 0, - covered_chunks: u32 = 0, - frustum_culled: u32 = 0, - drawn: u32 = 0, -}; - -/// A compact tile may be selected before render-graph resources or descriptor -/// snapshots become usable. That is a retryable availability state, not a -/// backend submission failure. -const CompactRenderResult = enum { - drawn, - unavailable, - backend_failed, -}; - -/// Value-only result of the frame visibility projection. It deliberately holds -/// no mesh or region pointer: the projection can survive the terrain pass and -/// is revalidated under the manager's shared lock before a later water pass. -const VisibleRegion = struct { - key: LODRegionKey, - model: Mat4, - mask_radius: f32, - lod_fade: f32, -}; - -const MAX_LOD_MDI_REGIONS: usize = 2048; - -/// Expected RHI interface for LODRenderer: -/// - createBuffer(size: usize, usage: BufferUsage) !BufferHandle -/// - destroyBuffer(handle: BufferHandle) void -/// - getFrameIndex() usize -/// - setLODInstanceBuffer(handle: BufferHandle) void -/// - setModelMatrix(model: Mat4, color: Vec3, mask_radius: f32) void -/// - draw(handle: BufferHandle, count: u32, mode: DrawMode) void -pub fn LODRenderer(comptime RHI: type) type { - return struct { - const Self = @This(); - - allocator: std.mem.Allocator, - rhi: RHI, - - // MDI Resources (Moved from LODManager) - instance_data: std.ArrayListUnmanaged(rhi_types.InstanceData), - draw_list: std.ArrayListUnmanaged(*LODMesh), - projection_regions: std.ArrayListUnmanaged(VisibleRegion), - cached_ready_disk_camera_x: i32, - cached_ready_disk_camera_z: i32, - cached_ready_disk_max_radius: i32, - cached_ready_disk_radius: i32, - cached_ready_disk_checker: ?ChunkChecker, - cached_ready_disk_checker_ctx: ?*anyopaque, - projection_frame: ?u64, - draw_commands: [LODLevel.count]std.ArrayListUnmanaged(rhi_types.DrawIndirectCommand), - instance_buffers: [rhi_types.MAX_FRAMES_IN_FLIGHT]rhi_types.BufferHandle, - indirect_buffers: [rhi_types.MAX_FRAMES_IN_FLIGHT]rhi_types.BufferHandle, - vertex_pools: [LODLevel.count]LODVertexPool, - compact_pool: CompactLODPool, - /// Per compact LOD: terrain grid with edge skirts, then water top grid. - /// Exact-width topology for each compact LOD and layer. A prefix of a - /// wider grid is not valid topology for a decimated tile: its row - /// stride remains the wider grid's stride and can feed invalid vertex - /// IDs to the water vertex-pulling path on RADV. - compact_index_buffers: [2][2][COMPACT_GRID_WIDTHS.len]rhi_types.BufferHandle, - /// Static index uploads are recorded in the first LOD render frame. - /// They become drawable only after that frame has been submitted. - compact_index_upload_frame: ?u64, - compact_index_init_failed: bool, - frame_index: usize, - frame_serial: u64, - enable_mdi: bool, - gpu_culling_requested: bool, - gpu_culling: ?ILODCullingSystem, - gpu_candidates: std.ArrayListUnmanaged(LODCullCandidate), - /// Keys parallel to `gpu_candidates`. Ineligible meshes stay on the - /// CPU path instead of disabling an otherwise valid GPU batch. - gpu_candidate_keys: std.ArrayListUnmanaged(LODRegionKey), - /// Compact submissions that need a parent fallback after the compact - /// timing scope has ended. This keeps expanded fallback draws out of - /// compact GPU timing evidence. - compact_fallback_regions: std.ArrayListUnmanaged(VisibleRegion), - gpu_culling_ready_frame: ?u64, - gpu_culling_threshold: usize, - gpu_culling_overflow_count: u32, - /// GPU culling prepares one candidate projection for terrain and water. - /// Keep its benchmark submission counter frame-scoped so those paired - /// streams are not reported as two independent culling submissions. - gpu_culling_submitted_frame: ?u64, - - fn uploadCompactIndexBuffer(allocator: std.mem.Allocator, resources: anytype, handle: rhi_types.BufferHandle, width: u32, include_skirts: bool) !void { - const indices = try compactGridIndices(allocator, width, include_skirts); - defer allocator.free(indices); - try resources.uploadBuffer(handle, std.mem.sliceAsBytes(indices)); - } - - /// Record static compact topology uploads only after a render frame has - /// opened its staging/transfer slot. Uploads queued during world setup - /// can otherwise be discarded when the first frame resets that slot. - fn ensureCompactIndexBuffers(self: *Self, frame_serial: u64) bool { - if (self.compact_index_upload_frame) |upload_frame| return frame_serial > upload_frame; - if (self.compact_index_init_failed) return false; - - const resources = if (@hasDecl(RHI, "resourceManager")) self.rhi.resourceManager() else self.rhi; - inline for (.{ LODLevel.lod3, LODLevel.lod4 }, 0..) |lod, idx| { - const max_width = @import("world-core").LODSimplifiedData.getGridSize(lod); - inline for (.{ true, false }, 0..) |include_skirts, layer_idx| for (COMPACT_GRID_WIDTHS, 0..) |width, width_idx| { - if (width > max_width) continue; - const handle = self.compact_index_buffers[idx][layer_idx][width_idx]; - if (handle == 0) { - self.compact_index_init_failed = true; - log.log.err("Compact LOD index topology is missing LOD{} width={} layer={}", .{ @intFromEnum(lod), width, layer_idx }); - return false; - } - uploadCompactIndexBuffer(self.allocator, resources, handle, width, include_skirts) catch |err| { - log.log.errWithTrace("Failed to upload compact LOD index topology: {}", .{err}); - return false; - }; - }; - } - self.compact_index_upload_frame = frame_serial; - return false; - } - - /// Allocates LOD renderer GPU buffers and per-frame indirect draw resources. - /// The renderer owns created buffers until `deinit`; allocation failures are returned to the caller. - pub fn init(allocator: std.mem.Allocator, rhi: RHI) !*Self { - const renderer = try allocator.create(Self); - errdefer allocator.destroy(renderer); - - var instance_buffers = [_]rhi_types.BufferHandle{0} ** rhi_types.MAX_FRAMES_IN_FLIGHT; - var indirect_buffers = [_]rhi_types.BufferHandle{0} ** rhi_types.MAX_FRAMES_IN_FLIGHT; - const resources = if (@hasDecl(RHI, "resourceManager")) rhi.resourceManager() else rhi; - errdefer { - for (0..rhi_types.MAX_FRAMES_IN_FLIGHT) |i| { - if (instance_buffers[i] != 0) resources.destroyBuffer(instance_buffers[i]); - if (indirect_buffers[i] != 0) resources.destroyBuffer(indirect_buffers[i]); - } - } - for (0..rhi_types.MAX_FRAMES_IN_FLIGHT) |i| { - instance_buffers[i] = try resources.createBuffer(MAX_LOD_MDI_REGIONS * @sizeOf(rhi_types.InstanceData), .storage); - indirect_buffers[i] = try resources.createBuffer(MAX_LOD_MDI_REGIONS * @sizeOf(rhi_types.DrawIndirectCommand), .indirect); - } - - var draw_commands: [LODLevel.count]std.ArrayListUnmanaged(rhi_types.DrawIndirectCommand) = undefined; - for (&draw_commands) |*commands| commands.* = .empty; - - var vertex_pools: [LODLevel.count]LODVertexPool = undefined; - for (0..LODLevel.count) |i| { - vertex_pools[i] = LODVertexPool.init(allocator, @enumFromInt(@as(u3, @intCast(i))), 8 * 1024 * 1024); - } - var compact_index_buffers = std.mem.zeroes([2][2][COMPACT_GRID_WIDTHS.len]rhi_types.BufferHandle); - errdefer for (&compact_index_buffers) |*lod_handles| for (lod_handles) |layer_handles| for (layer_handles) |handle| if (handle != 0) resources.destroyBuffer(handle); - if (comptime @hasDecl(RHI, "resourceManager") or @hasDecl(RHI, "uploadBuffer") or @hasDecl(RHI, "updateBuffer")) { - inline for (.{ LODLevel.lod3, LODLevel.lod4 }, 0..) |lod, idx| { - const max_width = @import("world-core").LODSimplifiedData.getGridSize(lod); - inline for (.{ true, false }, 0..) |include_skirts, layer_idx| for (COMPACT_GRID_WIDTHS, 0..) |width, width_idx| { - _ = include_skirts; - if (width > max_width) continue; - const byte_count = compactGridIndexCount(width, layer_idx == 0) * @sizeOf(u32); - compact_index_buffers[idx][layer_idx][width_idx] = try resources.createBuffer(byte_count, .index); - }; - } - } - - const gpu_culling_requested = gpuCullingRequested(build_options.benchmark_gpu_culling, engine_core.envFlag("ZIGCRAFT_LOD_GPU_CULLING", false)); - if (engine_core.envFlag("ZIGCRAFT_LOD_COMPACT_DIAG", false)) { - std.debug.print("LOD_COMPACT: event=gpu_culling_init requested={} env={s}\n", .{ gpu_culling_requested, engine_core.getenv("ZIGCRAFT_LOD_GPU_CULLING") orelse "unset" }); - } - var gpu_culling: ?ILODCullingSystem = null; - if (gpu_culling_requested and @hasDecl(RHI, "createLODCullingSystem")) { - gpu_culling = rhi.createLODCullingSystem(allocator, MAX_LOD_MDI_REGIONS) catch |err| blk: { - log.log.warn("LOD GPU culling unavailable ({}); CPU fallback active", .{err}); - break :blk null; - }; - } - renderer.* = .{ - .allocator = allocator, - .rhi = rhi, - .instance_data = .empty, - .draw_list = .empty, - .projection_regions = .empty, - .cached_ready_disk_camera_x = 0, - .cached_ready_disk_camera_z = 0, - .cached_ready_disk_max_radius = -1, - .cached_ready_disk_radius = -1, - .cached_ready_disk_checker = null, - .cached_ready_disk_checker_ctx = null, - .projection_frame = null, - .draw_commands = draw_commands, - .instance_buffers = instance_buffers, - .indirect_buffers = indirect_buffers, - .vertex_pools = vertex_pools, - .compact_pool = CompactLODPool.init(allocator), - .compact_index_buffers = compact_index_buffers, - .compact_index_upload_frame = null, - .compact_index_init_failed = false, - .frame_index = 0, - .frame_serial = 0, - .enable_mdi = !engine_core.envFlag("ZIGCRAFT_DISABLE_LOD_MDI", false), - .gpu_culling_requested = gpu_culling_requested, - .gpu_culling = gpu_culling, - .gpu_candidates = .empty, - .gpu_candidate_keys = .empty, - .compact_fallback_regions = .empty, - .gpu_culling_ready_frame = null, - .gpu_culling_threshold = gpuCullingThreshold(), - .gpu_culling_overflow_count = 0, - .gpu_culling_submitted_frame = null, - }; - - if (!renderer.enable_mdi) { - log.log.info("LOD MDI disabled by ZIGCRAFT_DISABLE_LOD_MDI", .{}); - } - - return renderer; - } - - /// Destroys GPU buffers owned by the LOD renderer and releases CPU-side storage. - /// Call once when the world LOD renderer is no longer used. - pub fn deinit(self: *Self) void { - self.rhi.waitIdle(); - const resources = if (@hasDecl(RHI, "resourceManager")) self.rhi.resourceManager() else self.rhi; - for (0..rhi_types.MAX_FRAMES_IN_FLIGHT) |i| { - if (self.instance_buffers[i] != 0) { - resources.destroyBuffer(self.instance_buffers[i]); - } - if (self.indirect_buffers[i] != 0) { - resources.destroyBuffer(self.indirect_buffers[i]); - } - } - const mesh_resources = LODMeshResources.fromProvider(RHI, &self.rhi); - for (0..LODLevel.count) |i| { - self.vertex_pools[i].deinit(mesh_resources); - } - self.compact_pool.deinit(mesh_resources); - for (self.compact_index_buffers) |lod_handles| for (lod_handles) |layer_handles| for (layer_handles) |handle| if (handle != 0) resources.destroyBuffer(handle); - self.instance_data.deinit(self.allocator); - self.draw_list.deinit(self.allocator); - self.projection_regions.deinit(self.allocator); - self.gpu_candidates.deinit(self.allocator); - self.gpu_candidate_keys.deinit(self.allocator); - self.compact_fallback_regions.deinit(self.allocator); - if (self.gpu_culling) |gpu| gpu.deinit(); - for (&self.draw_commands) |*commands| commands.deinit(self.allocator); - self.allocator.destroy(self); - } - - /// Render all LOD meshes using explicitly provided data. - /// - /// `frame_serial` is supplied by WorldRenderer and advances only from - /// its `beginFrame`. Visibility and full-chunk coverage are projected - /// once for that frame; terrain and water then consume value-only - /// entries without retaining map-owned mesh pointers. - pub fn renderFrame( - self: *Self, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - detail_render_radius: i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - self.frame_serial = frame_serial; - _ = self.ensureCompactIndexBuffers(frame_serial); - const query = if (@hasDecl(RHI, "query")) self.rhi.query() else self.rhi; - self.frame_index = query.getFrameIndex(); - // Reusing a frame slot means the RHI has completed that slot's - // fence. The compact pool additionally requires a newer monotonic - // frame serial before it returns any retired range to allocation. - self.compact_pool.collectRetired(frame_serial, self.frame_index); - for (&self.vertex_pools) |*pool| pool.collectRetired(frame_serial, self.frame_index); - if (self.projection_frame == null or self.projection_frame.? != frame_serial) { - const timer = if (profiling) |profile| profile.begin() else null; - defer if (profiling) |profile| profile.end(.visibility, timer); - self.buildVisibilityProjection(meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, stats, profiling) catch |err| { - log.log.errWithTrace("Failed to project LOD visibility: {}", .{err}); - return; - }; - self.projection_frame = frame_serial; - } - if (!self.renderProjectedLayer(meshes, layer, stats, profiling)) { - // GPU submission can still fail after the compute prepass (for - // example if a pooled buffer becomes unavailable). Never feed - // the broad GPU candidate projection into the CPU renderer. - self.gpu_culling_ready_frame = null; - const timer = if (profiling) |profile| profile.begin() else null; - defer if (profiling) |profile| profile.end(.visibility, timer); - self.buildVisibilityProjection(meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, stats, profiling) catch |err| { - log.log.errWithTrace("Failed to rebuild CPU LOD visibility: {}", .{err}); - self.projection_frame = null; - return; - }; - self.projection_frame = frame_serial; - _ = self.renderProjectedLayer(meshes, layer, stats, profiling); - } - } - - /// Records the LOD compute pass before the render graph enters a graphics - /// pass. Hierarchy/readiness/chunk-coverage stay CPU authoritative. - pub fn prepareFrame( - self: *Self, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - max_distance_chunks: ?i32, - detail_render_radius: i32, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - self.updateGpuCullingProfiling(profiling); - const gpu = self.gpu_culling orelse return; - if (!self.gpu_culling_requested or self.projection_frame == frame_serial) return; - const visibility_timer = if (profiling) |profile| profile.begin() else null; - defer if (profiling) |profile| profile.end(.visibility, visibility_timer); - self.buildVisibilityProjection(meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, false, max_distance_chunks, detail_render_radius, stats, profiling) catch |err| { - log.log.err("LOD GPU culling projection failed: {}", .{err}); - return; - }; - self.projection_frame = frame_serial; - self.gpu_candidates.clearRetainingCapacity(); - self.gpu_candidate_keys.clearRetainingCapacity(); - // Never truncate a visibility projection: losing an LOD fallback - // region can create a terrain hole, so overflow is CPU-rendered. - if (self.projection_regions.items.len > MAX_LOD_MDI_REGIONS) { - self.gpu_culling_overflow_count +%= 1; - self.updateGpuCullingProfiling(profiling); - self.projection_frame = null; - return; - } - for (self.projection_regions.items) |visible| { - const lod_index = @intFromEnum(visible.key.lod); - const mesh = meshes[lod_index].get(visible.key) orelse continue; - const chunk = regions[lod_index].get(visible.key) orelse continue; - const terrain = mesh.drawRange(.terrain); - const fluid = mesh.drawRange(.fluid); - if ((terrain orelse fluid) == null) continue; - const compact = mesh.isCompact(); - // Dedicated expanded fallback meshes are not backed by the - // per-LOD vertex pools required for GPU indirect submission. - // Omit only those meshes: compact candidates and pooled meshes - // still benefit from GPU culling in the same frame. - if (!compact and !mesh.isPooled()) continue; - const bounds = chunk.worldBounds(); - const cell_size: f32 = if (compact) @as(f32, @floatFromInt(lod_chunk.regionSizeBlocks(visible.key.lod))) / @as(f32, @floatFromInt(mesh.compact_tile_width - 1)) else 0; - self.gpu_candidates.append(self.allocator, .{ - .min_point = .{ @as(f32, @floatFromInt(bounds.min_x)) - camera_pos.x, bounds.min_y - camera_pos.y, @as(f32, @floatFromInt(bounds.min_z)) - camera_pos.z, 0 }, - .max_point = .{ @as(f32, @floatFromInt(bounds.max_x)) - camera_pos.x, bounds.max_y - camera_pos.y, @as(f32, @floatFromInt(bounds.max_z)) - camera_pos.z, 0 }, - .model = visible.model, - .instance_params = .{ visible.mask_radius, visible.lod_fade, if (compact) cell_size else 0, if (compact) @max(16.0, cell_size * 2.0) else 0 }, - .compact_words = .{ mesh.compact_sample_offset, mesh.compact_tile_width, 0, mesh.compactApronMask() }, - .compact_metrics = .{ cell_size, @max(16.0, cell_size * 2.0), 0, 0 }, - .terrain_command = cullCommandFor(mesh, terrain, compact, true), - .water_command = cullCommandFor(mesh, fluid, compact, false), - .lod_and_padding = .{ @intCast(lod_index), if (compact) 1 else 0, 0, 0 }, - }) catch return; - self.gpu_candidate_keys.append(self.allocator, visible.key) catch { - _ = self.gpu_candidates.pop(); - return; - }; - } - if (profiling) |profile| profile.setGpuCullingCandidateCount(self.gpu_candidates.items.len); - if (self.gpu_candidates.items.len < self.gpu_culling_threshold) { - self.projection_frame = null; - return; - } - const query = if (@hasDecl(RHI, "query")) self.rhi.query() else self.rhi; - if (!query.supportsIndirectFirstInstance()) { - self.projection_frame = null; - return; - } - const distance_chunks = max_distance_chunks orelse config.getRadii()[lod_chunk.activeLODCount(config) - 1]; - if (comptime @hasDecl(RHI, "timing")) { - const timing = self.rhi.timing(); - timing.beginPassTiming("LODGpuCullingComputeBarrier"); - defer timing.endPassTiming("LODGpuCullingComputeBarrier"); - } - if (gpu.dispatch(query.getFrameIndex(), self.gpu_candidates.items, .{ - .planes = extractPlanes(view_proj), - .candidate_count = 0, - .max_distance_blocks = @as(f32, @floatFromInt(distance_chunks * CHUNK_SIZE_X)), - .max_commands_per_lod = 0, - })) self.gpu_culling_ready_frame = frame_serial else self.projection_frame = null; - self.updateGpuCullingProfiling(profiling); - } - - pub fn render( - self: *Self, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - // Update frame index - const query = if (@hasDecl(RHI, "query")) self.rhi.query() else self.rhi; - const render_ctx = if (@hasDecl(RHI, "renderContext")) self.rhi.renderContext() else self.rhi; - self.frame_index = query.getFrameIndex(); - // Use the LOD descriptor set while issuing LOD draws, then restore - // normal terrain descriptor mode so the chunk pass keeps its textures. - defer if (@hasDecl(@TypeOf(render_ctx), "setInstanceBuffer")) render_ctx.setInstanceBuffer(0); - selectLODDescriptorStream(render_ctx, layer, false, false); - render_ctx.setLODInstanceBuffer(self.instance_buffers[self.frame_index]); - - const frustum = Frustum.fromViewProj(view_proj); - // Keep opaque LOD terrain just below full chunks during the masked - // handoff. Water must stay at the true surface height or shorelines - // visibly step down at the LOD boundary. - const lod_y_offset: f32 = if (layer == .fluid) 0.0 else -0.05; - - self.instance_data.clearRetainingCapacity(); - self.draw_list.clearRetainingCapacity(); - for (&self.draw_commands) |*commands| commands.clearRetainingCapacity(); - if (stats) |s| { - if (layer == .terrain) { - s.drawn = [_]u32{0} ** LODLevel.count; - s.instances = [_]u32{0} ** LODLevel.count; - s.fluid_drawn = [_]u32{0} ** LODLevel.count; - s.fluid_instances = [_]u32{0} ** LODLevel.count; - } - } - - // Collect visible meshes from coarsest active LOD down so parent - // fallback draws first and finer children overdraw it as they arrive. - var i: usize = lod_chunk.activeLODCount(config); - while (i > 0) { - i -= 1; - const lod: LODLevel = @enumFromInt(@as(u3, @intCast(i))); - self.collectVisibleMeshes(meshes, regions, lod, config, view_proj, camera_pos, frustum, lod_y_offset, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer, stats, profiling) catch |err| { - log.log.errWithTrace("Failed to collect visible meshes for LOD{}: {}", .{ i, err }); - }; - } - - if (self.instance_data.items.len == 0) return; - - const indirect_drawn = self.enable_mdi and self.renderIndirectBatches(render_ctx, query); - - const compact_timing_name = if (layer == .terrain) "LODCompactTerrainPass" else "LODCompactWaterPass"; - if (comptime @hasDecl(RHI, "timing")) self.rhi.timing().beginPassTiming(compact_timing_name); - for (self.draw_list.items, 0..) |mesh, idx| { - if (indirect_drawn and mesh.isPooled()) continue; - if (!mesh.isCompact()) continue; - const instance = self.instance_data.items[idx]; - const range = mesh.drawRange(layer) orelse continue; - render_ctx.setModelMatrix(instance.model, Vec3.one, instance.mask_radius); - if (@hasDecl(@TypeOf(render_ctx), "drawOffset")) { - render_ctx.drawOffset(mesh.bufferHandle(), range.count, .triangles, mesh.vertexOffset() + range.offset); - } else { - render_ctx.draw(mesh.bufferHandle(), range.count, .triangles); - } - } - if (comptime @hasDecl(RHI, "timing")) self.rhi.timing().endPassTiming(compact_timing_name); - for (self.draw_list.items, 0..) |mesh, idx| { - if (indirect_drawn and mesh.isPooled()) continue; - if (mesh.isCompact()) continue; - const instance = self.instance_data.items[idx]; - const range = mesh.drawRange(layer) orelse continue; - render_ctx.setModelMatrix(instance.model, Vec3.one, instance.mask_radius); - if (@hasDecl(@TypeOf(render_ctx), "drawOffset")) { - render_ctx.drawOffset(mesh.bufferHandle(), range.count, .triangles, mesh.vertexOffset() + range.offset); - } else { - render_ctx.draw(mesh.bufferHandle(), range.count, .triangles); - } - } - } - - fn renderIndirectBatches(self: *Self, render_ctx: anytype, query: anytype) bool { - if (!supports_lod_indirect(@TypeOf(render_ctx), @TypeOf(query), @TypeOf(self.rhi))) return false; - if (!query.supportsIndirectFirstInstance()) return false; - if (self.instance_data.items.len == 0) return false; - - const resources = if (@hasDecl(RHI, "resourceManager")) self.rhi.resourceManager() else self.rhi; - if (!@hasDecl(@TypeOf(resources), "updateBuffer")) return false; - - const fi = self.frame_index; - if (self.instance_data.items.len > MAX_LOD_MDI_REGIONS) { - log.log.warn("LOD MDI: instance overflow ({} > {}), falling back to CPU draw", .{ self.instance_data.items.len, MAX_LOD_MDI_REGIONS }); - return false; - } - - var total_commands: usize = 0; - for (self.draw_commands) |commands| total_commands += commands.items.len; - if (total_commands == 0) return false; - if (total_commands > MAX_LOD_MDI_REGIONS) { - log.log.warn("LOD MDI: command overflow ({} > {}), falling back to CPU draw", .{ total_commands, MAX_LOD_MDI_REGIONS }); - return false; - } - - for (0..LODLevel.count) |lod_idx| { - if (self.draw_commands[lod_idx].items.len > 0 and self.vertex_pools[lod_idx].buffer_handle == 0) return false; - } - - resources.updateBuffer(self.instance_buffers[fi], 0, std.mem.sliceAsBytes(self.instance_data.items)) catch |err| { - log.log.err("LOD MDI: failed to update instance buffer: {}", .{err}); - return false; - }; - - var merged_commands = std.ArrayListUnmanaged(rhi_types.DrawIndirectCommand).empty; - defer merged_commands.deinit(self.allocator); - merged_commands.ensureTotalCapacity(self.allocator, total_commands) catch |err| { - log.log.err("LOD MDI: failed to reserve command staging: {}", .{err}); - return false; - }; - - var lod_offsets: [LODLevel.count]usize = [_]usize{0} ** LODLevel.count; - var lod_counts: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count; - for (0..LODLevel.count) |lod_idx| { - lod_offsets[lod_idx] = merged_commands.items.len; - lod_counts[lod_idx] = @intCast(self.draw_commands[lod_idx].items.len); - merged_commands.appendSliceAssumeCapacity(self.draw_commands[lod_idx].items); - } - - resources.updateBuffer(self.indirect_buffers[fi], 0, std.mem.sliceAsBytes(merged_commands.items)) catch |err| { - log.log.err("LOD MDI: failed to update indirect buffer: {}", .{err}); - return false; - }; - - render_ctx.setLODInstanceBuffer(self.instance_buffers[fi]); - const stride = @sizeOf(rhi_types.DrawIndirectCommand); - for (0..LODLevel.count) |lod_idx| { - if (lod_counts[lod_idx] == 0) continue; - const pool_buffer = self.vertex_pools[lod_idx].buffer_handle; - render_ctx.drawIndirect(pool_buffer, self.indirect_buffers[fi], lod_offsets[lod_idx] * stride, lod_counts[lod_idx], stride); - } - return true; - } - - fn readyDiskRadiusForProjection(self: *Self, checker: ?ChunkChecker, checker_ctx: ?*anyopaque, camera_chunk_x: i32, camera_chunk_z: i32, max_radius: i32) i32 { - const safe_max_radius = @max(max_radius, 0); - const cache_source_matches = self.cached_ready_disk_max_radius >= 0 and - self.cached_ready_disk_checker == checker and - self.cached_ready_disk_checker_ctx == checker_ctx; - const cache_matches = cache_source_matches and - self.cached_ready_disk_camera_x == camera_chunk_x and - self.cached_ready_disk_camera_z == camera_chunk_z; - - const ready_radius = if (!cache_matches) - if (cache_source_matches) shifted: { - // A disk reduced by the camera's Manhattan displacement is - // guaranteed to remain inside the previously verified disk. - // Expand only the newly exposed shells instead of rescanning - // the full area whenever the player crosses a chunk edge. - const shift_x: i64 = @intCast(@abs(@as(i64, camera_chunk_x) - @as(i64, self.cached_ready_disk_camera_x))); - const shift_z: i64 = @intCast(@abs(@as(i64, camera_chunk_z) - @as(i64, self.cached_ready_disk_camera_z))); - const retained_radius: i32 = @intCast(@max(@as(i64, self.cached_ready_disk_radius) - shift_x - shift_z, -1)); - break :shifted expandContiguousReadyDiskRadius(checker, checker_ctx, camera_chunk_x, camera_chunk_z, retained_radius, safe_max_radius); - } else contiguousReadyDiskRadius(checker, checker_ctx, camera_chunk_x, camera_chunk_z, safe_max_radius) - else if (self.cached_ready_disk_radius >= safe_max_radius) - safe_max_radius - else - expandContiguousReadyDiskRadius(checker, checker_ctx, camera_chunk_x, camera_chunk_z, self.cached_ready_disk_radius, safe_max_radius); - - self.cached_ready_disk_camera_x = camera_chunk_x; - self.cached_ready_disk_camera_z = camera_chunk_z; - self.cached_ready_disk_max_radius = safe_max_radius; - self.cached_ready_disk_radius = ready_radius; - self.cached_ready_disk_checker = checker; - self.cached_ready_disk_checker_ctx = checker_ctx; - return ready_radius; - } - - fn buildVisibilityProjection( - self: *Self, - all_meshes: *const [LODLevel.count]MeshMap, - all_regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - detail_render_radius: i32, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) !void { - self.projection_regions.clearRetainingCapacity(); - errdefer self.projection_regions.clearRetainingCapacity(); - if (stats) |s| { - s.drawn = [_]u32{0} ** LODLevel.count; - s.instances = [_]u32{0} ** LODLevel.count; - s.fluid_drawn = [_]u32{0} ** LODLevel.count; - s.fluid_instances = [_]u32{0} ** LODLevel.count; - s.gpu_terrain_candidates = 0; - s.gpu_fluid_candidates = 0; - } - - const frustum = Frustum.fromViewProj(view_proj); - const disable_frustum = engine_core.envFlag("ZIGCRAFT_LOD_DISABLE_FRUSTUM", false); - const camera_chunk = worldToChunkFromFloat(camera_pos.x, camera_pos.z); - const chunk_radius = @max(detail_render_radius, 0); - const ready_detail_radius = self.readyDiskRadiusForProjection(chunk_checker, checker_ctx, camera_chunk.chunk_x, camera_chunk.chunk_z, chunk_radius); - const handoff_mask_radius = readyDiskMaskRadius(ready_detail_radius); - var i = lod_chunk.activeLODCount(config); - while (i > 0) { - i -= 1; - const lod: LODLevel = @enumFromInt(@as(u3, @intCast(i))); - var telemetry = @import("lod_stats.zig").LODVisibilityLevelSnapshot{}; - var iter = all_meshes[i].iterator(); - while (iter.next()) |entry| { - telemetry.candidates += 1; - const mesh = entry.value_ptr.*; - if (!mesh.isReady()) { - telemetry.rejected_not_ready += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - if ((mesh.drawRange(.terrain) orelse mesh.drawRange(.fluid)) == null) { - telemetry.rejected_no_draw += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - const chunk = all_regions[i].get(entry.key_ptr.*) orelse { - telemetry.rejected_missing_region += 1; - if (profiling) |profile| profile.addRejected(); - continue; - }; - if (!chunk.isRenderable()) { - telemetry.rejected_not_renderable += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - if (self.isCoveredByFinerLOD(chunk, config)) { - telemetry.rejected_finer_coverage += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - const bounds = chunk.worldBounds(); - const chunk_bounds = chunk.chunkBounds(); - // Cheap radial and frustum tests intentionally precede the - // potentially large chunk-coverage scan. - if (max_distance_chunks) |max_dist| { - if (!isRegionInRange(chunk_bounds, camera_pos, max_dist)) { - telemetry.rejected_range += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - } - if (use_frustum and !disable_frustum and !isRegionInFrustum(frustum, bounds, camera_pos)) { - telemetry.rejected_frustum += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - - const mask_radius = handoff_mask_radius; - if (chunk_checker) |checker| { - if (checker_ctx) |ctx_ptr| { - const coverage_timer = if (profiling) |profile| profile.begin() else null; - const cov = self.isCoveredByChunks(bounds, checker, ctx_ptr, camera_chunk.chunk_x, camera_chunk.chunk_z, chunk_radius); - if (profiling) |profile| { - profile.end(.coverage, coverage_timer); - profile.addCoverage(); - } - telemetry.coverage_checks += 1; - if (cov.covered) { - telemetry.rejected_chunk_coverage += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - } - } - - try self.projection_regions.append(self.allocator, .{ - .key = entry.key_ptr.*, - .model = Mat4.translate(Vec3.init(@as(f32, @floatFromInt(bounds.min_x)) - camera_pos.x, -camera_pos.y, @as(f32, @floatFromInt(bounds.min_z)) - camera_pos.z)), - .mask_radius = mask_radius, - .lod_fade = chunk.transitionFadeProgress(), - }); - telemetry.accepted += 1; - if (profiling) |profile| profile.addVisible(); - } - if (profiling) |profile| profile.addVisibilityLevel(lod, telemetry); - } - } - - /// Returns false only when a prepared GPU frame could not be submitted; - /// callers must rebuild the CPU projection with normal culling first. - fn renderProjectedLayer(self: *Self, all_meshes: *const [LODLevel.count]MeshMap, layer: LODRenderLayer, stats: ?*LODStats, profiling: ?*LODProfilingCollector) bool { - const query = if (@hasDecl(RHI, "query")) self.rhi.query() else self.rhi; - const render_ctx = if (@hasDecl(RHI, "renderContext")) self.rhi.renderContext() else self.rhi; - self.frame_index = query.getFrameIndex(); - defer if (@hasDecl(@TypeOf(render_ctx), "setInstanceBuffer")) render_ctx.setInstanceBuffer(0); - selectLODDescriptorStream(render_ctx, layer, false, false); - render_ctx.setLODInstanceBuffer(self.instance_buffers[self.frame_index]); - self.instance_data.clearRetainingCapacity(); - self.draw_list.clearRetainingCapacity(); - self.compact_fallback_regions.clearRetainingCapacity(); - for (&self.draw_commands) |*commands| commands.clearRetainingCapacity(); - self.updateGpuCullingProfiling(profiling); - if (stats) |s| { - s.gpu_culling_overflows = self.gpu_culling_overflow_count; - s.gpu_culling_requested = self.gpu_culling_requested; - s.gpu_culling_threshold = @intCast(@min(self.gpu_culling_threshold, std.math.maxInt(u32))); - s.gpu_culling_candidate_count = 0; - s.gpu_culling_draw_submissions = 0; - if (self.gpu_culling) |gpu| { - const diagnostics = gpu.diagnostics(); - s.gpu_culling_validation_mismatches = diagnostics.validation_mismatch_count; - s.gpu_culling_validation_generation = diagnostics.validation_generation; - s.gpu_culling_validation_completed_generation = diagnostics.validation_completed_generation; - s.gpu_culling_validation_completed_count = diagnostics.validation_completed_count; - } - } - - const gpu_frame_prepared = self.projection_frame != null and self.gpu_culling_ready_frame == self.projection_frame; - const gpu_submitted = self.renderGpuCulledLayer(layer, stats, profiling, render_ctx, query); - if (gpu_frame_prepared and !gpu_submitted) return false; - // GPU and CPU fallbacks use distinct immutable descriptor streams. - // Re-select direct before any fallback binding or draw is recorded. - selectLODDescriptorStream(render_ctx, layer, false, false); - render_ctx.setLODInstanceBuffer(self.instance_buffers[self.frame_index]); - - { - var compact_timing_started = false; - const compact_timing_name = if (layer == .terrain) "LODCompactTerrainPass" else "LODCompactWaterPass"; - defer if (compact_timing_started and @hasDecl(RHI, "timing")) self.rhi.timing().endPassTiming(compact_timing_name); - for (self.projection_regions.items) |visible| { - const lod_idx = @intFromEnum(visible.key.lod); - const mesh = all_meshes[lod_idx].get(visible.key) orelse continue; - const range = mesh.drawRange(layer) orelse continue; - if (!mesh.isReady() or range.count == 0) continue; - if (gpu_submitted and self.gpuCandidateDraws(visible.key, layer)) continue; - if (mesh.isCompact()) { - if (!compact_timing_started and @hasDecl(RHI, "timing")) { - self.rhi.timing().beginPassTiming(compact_timing_name); - compact_timing_started = true; - } - const result = self.renderCompactMesh(render_ctx, visible, mesh, layer); - if (result == .drawn) { - if (profiling) |profile| profile.addCompactSubmission(); - if (stats) |s| if (layer == .fluid) { - s.fluid_drawn[lod_idx] += 1; - s.fluid_instances[lod_idx] += 1; - } else { - s.drawn[lod_idx] += 1; - s.instances[lod_idx] += 1; - }; - } else { - self.noteCompactRenderFailure(mesh, result, profiling); - // Draw parent fallbacks only after the compact pass - // scope closes so expanded draws do not contaminate - // compact GPU timing. - self.compact_fallback_regions.append(self.allocator, visible) catch {}; - } - continue; - } - const lod_y_offset: f32 = if (layer == .fluid) 0.0 else -0.05; - var instance = rhi_types.InstanceData{ .model = visible.model, .mask_radius = visible.mask_radius, .lod_fade = visible.lod_fade, .padding = .{ 0, 0 } }; - instance.model.data[3][1] += lod_y_offset; - self.instance_data.append(self.allocator, instance) catch continue; - self.draw_list.append(self.allocator, mesh) catch { - _ = self.instance_data.pop(); - continue; - }; - if (mesh.isPooled()) { - self.draw_commands[lod_idx].append(self.allocator, .{ - .vertexCount = range.count, - .instanceCount = 1, - .firstVertex = mesh.firstVertex(range), - .firstInstance = @intCast(self.instance_data.items.len - 1), - }) catch { - _ = self.draw_list.pop(); - _ = self.instance_data.pop(); - continue; - }; - } - if (stats) |s| { - if (layer == .fluid) { - s.fluid_drawn[lod_idx] += 1; - s.fluid_instances[lod_idx] += 1; - } else { - s.drawn[lod_idx] += 1; - s.instances[lod_idx] += 1; - } - } - } - } - for (self.compact_fallback_regions.items) |visible| { - _ = self.renderParentFallback(all_meshes, visible, layer, render_ctx, profiling); - } - if (self.instance_data.items.len == 0) return true; - const indirect_drawn = self.enable_mdi and self.renderIndirectBatches(render_ctx, query); - for (self.draw_list.items, 0..) |mesh, index| { - if (indirect_drawn and mesh.isPooled()) continue; - const instance = self.instance_data.items[index]; - const range = mesh.drawRange(layer) orelse continue; - render_ctx.setModelMatrix(instance.model, Vec3.one, instance.mask_radius); - if (@hasDecl(@TypeOf(render_ctx), "drawOffset")) { - render_ctx.drawOffset(mesh.bufferHandle(), range.count, .triangles, mesh.vertexOffset() + range.offset); - } else render_ctx.draw(mesh.bufferHandle(), range.count, .triangles); - } - return true; - } - - fn gpuCandidateDraws(self: *const Self, key: LODRegionKey, layer: LODRenderLayer) bool { - for (self.gpu_candidate_keys.items, self.gpu_candidates.items) |candidate_key, candidate| { - if (!std.meta.eql(candidate_key, key)) continue; - const command = if (layer == .fluid) candidate.water_command else candidate.terrain_command; - if (command.count == 0) return false; - // Indirect compact streams are currently partitioned by LOD, - // not grid width. If async density changes produce two widths - // in one LOD, no single index buffer can draw that stream; keep - // every affected tile on the width-correct direct path. - if (candidate.lod_and_padding[1] != 0) return self.gpuCompactWidthForLOD(key.lod, layer) != null; - return true; - } - return false; - } - - fn renderParentFallback(self: *Self, all_meshes: *const [LODLevel.count]MeshMap, child: VisibleRegion, layer: LODRenderLayer, render_ctx: anytype, profiling: ?*LODProfilingCollector) bool { - var child_key = child.key; - var child_model = child.model; - while (child_key.parentKey()) |parent_key| { - const child_size: i32 = @intCast(child_key.lod.chunksPerSide() * CHUNK_SIZE_X); - const parent_size: i32 = @intCast(parent_key.lod.chunksPerSide() * CHUNK_SIZE_X); - child_model.data[3][0] += @floatFromInt(parent_key.rx * parent_size - child_key.rx * child_size); - child_model.data[3][2] += @floatFromInt(parent_key.rz * parent_size - child_key.rz * child_size); - const mesh = all_meshes[@intFromEnum(parent_key.lod)].get(parent_key) orelse { - child_key = parent_key; - continue; - }; - const range = mesh.drawRange(layer) orelse { - child_key = parent_key; - continue; - }; - if (!mesh.isReady() or range.count == 0) { - child_key = parent_key; - continue; - } - const parent_visible = VisibleRegion{ - .key = parent_key, - .model = child_model, - .mask_radius = child.mask_radius, - .lod_fade = 1.0, - }; - if (mesh.isCompact()) { - const result = self.renderCompactMesh(render_ctx, parent_visible, mesh, layer); - if (result == .drawn) { - if (profiling) |profile| profile.addCompactSubmission(); - return true; - } - self.noteCompactRenderFailure(mesh, result, profiling); - child_key = parent_key; - continue; - } - selectLODDescriptorStream(render_ctx, layer, false, false); - render_ctx.setLODInstanceBuffer(self.instance_buffers[self.frame_index]); - render_ctx.setModelMatrix(child_model, Vec3.one, child.mask_radius); - if (@hasDecl(@TypeOf(render_ctx), "drawOffset")) { - render_ctx.drawOffset(mesh.bufferHandle(), range.count, .triangles, mesh.vertexOffset() + range.offset); - } else { - render_ctx.draw(mesh.bufferHandle(), range.count, .triangles); - } - return true; - } - return false; - } - - fn noteCompactRenderFailure(_: *Self, mesh: *LODMesh, result: CompactRenderResult, profiling: ?*LODProfilingCollector) void { - switch (result) { - .drawn => unreachable, - .unavailable => if (profiling) |profile| profile.addCompactDrawUnavailable(), - .backend_failed => { - if (profiling) |profile| profile.addCompactDrawFailure(); - if (mesh.noteCompactBackendDrawFailure()) { - log.log.warn("LOD{} compact draw failed {} times after preflight; scheduling expanded fallback", .{ @intFromEnum(mesh.lodLevel()), LODMesh.COMPACT_BACKEND_FAILURE_LIMIT }); - } - }, - } - } - - fn renderCompactMesh(self: *Self, render_ctx: anytype, visible: VisibleRegion, mesh: *LODMesh, layer: LODRenderLayer) CompactRenderResult { - if (comptime !@hasDecl(@TypeOf(render_ctx), "drawCompactLOD") or !@hasDecl(@TypeOf(render_ctx), "setLODCompactSampleBuffer") or !@hasDecl(@TypeOf(render_ctx), "setLODDescriptorStream")) return .unavailable; - const lod = mesh.lodLevel(); - if ((lod != .lod3 and lod != .lod4) or self.compact_pool.buffer_handle == 0 or mesh.compact_tile_width < 2) return .unavailable; - selectLODDescriptorStream(render_ctx, layer, true, false); - render_ctx.setLODCompactSampleBuffer(self.compact_pool.buffer_handle); - const index_buffer = self.compactIndexBuffer(lod, mesh.compact_tile_width, layer); - if (index_buffer == 0) return .unavailable; - const index_count: u32 = @intCast(compactGridIndexCount(mesh.compact_tile_width, layer == .terrain)); - if (index_count == 0) return .unavailable; - var model = visible.model; - // Match expanded terrain's handoff bias. Water remains at its true - // surface height so compact/expanded representation changes do not - // introduce a vertical seam. - if (layer == .terrain) model.data[3][1] -= 0.05; - if (render_ctx.drawCompactLOD(index_buffer, index_count, .{ - .model = model, - .mask_radius = visible.mask_radius, - .lod_fade = visible.lod_fade, - .sample_offset = mesh.compact_sample_offset, - .width = mesh.compact_tile_width, - .cell_size = @as(f32, @floatFromInt(lod_chunk.regionSizeBlocks(lod))) / @as(f32, @floatFromInt(mesh.compact_tile_width - 1)), - .layer = if (layer == .fluid) 1 else 0, - .skirt_depth = @max(16.0, @as(f32, @floatFromInt(lod_chunk.regionSizeBlocks(lod))) / @as(f32, @floatFromInt(mesh.compact_tile_width - 1)) * 2.0), - .edge_masks = mesh.compactApronMask(), - })) { - mesh.resetCompactBackendDrawFailures(); - return .drawn; - } - return .backend_failed; - } - - fn renderGpuCulledLayer(self: *Self, layer: LODRenderLayer, stats: ?*LODStats, profiling: ?*LODProfilingCollector, render_ctx: anytype, query: anytype) bool { - const frame = self.projection_frame orelse return false; - if (self.gpu_culling_ready_frame != frame) return false; - const gpu = self.gpu_culling orelse return false; - if (!@hasDecl(@TypeOf(render_ctx), "drawIndirectCount")) return false; - if (!query.supportsIndirectFirstInstance() or self.gpu_candidates.items.len < self.gpu_culling_threshold) return false; - var has_standard = false; - var has_compact = false; - for (self.gpu_candidates.items) |candidate| { - const lod_index = candidate.lod_and_padding[0]; - if (lod_index >= LODLevel.count) return false; - const command = if (layer == .fluid) candidate.water_command else candidate.terrain_command; - if (candidate.lod_and_padding[1] == 0 and command.count != 0) { - has_standard = true; - if (self.vertex_pools[lod_index].buffer_handle == 0) return false; - } - if (candidate.lod_and_padding[1] != 0 and command.count != 0) has_compact = true; - } - const fi = self.frame_index; - // Complete validation before emitting any graphics command. A - // false result above is safe to fall back to CPU rendering; after - // the first draw is recorded it is not. - if ((has_standard or has_compact) and (gpu.countBuffer(fi) == 0 or gpu.indirectBuffer(fi, layer == .fluid, false) == 0)) return false; - if (has_standard and gpu.instanceBuffer(fi, layer == .fluid, false) == 0) return false; - if (has_compact) { - if (!@hasDecl(@TypeOf(render_ctx), "drawCompactLODIndirectCount") or - !@hasDecl(@TypeOf(render_ctx), "setLODCompactSampleBuffer") or - !@hasDecl(@TypeOf(render_ctx), "setLODCompactInstanceBuffer") or - !@hasDecl(@TypeOf(render_ctx), "setLODDescriptorStream") or - self.compact_pool.buffer_handle == 0 or - gpu.instanceBuffer(fi, layer == .fluid, true) == 0 or - gpu.indirectBuffer(fi, layer == .fluid, true) == 0) - { - return false; - } - inline for (.{ LODLevel.lod3, LODLevel.lod4 }) |lod| if (self.gpuCompactWidthForLOD(lod, layer)) |width| { - if (self.compactIndexBuffer(lod, width, layer) == 0) return false; - }; - } - selectLODDescriptorStream(render_ctx, layer, false, true); - render_ctx.setLODInstanceBuffer(gpu.instanceBuffer(fi, layer == .fluid, false)); - const commands = gpu.indirectBuffer(fi, layer == .fluid, false); - const counts = gpu.countBuffer(fi); - const stride = @sizeOf(rhi_types.DrawIndirectCommand); - if (has_standard) for (0..LODLevel.count) |lod_index| { - if (self.vertex_pools[lod_index].buffer_handle == 0) continue; - if (!render_ctx.drawIndirectCount(self.vertex_pools[lod_index].buffer_handle, commands, lod_index * MAX_LOD_MDI_REGIONS * stride, counts, (if (layer == .fluid) LODLevel.count + lod_index else lod_index) * @sizeOf(u32), MAX_LOD_MDI_REGIONS, stride)) return false; - }; - if (has_compact) { - selectLODDescriptorStream(render_ctx, layer, true, true); - render_ctx.setLODCompactSampleBuffer(self.compact_pool.buffer_handle); - render_ctx.setLODCompactInstanceBuffer(gpu.instanceBuffer(fi, layer == .fluid, true)); - const compact_commands = gpu.indirectBuffer(fi, layer == .fluid, true); - const compact_count_base: usize = if (layer == .fluid) LODLevel.count * 3 else LODLevel.count * 2; - const compact_timing_name = if (layer == .terrain) "LODCompactTerrainPass" else "LODCompactWaterPass"; - if (comptime @hasDecl(RHI, "timing")) { - const timing = self.rhi.timing(); - timing.beginPassTiming(compact_timing_name); - defer timing.endPassTiming(compact_timing_name); - } - for ([_]LODLevel{ .lod3, .lod4 }) |lod| { - const lod_index = @intFromEnum(lod); - const width = self.gpuCompactWidthForLOD(lod, layer) orelse continue; - const index_buffer = self.compactIndexBuffer(lod, width, layer); - if (!render_ctx.drawCompactLODIndirectCount(index_buffer, compact_commands, lod_index * MAX_LOD_MDI_REGIONS * @sizeOf(rhi_types.DrawIndexedIndirectCommand), counts, (compact_count_base + lod_index) * @sizeOf(u32), MAX_LOD_MDI_REGIONS)) return false; - } - if (profiling) |profile| profile.addCompactSubmission(); - } - if (stats) |s| { - // GPU counters remain device-local; keep submitted streams distinct - // from CPU visibility counts without introducing a readback stall. - const submitted: u32 = @intCast(self.gpu_candidates.items.len); - if (layer == .fluid) s.gpu_fluid_candidates = submitted else s.gpu_terrain_candidates = submitted; - s.gpu_culling_candidate_count = submitted; - s.gpu_culling_draw_submissions +|= 1; - const diagnostics = gpu.diagnostics(); - s.gpu_culling_overflows = self.gpu_culling_overflow_count + diagnostics.overflow_count; - s.gpu_culling_validation_mismatches = diagnostics.validation_mismatch_count; - s.gpu_culling_validation_generation = diagnostics.validation_generation; - s.gpu_culling_validation_completed_generation = diagnostics.validation_completed_generation; - s.gpu_culling_validation_completed_count = diagnostics.validation_completed_count; - } - if (self.gpu_culling_submitted_frame != frame) { - if (profiling) |profile| profile.addGpuCullingSubmission(); - self.gpu_culling_submitted_frame = frame; - } - self.updateGpuCullingProfiling(profiling); - return true; - } - - fn updateGpuCullingProfiling(self: *Self, profiling: ?*LODProfilingCollector) void { - const profile = profiling orelse return; - profile.setGpuCullingConfiguration(self.gpu_culling_requested, self.gpu_culling_threshold); - if (self.gpu_culling) |gpu| { - const diagnostics = gpu.diagnostics(); - profile.setGpuCullingDiagnostics( - self.gpu_culling_overflow_count +| diagnostics.overflow_count, - diagnostics.validation_mismatch_count, - diagnostics.validation_generation, - diagnostics.validation_completed_generation, - diagnostics.validation_completed_count, - ); - } else profile.setGpuCullingDiagnostics(self.gpu_culling_overflow_count, 0, 0, 0, 0); - } - - fn compactIndexBuffer(self: *const Self, lod: LODLevel, width: u32, layer: LODRenderLayer) rhi_types.BufferHandle { - const upload_frame = self.compact_index_upload_frame orelse return 0; - if (self.frame_serial <= upload_frame) return 0; - if (lod != .lod3 and lod != .lod4) return 0; - const width_index = compactGridVariant(width) orelse return 0; - return self.compact_index_buffers[@intFromEnum(lod) - @intFromEnum(LODLevel.lod3)][if (layer == .fluid) 1 else 0][width_index]; - } - - fn gpuCompactWidthForLOD(self: *const Self, lod: LODLevel, layer: LODRenderLayer) ?u32 { - var result: ?u32 = null; - for (self.gpu_candidates.items) |candidate| { - if (candidate.lod_and_padding[0] != @intFromEnum(lod) or candidate.lod_and_padding[1] == 0) continue; - const command = if (layer == .fluid) candidate.water_command else candidate.terrain_command; - if (command.count == 0) continue; - const width = candidate.compact_words[1]; - if (result) |existing| { - if (existing != width) return null; - } else result = width; - } - return result; - } - - fn collectVisibleMeshes( - self: *Self, - all_meshes: *const [LODLevel.count]MeshMap, - all_regions: *const [LODLevel.count]RegionMap, - lod: LODLevel, - config: ILODConfig, - _: Mat4, - camera_pos: Vec3, - frustum: Frustum, - lod_y_offset: f32, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) !void { - const meshes = &all_meshes[@intFromEnum(lod)]; - const regions = &all_regions[@intFromEnum(lod)]; - const diag_enabled = engine_core.envFlag("ZIGCRAFT_LOD_DIAG", false); - const disable_frustum = engine_core.envFlag("ZIGCRAFT_LOD_DISABLE_FRUSTUM", false); - var diag = RenderDiag{}; - var lod_rendered: u32 = 0; - var lod_covered: u32 = 0; - var first_missing_cx: i32 = 0; - var first_missing_cz: i32 = 0; - var first_missing_in_radius: bool = false; - - const camera_chunk_diag = worldToChunkFromFloat(camera_pos.x, camera_pos.z); - const pc_x_diag = camera_chunk_diag.chunk_x; - const pc_z_diag = camera_chunk_diag.chunk_z; - - var iter = meshes.iterator(); - while (iter.next()) |entry| { - diag.meshes_seen += 1; - const mesh = entry.value_ptr.*; - // The legacy immediate path has no indexed compact submission - // context. Never reinterpret compact samples as expanded - // vertices; frame-based rendering owns compact draws. - if (mesh.isCompact()) continue; - const draw_range = mesh.drawRange(layer) orelse { - diag.not_ready += 1; - if (profiling) |profile| profile.addRejected(); - continue; - }; - if (!mesh.isReady() or draw_range.count == 0) { - diag.not_ready += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - if (regions.get(entry.key_ptr.*)) |chunk| { - if (!chunk.isRenderable()) { - diag.bad_state += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - if (self.isCoveredByFinerLOD(chunk, config)) { - diag.covered_finer_lod += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - const bounds = chunk.worldBounds(); - const chunk_bounds = chunk.chunkBounds(); - - if (max_distance_chunks) |max_dist| { - if (!isRegionInRange(chunk_bounds, camera_pos, max_dist)) { - diag.out_of_range += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - } - - if (use_frustum and !disable_frustum) { - if (!isRegionInFrustum(frustum, bounds, camera_pos)) { - diag.frustum_culled += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - } - - const exact_mask_radius = config.calculateMaskRadius(); - var mask_radius = conservativeChunkDiskMaskRadius(exact_mask_radius); - if (chunk_checker) |checker| { - if (checker_ctx) |ctx_ptr| { - const camera_chunk = worldToChunkFromFloat(camera_pos.x, camera_pos.z); - const pc_x = camera_chunk.chunk_x; - const pc_z = camera_chunk.chunk_z; - const chunk_radius = config.getChunkRenderRadius(); - const coverage_timer = if (profiling) |profile| profile.begin() else null; - const cov = self.isCoveredByChunks(bounds, checker, ctx_ptr, pc_x, pc_z, chunk_radius); - if (profiling) |profile| { - profile.end(.coverage, coverage_timer); - profile.addCoverage(); - } - if (cov.covered) { - lod_covered += 1; - diag.covered_chunks += 1; - if (profiling) |profile| profile.addRejected(); - continue; - } - if (lod_rendered == 0) { - first_missing_cx = cov.missing_cx; - first_missing_cz = cov.missing_cz; - first_missing_in_radius = cov.missing_chunk_in_radius; - } - // A single LOD instance cannot exclude individual loaded - // chunks while this boundary region is incomplete. Keep - // the conservative inner chunk disk until all of its detail - // cells are ready, then switch to exact chunk ownership. - if (cov.missing_chunk_in_radius) { - if (!cov.has_chunk_coverage_in_radius) mask_radius = LOD_UNMASKED_SENTINEL; - } else { - mask_radius = exact_mask_radius; - } - } - } - - lod_rendered += 1; - - const model = Mat4.translate(Vec3.init(@as(f32, @floatFromInt(bounds.min_x)) - camera_pos.x, -camera_pos.y + lod_y_offset, @as(f32, @floatFromInt(bounds.min_z)) - camera_pos.z)); - - // Parent coverage, not camera distance, owns LOD handoff. A - // distance-band cutoff creates concentric visible rings while - // the parent already provides complete fallback terrain. - const fade = chunk.transitionFadeProgress(); - try self.instance_data.append(self.allocator, .{ - .model = model, - .mask_radius = mask_radius, - .lod_fade = fade, - .padding = .{ 0, 0 }, - }); - try self.draw_list.append(self.allocator, mesh); - if (mesh.isPooled()) { - try self.draw_commands[@intFromEnum(lod)].append(self.allocator, .{ - .vertexCount = draw_range.count, - .instanceCount = 1, - .firstVertex = mesh.firstVertex(draw_range), - .firstInstance = @intCast(self.instance_data.items.len - 1), - }); - } - diag.drawn += 1; - if (profiling) |profile| profile.addVisible(); - if (stats) |s| { - const lod_idx = @intFromEnum(lod); - if (layer == .fluid) { - s.fluid_drawn[lod_idx] += 1; - s.fluid_instances[lod_idx] += 1; - } else { - s.drawn[lod_idx] += 1; - s.instances[lod_idx] += 1; - } - } - } else { - diag.missing_region += 1; - if (profiling) |profile| profile.addRejected(); - } - } - - if (diag_enabled) { - const S = struct { - var counter: [LODLevel.count]u64 = .{0} ** LODLevel.count; - }; - const lod_idx = @intFromEnum(lod); - S.counter[lod_idx] += 1; - if (S.counter[lod_idx] % 120 == 1) { - log.log.info("LOD_RENDER_DIAG lod={} meshes={} drawn={} not_ready={} bad_state={} no_region={} finer={} chunk_cov={} frustum={} range={} frustum_disabled={} cam_chunk=({}, {})", .{ - lod_idx, - diag.meshes_seen, - diag.drawn, - diag.not_ready, - diag.bad_state, - diag.missing_region, - diag.covered_finer_lod, - diag.covered_chunks, - diag.frustum_culled, - diag.out_of_range, - disable_frustum, - pc_x_diag, - pc_z_diag, - }); - } - } - - if (lod_rendered > 0 or lod_covered > 0) { - const missing_dx = first_missing_cx - pc_x_diag; - const missing_dz = first_missing_cz - pc_z_diag; - const missing_dist_sq = missing_dx * missing_dx + missing_dz * missing_dz; - // Only log every 300 frames to reduce spam - if (lod_covered == 0 and lod_rendered > 0) { - // Track a static counter for throttling - const S = struct { - var counter: u64 = 0; - }; - S.counter += 1; - if (build_options.startup_diagnostic_seconds == 0 and S.counter % 300 == 1) { - log.log.debug("LOD_DIAG: rendered={} covered={} first_missing=({},{}) missing_dist2={} in_radius={} cam=({d:.0},{d:.0}) cam_chunk=({},{})", .{ - lod_rendered, lod_covered, - first_missing_cx, first_missing_cz, - missing_dist_sq, first_missing_in_radius, - camera_pos.x, camera_pos.z, - pc_x_diag, pc_z_diag, - }); - } - } - } - } - - fn isCoveredByFinerLOD( - _: *Self, - chunk: *const LODChunk, - config: ILODConfig, - ) bool { - return chunk.isCoveredByFinerLOD(config.getFallbackMissingChildThreshold()); - } - - const CoverageResult = struct { - covered: bool, - missing_cx: i32, - missing_cz: i32, - missing_chunk_in_radius: bool, - has_chunk_coverage_in_radius: bool, - }; - - fn isCoveredByChunks( - _: *Self, - bounds: LODChunk.WorldBounds, - checker: ChunkChecker, - ctx: *anyopaque, - pc_x: i32, - pc_z: i32, - lod0_radius: i32, - ) CoverageResult { - const min_cx = @divFloor(bounds.min_x, CHUNK_SIZE_X) - CHUNK_COVERAGE_PADDING; - const min_cz = @divFloor(bounds.min_z, CHUNK_SIZE_Z) - CHUNK_COVERAGE_PADDING; - const max_cx = @divFloor(bounds.max_x, CHUNK_SIZE_X) - 1 + CHUNK_COVERAGE_PADDING; - const max_cz = @divFloor(bounds.max_z, CHUNK_SIZE_Z) - 1 + CHUNK_COVERAGE_PADDING; - - const radius_sq: i64 = @as(i64, lod0_radius) * @as(i64, lod0_radius); - - var first_outside_cx: i32 = 0; - var first_outside_cz: i32 = 0; - var has_outside_radius = false; - var first_missing_cx: i32 = 0; - var first_missing_cz: i32 = 0; - var has_missing_in_radius = false; - var has_chunk_coverage_in_radius = false; - - var cz = min_cz; - while (cz <= max_cz) : (cz += 1) { - var cx = min_cx; - while (cx <= max_cx) : (cx += 1) { - const dx: i64 = @as(i64, cx) - @as(i64, pc_x); - const dz: i64 = @as(i64, cz) - @as(i64, pc_z); - if (dx * dx + dz * dz > radius_sq) { - if (!has_outside_radius) { - has_outside_radius = true; - first_outside_cx = cx; - first_outside_cz = cz; - } - continue; - } - if (checker(cx, cz, ctx)) { - has_chunk_coverage_in_radius = true; - } else if (!has_missing_in_radius) { - has_missing_in_radius = true; - first_missing_cx = cx; - first_missing_cz = cz; - } - } - } - - if (has_missing_in_radius) { - return .{ .covered = false, .missing_cx = first_missing_cx, .missing_cz = first_missing_cz, .missing_chunk_in_radius = true, .has_chunk_coverage_in_radius = has_chunk_coverage_in_radius }; - } - - if (has_outside_radius) { - return .{ .covered = false, .missing_cx = first_outside_cx, .missing_cz = first_outside_cz, .missing_chunk_in_radius = false, .has_chunk_coverage_in_radius = has_chunk_coverage_in_radius }; - } - return .{ .covered = true, .missing_cx = 0, .missing_cz = 0, .missing_chunk_in_radius = false, .has_chunk_coverage_in_radius = has_chunk_coverage_in_radius }; - } - - /// Create a LODGPUBridge that delegates to this renderer's RHI. - pub fn createGPUBridge(self: *Self) LODGPUBridge { - const Wrapper = struct { - fn onUpload(mesh: *LODMesh, ctx: *anyopaque) rhi_types.RhiError!void { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - const resources = LODMeshResources.fromProvider(RHI, &renderer.rhi); - if (mesh.isCompact()) return renderer.compact_pool.upload(mesh, resources); - // Expanded far CPU fallback meshes use dedicated buffers. - // Growing a far shared pool republishes its whole shadow - // and can exceed the bounded staging ring in one frame. - if (!renderer.enable_mdi or mesh.lodLevel() == .lod3 or mesh.lodLevel() == .lod4) return mesh.upload(resources); - return renderer.vertex_pools[@intFromEnum(mesh.lodLevel())].uploadMesh(mesh, resources); - } - fn onDestroy(mesh: *LODMesh, ctx: *anyopaque) void { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - const resources = LODMeshResources.fromProvider(RHI, &renderer.rhi); - if (mesh.isCompact()) { - renderer.compact_pool.retireMesh(mesh, renderer.frame_serial, renderer.frame_index); - return; - } - if (mesh.isPooled()) { - renderer.vertex_pools[@intFromEnum(mesh.lodLevel())].destroyMeshDeferred(mesh, renderer.frame_serial, renderer.frame_index); - } else { - mesh.deinit(resources); - } - } - fn onUploadCost(mesh: *LODMesh, ctx: *anyopaque) LODStagingCost { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - if (mesh.isCompact() or !renderer.enable_mdi or mesh.lodLevel() == .lod3 or mesh.lodLevel() == .lod4) { - return .{ .payload_bytes = mesh.pendingUploadBytes() }; - } - return renderer.vertex_pools[@intFromEnum(mesh.lodLevel())].uploadCost(mesh); - } - fn onWaitIdle(ctx: *anyopaque) void { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - renderer.rhi.waitIdle(); - } - fn onSupportsCompact(ctx: *anyopaque) bool { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - const RenderContext = @TypeOf(if (@hasDecl(RHI, "renderContext")) @as(RHI, undefined).renderContext() else @as(RHI, undefined)); - if (!@hasDecl(RenderContext, "drawCompactLOD") or - !@hasDecl(RenderContext, "setLODCompactSampleBuffer") or - !@hasDecl(RenderContext, "setLODDescriptorStream")) return false; - inline for (.{ LODLevel.lod3, LODLevel.lod4 }, 0..) |lod, lod_index| { - const max_width = @import("world-core").LODSimplifiedData.getGridSize(lod); - for (COMPACT_GRID_WIDTHS, 0..) |width, width_index| { - if (width > max_width) continue; - for (renderer.compact_index_buffers[lod_index]) |layer_handles| { - if (layer_handles[width_index] == 0) return false; - } - } - } - return true; - } - fn onSupportsCompactGpuCulling(ctx: *anyopaque) bool { - const renderer: *Self = @ptrCast(@alignCast(ctx)); - if (!onSupportsCompact(ctx)) return false; - const Query = @TypeOf(if (@hasDecl(RHI, "query")) @as(RHI, undefined).query() else @as(RHI, undefined)); - if (comptime !@hasDecl(Query, "supportsCompactLODGpuCulling")) return false; - return renderer.rhi.query().supportsCompactLODGpuCulling(); - } - }; - return .{ - .on_upload = Wrapper.onUpload, - .on_destroy = Wrapper.onDestroy, - .on_wait_idle = Wrapper.onWaitIdle, - .on_upload_cost = Wrapper.onUploadCost, - .ctx = @ptrCast(self), - .on_supports_compact = Wrapper.onSupportsCompact, - .on_supports_compact_gpu_culling = Wrapper.onSupportsCompactGpuCulling, - }; - } - - fn memoryStats(self: *Self) LODRendererMemoryStats { - var result = LODRendererMemoryStats{}; - for (&self.vertex_pools) |*pool| { - const capacity = pool.gpuMemoryBytes(); - const allocated = pool.allocatedBytes(); - result.pool_gpu_capacity_bytes += capacity; - result.pool_gpu_allocated_bytes += allocated; - result.pool_gpu_slack_bytes += capacity - allocated; - // The pool keeps a full CPU copy so it can grow and compact - // without a GPU readback. - result.pool_cpu_shadow_bytes += capacity; - } - const compact = self.compact_pool.memoryStats(); - result.compact_pool_capacity_bytes = compact.capacity_bytes; - result.compact_pool_allocated_bytes = compact.allocated_bytes; - result.compact_pool_free_bytes = compact.free_bytes; - result.compact_pool_retired_bytes = compact.retired_bytes; - return result; - } - - /// Create a type-erased LODRenderInterface from this renderer. - pub fn toInterface(self: *Self) LODRenderInterface { - const Wrapper = struct { - fn renderFn( - self_ptr: *anyopaque, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - const renderer: *Self = @ptrCast(@alignCast(self_ptr)); - renderer.render(meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer, stats, profiling); - } - fn renderFrameFn( - self_ptr: *anyopaque, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - detail_render_radius: i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - const renderer: *Self = @ptrCast(@alignCast(self_ptr)); - renderer.renderFrame(frame_serial, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, layer, stats, profiling); - } - fn prepareFrameFn(self_ptr: *anyopaque, frame_serial: u64, meshes: *const [LODLevel.count]MeshMap, regions: *const [LODLevel.count]RegionMap, config: ILODConfig, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, max_distance_chunks: ?i32, detail_render_radius: i32, stats: ?*LODStats, profiling: ?*LODProfilingCollector) void { - const renderer: *Self = @ptrCast(@alignCast(self_ptr)); - renderer.prepareFrame(frame_serial, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, max_distance_chunks, detail_render_radius, stats, profiling); - } - fn memoryStatsFn(self_ptr: *anyopaque) LODRendererMemoryStats { - const renderer: *Self = @ptrCast(@alignCast(self_ptr)); - return renderer.memoryStats(); - } - fn deinitFn(self_ptr: *anyopaque) void { - const renderer: *Self = @ptrCast(@alignCast(self_ptr)); - renderer.deinit(); - } - }; - return .{ - .render_fn = Wrapper.renderFn, - .render_frame_fn = Wrapper.renderFrameFn, - .prepare_frame_fn = Wrapper.prepareFrameFn, - .memory_stats_fn = Wrapper.memoryStatsFn, - .deinit_fn = Wrapper.deinitFn, - .ptr = self, - }; - } - }; -} - -fn isRegionInRange(bounds: ChunkBounds, camera_pos: Vec3, max_distance_chunks: i32) bool { - const camera_chunk = worldToChunkFromFloat(camera_pos.x, camera_pos.z); - return bounds.intersectsRadius(camera_chunk.chunk_x, camera_chunk.chunk_z, max_distance_chunks); -} - -test "distant LOD render limit rejects disconnected resident regions" { - const near = ChunkBounds{ .min_x = 200, .min_z = -16, .max_x = 240, .max_z = 16 }; - const far = ChunkBounds{ .min_x = 300, .min_z = -16, .max_x = 340, .max_z = 16 }; - try std.testing.expect(isRegionInRange(near, Vec3.zero, 256)); - try std.testing.expect(!isRegionInRange(far, Vec3.zero, 256)); -} - -fn calculateBandFade(config: ILODConfig, lod: LODLevel, bounds: ChunkBounds, camera_pos: Vec3) f32 { - const lod_idx = @intFromEnum(lod); - if (lod_idx == 0) return 1.0; - - const camera_chunk = worldToChunkFromFloat(camera_pos.x, camera_pos.z); - const dist_sq = bounds.distanceSquaredToPoint(camera_chunk.chunk_x, camera_chunk.chunk_z); - const dist_chunks = @sqrt(@as(f32, @floatFromInt(dist_sq))); - const radii = config.getRadii(); - const end = @as(f32, @floatFromInt(@max(radii[lod_idx], 1))); - const inner = @as(f32, @floatFromInt(@max(radii[lod_idx - 1], 0))); - const configured_start = end * std.math.clamp(config.getFogStartPercent(lod), 0.0, 1.0); - const start = @min(@max(inner, configured_start), end - 0.001); - return std.math.clamp((dist_chunks - start) / @max(end - start, 0.001), 0.0, 1.0); -} - -fn supports_lod_indirect(comptime RenderCtx: type, comptime Query: type, comptime RHI: type) bool { - _ = RHI; - return @hasDecl(RenderCtx, "drawIndirect") and - @hasDecl(RenderCtx, "setLODInstanceBuffer") and - @hasDecl(Query, "supportsIndirectFirstInstance"); -} - -fn cullCommandFor(mesh: *const LODMesh, range: ?LODMesh.DrawRange, compact: bool, terrain: bool) rhi_types.LODCullCommand { - const draw_range = range orelse return .{ .count = 0, .instance_count = 0, .first = 0 }; - if (compact) return .{ - .count = @intCast(compactGridIndexCount(mesh.compact_tile_width, terrain)), - .instance_count = 1, - .first = 0, - .vertex_offset = 0, - }; - return .{ - .count = draw_range.count, - .instance_count = 1, - .first = mesh.firstVertex(draw_range), - }; -} - -fn extractPlanes(view_proj: Mat4) [6][4]f32 { - const m = view_proj.data; - var planes = [6][4]f32{ - .{ m[0][3] + m[0][0], m[1][3] + m[1][0], m[2][3] + m[2][0], m[3][3] + m[3][0] }, - .{ m[0][3] - m[0][0], m[1][3] - m[1][0], m[2][3] - m[2][0], m[3][3] - m[3][0] }, - .{ m[0][3] + m[0][1], m[1][3] + m[1][1], m[2][3] + m[2][1], m[3][3] + m[3][1] }, - .{ m[0][3] - m[0][1], m[1][3] - m[1][1], m[2][3] - m[2][1], m[3][3] - m[3][1] }, - .{ m[0][2], m[1][2], m[2][2], m[3][2] }, - .{ m[0][3] - m[0][2], m[1][3] - m[1][2], m[2][3] - m[2][2], m[3][3] - m[3][2] }, - }; - for (&planes) |*plane| { - const length = @sqrt(plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]); - if (length > 0.0001) { - for (plane) |*value| value.* /= length; - } - } - return planes; -} - -fn gpuCullingThreshold() usize { - return engine_core.envInt("ZIGCRAFT_LOD_GPU_CULLING_THRESHOLD", 128); -} - -/// Keep the compile-time benchmark opt-in independent of process environment. -/// This is intentionally a pure helper so both build-option states are covered -/// without tests mutating a global environment. -fn gpuCullingRequested(benchmark_gpu_culling: bool, environment_requested: bool) bool { - return benchmark_gpu_culling or environment_requested; -} - -fn isRegionInFrustum(frustum: Frustum, bounds: LODChunk.WorldBounds, camera_pos: Vec3) bool { - const min_x: f32 = @floatFromInt(bounds.min_x); - const min_z: f32 = @floatFromInt(bounds.min_z); - const max_x: f32 = @floatFromInt(bounds.max_x); - const max_z: f32 = @floatFromInt(bounds.max_z); - const min_y = bounds.min_y; - const max_y = bounds.max_y; - - const center = Vec3.init( - (min_x + max_x) * 0.5 - camera_pos.x, - (min_y + max_y) * 0.5 - camera_pos.y, - (min_z + max_z) * 0.5 - camera_pos.z, - ); - const half_x = (max_x - min_x) * 0.5; - const half_y = @max((max_y - min_y) * 0.5, 1.0); - const half_z = (max_z - min_z) * 0.5; - const radius = @sqrt(half_x * half_x + half_y * half_y + half_z * half_z); - return frustum.intersectsSphere(center, radius); -} - -// Tests -test "benchmark GPU-culling build option requests telemetry without environment" { - try std.testing.expect(gpuCullingRequested(true, false)); - try std.testing.expect(!gpuCullingRequested(false, false)); -} - -test "GPU culling planes match the canonical high-altitude frustum" { - const camera = Vec3.init(0.0, 900.0, 0.0); - const target = Vec3.init(256.0, 64.0, -384.0); - const view = Mat4.lookAt(Vec3.zero, target.sub(camera), Vec3.init(0.0, 0.0, -1.0)); - const projection = Mat4.perspectiveReverseZ(std.math.pi / 3.0, 16.0 / 9.0, 0.5, 20_000.0); - const view_proj = projection.multiply(view); - const canonical = Frustum.fromViewProj(view_proj); - const gpu_planes = extractPlanes(view_proj); - - for (canonical.planes, gpu_planes) |expected, actual| { - try std.testing.expectApproxEqAbs(expected.normal.x, actual[0], 0.0001); - try std.testing.expectApproxEqAbs(expected.normal.y, actual[1], 0.0001); - try std.testing.expectApproxEqAbs(expected.normal.z, actual[2], 0.0001); - try std.testing.expectApproxEqAbs(expected.distance, actual[3], 0.0001); - } -} - -test "compact grid variants retain exact decimated topology" { - for (COMPACT_GRID_WIDTHS, 0..) |width, expected_variant| { - try std.testing.expectEqual(expected_variant, compactGridVariant(width).?); - inline for (.{ false, true }) |include_skirts| { - const indices = try compactGridIndices(std.testing.allocator, width, include_skirts); - defer std.testing.allocator.free(indices); - try std.testing.expectEqual(compactGridIndexCount(width, include_skirts), indices.len); - const vertex_count = @as(u32, width) * width + if (include_skirts) 4 * width else 0; - for (indices) |index| try std.testing.expect(index < vertex_count); - } - } - try std.testing.expect(compactGridVariant(7) == null); -} - -test "compact index topology uploads in the first render frame before becoming drawable" { - const MockState = struct { - next_handle: u32 = 1, - uploads: u32 = 0, - destroys: u32 = 0, - }; - const MockRHI = struct { - state: *MockState, - - pub fn createBuffer(self: @This(), _: usize, _: anytype) !u32 { - const handle = self.state.next_handle; - self.state.next_handle += 1; - return handle; - } - pub fn uploadBuffer(self: @This(), _: u32, data: []const u8) !void { - try std.testing.expect(data.len > 0); - self.state.uploads += 1; - } - pub fn destroyBuffer(self: @This(), _: u32) void { - self.state.destroys += 1; - } - pub fn waitIdle(_: @This()) void {} - }; - - var state = MockState{}; - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(std.testing.allocator, .{ .state = &state }); - defer renderer.deinit(); - - try std.testing.expectEqual(@as(u32, 0), state.uploads); - renderer.frame_serial = 7; - try std.testing.expect(!renderer.ensureCompactIndexBuffers(7)); - try std.testing.expectEqual(@as(u32, 22), state.uploads); - try std.testing.expectEqual(@as(rhi_types.BufferHandle, 0), renderer.compactIndexBuffer(.lod4, 65, .terrain)); - - renderer.frame_serial = 8; - try std.testing.expect(renderer.ensureCompactIndexBuffers(8)); - try std.testing.expect(renderer.compactIndexBuffer(.lod4, 65, .terrain) != 0); - try std.testing.expectEqual(@as(u32, 22), state.uploads); -} - -test "ready detail disk stops at the first incomplete chunk ring" { - const CheckerState = struct { - missing_x: i32, - missing_z: i32, - - fn isLoaded(cx: i32, cz: i32, ctx: *anyopaque) bool { - const state: *@This() = @ptrCast(@alignCast(ctx)); - return cx != state.missing_x or cz != state.missing_z; - } - }; - - try std.testing.expectEqual(@as(i32, -1), contiguousReadyDiskRadius(null, null, 0, 0, 4)); - - var state = CheckerState{ .missing_x = -7, .missing_z = 3 }; - try std.testing.expectEqual(@as(i32, -1), contiguousReadyDiskRadius(CheckerState.isLoaded, &state, -7, 3, 4)); - - state = .{ .missing_x = -5, .missing_z = 3 }; - try std.testing.expectEqual(@as(i32, 1), contiguousReadyDiskRadius(CheckerState.isLoaded, &state, -7, 3, 4)); - - state = .{ .missing_x = 100, .missing_z = 100 }; - try std.testing.expectEqual(@as(i32, 4), contiguousReadyDiskRadius(CheckerState.isLoaded, &state, -7, 3, 4)); -} - -test "ready detail disk mask uses sign encoding" { - try std.testing.expectEqual(@as(f32, 0.5), readyDiskMaskRadius(-1)); - try std.testing.expectEqual(@as(f32, -1.0), readyDiskMaskRadius(0)); - try std.testing.expectEqual(@as(f32, -16.0), readyDiskMaskRadius(1)); - try std.testing.expectEqual(@as(f32, -64.0), readyDiskMaskRadius(4)); -} - -test "LODRenderer init/deinit lifecycle" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - buffers_created: u32 = 0, - buffers_destroyed: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(self: @This(), _: usize, _: anytype) !u32 { - self.state.buffers_created += 1; - return self.state.buffers_created; - } - pub fn destroyBuffer(self: @This(), _: u32) void { - self.state.buffers_destroyed += 1; - } - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn draw(_: @This(), _: u32, _: u32, _: anytype) void {} - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - - // Verify init created instance + indirect buffers for each frame in flight. - try std.testing.expectEqual(@as(u32, rhi_types.MAX_FRAMES_IN_FLIGHT * 2), mock_state.buffers_created); - try std.testing.expectEqual(@as(u32, 0), mock_state.buffers_destroyed); - - renderer.compact_pool.buffer_handle = 999; - const memory = renderer.memoryStats(); - try std.testing.expectEqual(@as(usize, 0), memory.pool_gpu_capacity_bytes); - try std.testing.expectEqual(@as(usize, 0), memory.pool_gpu_allocated_bytes); - try std.testing.expectEqual(@as(usize, 0), memory.pool_gpu_slack_bytes); - try std.testing.expectEqual(@as(usize, CompactLODPool.CAPACITY_BYTES), memory.compact_pool_capacity_bytes); - renderer.compact_pool.buffer_handle = 0; - - renderer.deinit(); - - // Verify deinit destroyed all buffers - try std.testing.expectEqual(@as(u32, rhi_types.MAX_FRAMES_IN_FLIGHT * 2), mock_state.buffers_destroyed); -} - -test "LODRenderer batches pooled meshes into per-LOD indirect draws" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - next_handle: u32 = 1, - draw_indirect_calls: u32 = 0, - direct_draw_calls: u32 = 0, - instance_updates: u32 = 0, - indirect_updates: u32 = 0, - last_draw_count: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(self: @This(), _: usize, usage: anytype) !u32 { - _ = usage; - const handle = self.state.next_handle; - self.state.next_handle += 1; - return handle; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn updateBuffer(self: @This(), _: u32, _: usize, data: []const u8) !void { - if (data.len % @sizeOf(rhi_types.InstanceData) == 0 and data.len != @sizeOf(rhi_types.DrawIndirectCommand)) { - self.state.instance_updates += 1; - } else { - self.state.indirect_updates += 1; - } - } - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn supportsIndirectFirstInstance(_: @This()) bool { - return true; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(_: @This(), _: u32, _: u32, _: anytype) void {} - pub fn drawOffset(self: @This(), _: u32, _: u32, _: anytype, _: usize) void { - self.state.direct_draw_calls += 1; - } - pub fn drawIndirect(self: @This(), _: u32, _: u32, _: usize, draw_count: u32, _: u32) void { - self.state.draw_indirect_calls += 1; - self.state.last_draw_count += draw_count; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - // Explicitly exercise the default-on MDI path. - renderer.enable_mdi = true; - - renderer.vertex_pools[1].buffer_handle = 101; - renderer.vertex_pools[2].buffer_handle = 102; - - var mesh_lod1 = LODMesh.init(allocator, .lod1); - mesh_lod1.buffer_handle = 101; - mesh_lod1.vertex_offset = 0; - mesh_lod1.vertex_count = 12; - mesh_lod1.pooled = true; - mesh_lod1.ready = true; - - var mesh_lod2 = LODMesh.init(allocator, .lod2); - mesh_lod2.buffer_handle = 102; - mesh_lod2.vertex_offset = 4 * @sizeOf(rhi_types.Vertex); - mesh_lod2.vertex_count = 18; - mesh_lod2.pooled = true; - mesh_lod2.ready = true; - - var chunk_lod1 = LODChunk.init(4, 0, .lod1); - chunk_lod1.state = .renderable; - var chunk_lod2 = LODChunk.init(8, 0, .lod2); - chunk_lod2.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - try meshes[1].put(.{ .rx = 4, .rz = 0, .lod = .lod1 }, &mesh_lod1); - try regions[1].put(.{ .rx = 4, .rz = 0, .lod = .lod1 }, &chunk_lod1); - try meshes[2].put(.{ .rx = 8, .rz = 0, .lod = .lod2 }, &mesh_lod2); - try regions[2].put(.{ .rx = 8, .rz = 0, .lod = .lod2 }, &chunk_lod2); - - var mock_config = LODConfig{ .radii = .{ 16, 128, 256, 512, 1024 } }; - var profiling = LODProfilingCollector.init(true); - renderer.renderFrame(99, &meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, mock_config.chunk_render_radius, .terrain, null, &profiling); - - try std.testing.expectEqual(@as(u32, 2), mock_state.draw_indirect_calls); - try std.testing.expectEqual(@as(u32, 2), mock_state.last_draw_count); - - // Water reuses the pooled indirect path rather than falling back to one - // direct submission per region. - mesh_lod1.water_vertex_offset = 12 * @sizeOf(rhi_types.Vertex); - mesh_lod1.water_vertex_count = 6; - mesh_lod2.water_vertex_offset = 18 * @sizeOf(rhi_types.Vertex); - mesh_lod2.water_vertex_count = 9; - renderer.renderFrame(99, &meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, mock_config.chunk_render_radius, .fluid, null, &profiling); - try std.testing.expectEqual(@as(u32, 4), mock_state.draw_indirect_calls); - try std.testing.expectEqual(@as(u32, 0), mock_state.direct_draw_calls); - const projection = profiling.snapshot().visibility_levels; - try std.testing.expectEqual(@as(u64, 1), projection[1].candidates); - try std.testing.expectEqual(@as(u64, 1), projection[2].candidates); - - // A direct-only mesh must not disable indirect submission for its pooled - // sibling. This is the upload-transition fallback used in production. - mesh_lod2.pooled = false; - renderer.renderFrame(99, &meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, mock_config.chunk_render_radius, .terrain, null, &profiling); - try std.testing.expectEqual(@as(u32, 5), mock_state.draw_indirect_calls); - try std.testing.expectEqual(@as(u32, 1), mock_state.direct_draw_calls); -} - -test "LODRenderer band fade follows configured fog start percent" { - var config = LODConfig{ - .radii = .{ 16, 40, 80, 160, 512 }, - .fog_start_percent = .{ 1.0, 0.5, 0.5, 0.5, 0.5 }, - }; - const iface = config.interface(); - - const near_bounds = ChunkBounds{ .min_x = 16, .min_z = 0, .max_x = 16, .max_z = 0 }; - try std.testing.expectEqual(@as(f32, 0.0), calculateBandFade(iface, .lod1, near_bounds, Vec3.zero)); - - const mid_bounds = ChunkBounds{ .min_x = 30, .min_z = 0, .max_x = 30, .max_z = 0 }; - try std.testing.expect(calculateBandFade(iface, .lod1, mid_bounds, Vec3.zero) > 0.0); - try std.testing.expect(calculateBandFade(iface, .lod1, mid_bounds, Vec3.zero) < 1.0); - - const far_bounds = ChunkBounds{ .min_x = 40, .min_z = 0, .max_x = 40, .max_z = 0 }; - try std.testing.expectEqual(@as(f32, 1.0), calculateBandFade(iface, .lod1, far_bounds, Vec3.zero)); -} - -test "LODRenderer transition fade distinguishes child fade-in and parent fade-out" { - var child = LODChunk.init(0, 0, .lod1); - child.transition_frames_remaining = lod_chunk.TRANSITION_FADE_FRAMES; - try std.testing.expectEqual(@as(f32, 0.0), child.transitionFadeProgress()); - - child.transition_frames_remaining = 0; - try std.testing.expectEqual(@as(f32, 1.0), child.transitionFadeProgress()); - - var parent = LODChunk.init(0, 0, .lod2); - parent.ready_children = 4; - parent.transition_frames_remaining = lod_chunk.TRANSITION_FADE_FRAMES; - try std.testing.expectEqual(@as(f32, 1.0), parent.transitionFadeProgress()); - - parent.transition_frames_remaining = 0; - try std.testing.expectEqual(@as(f32, 1.0), parent.transitionFadeProgress()); -} - -test "LODRenderer render draw path" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - set_matrix_calls: u32 = 0, - last_vertex_count: u32 = 0, - last_buffer_handle: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, _: f32) void { - self.state.set_matrix_calls += 1; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), handle: u32, count: u32, _: anytype) void { - self.state.draw_calls += 1; - self.state.last_buffer_handle = handle; - self.state.last_vertex_count = count; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - // MDI is enabled, but this RHI intentionally lacks indirect support. - // Rendering must preserve the direct fallback for both terrain and water. - renderer.enable_mdi = true; - - // Create mock mesh - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 42; - mesh.vertex_count = 106; - mesh.opaque_vertex_count = 100; - mesh.water_vertex_offset = 100 * @sizeOf(rhi_types.Vertex); - mesh.water_vertex_count = 6; - mesh.ready = true; - - // Create mock LODChunk in renderable state - var chunk = LODChunk.init(5, 0, .lod1); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - // Add mesh and region at LOD1 - const key = LODRegionKey{ .rx = 5, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{}; - - // Create view-projection matrix that includes origin (where our chunk is) - // Use identity for simplicity - frustum will include everything - const view_proj = Mat4.identity; - const camera_pos = Vec3.zero; - - var stats = LODStats{}; - - // Call render with explicit parameters - renderer.render(&meshes, ®ions, mock_config.interface(), view_proj, camera_pos, null, null, false, null, .terrain, &stats, null); - - // Verify draw was called with correct parameters - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 1), mock_state.set_matrix_calls); - try std.testing.expectEqual(@as(u32, 42), mock_state.last_buffer_handle); - try std.testing.expectEqual(@as(u32, 100), mock_state.last_vertex_count); - try std.testing.expectEqual(@as(u32, 1), stats.drawn[1]); - try std.testing.expectEqual(@as(u32, 1), stats.instances[1]); - - renderer.render(&meshes, ®ions, mock_config.interface(), view_proj, camera_pos, null, null, false, null, .fluid, &stats, null); - try std.testing.expectEqual(@as(u32, 2), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 6), mock_state.last_vertex_count); - try std.testing.expectEqual(@as(u32, 1), stats.drawn[1]); - try std.testing.expectEqual(@as(u32, 1), stats.instances[1]); - try std.testing.expectEqual(@as(u32, 1), stats.fluid_drawn[1]); - try std.testing.expectEqual(@as(u32, 1), stats.fluid_instances[1]); -} - -test "LODRenderer keeps coarse LOD visible while finer bands stream" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - set_matrix_calls: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, _: f32) void { - self.state.set_matrix_calls += 1; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod2); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - // Region (2,0) at LOD2 covers chunks 16..23. It sits inside the LOD1 radius, - // so inner-band culling would incorrectly suppress it if no finer LOD exists yet. - var chunk = LODChunk.init(2, 0, .lod2); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod2 }; - try meshes[2].put(key, &mesh); - try regions[2].put(key, &chunk); - - var mock_config = LODConfig{ - .radii = .{ 16, 32, 64, 100, 256 }, - }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 1), mock_state.set_matrix_calls); -} - -test "LODRenderer disables mask when chunks are missing inside chunk render radius" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - last_mask_radius: f32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, mask_radius: f32) void { - self.state.last_mask_radius = mask_radius; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - var chunk = LODChunk.init(0, 0, .lod1); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100, 256 } }; - var checker_ctx: u8 = 0; - const Checker = struct { - fn missingInRadius(_: i32, _: i32, _: *anyopaque) bool { - return false; - } - }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.missingInRadius, &checker_ctx, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(LOD_UNMASKED_SENTINEL, mock_state.last_mask_radius); -} - -test "LODRenderer chunk mask uses chunk render radius instead of LOD0 radius" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - last_mask_radius: f32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, mask_radius: f32) void { - self.state.last_mask_radius = mask_radius; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - // Region rx=3 sits inside the LOD0 ring radius below, but outside the - // configured full-chunk render radius. Missing chunks there must not unmask - // the inner chunk/LOD handoff. - var chunk = LODChunk.init(3, 0, .lod1); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 3, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{ - .chunk_render_radius = 4, - .radii = .{ 16, 32, 64, 100, 256 }, - }; - var checker_ctx: u8 = 0; - const Checker = struct { - fn missing(_: i32, _: i32, _: *anyopaque) bool { - return false; - } - }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.missing, &checker_ctx, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(mock_config.interface().calculateMaskRadius(), mock_state.last_mask_radius); -} - -test "LODRenderer keeps mask when only outside-radius chunks are uncovered" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - last_mask_radius: f32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, mask_radius: f32) void { - self.state.last_mask_radius = mask_radius; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - var chunk = LODChunk.init(4, 0, .lod1); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 4, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100, 256 } }; - var checker_ctx: u8 = 0; - const Checker = struct { - fn loaded(_: i32, _: i32, _: *anyopaque) bool { - return true; - } - }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.loaded, &checker_ctx, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(mock_config.interface().calculateMaskRadius(), mock_state.last_mask_radius); -} - -test "LODRenderer keeps mask for partially covered chunk regions" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - last_mask_radius: f32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, mask_radius: f32) void { - self.state.last_mask_radius = mask_radius; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - var chunk = LODChunk.init(0, 0, .lod1); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{ .chunk_render_radius = 4, .radii = .{ 16, 32, 64, 100, 256 } }; - var checker_ctx: u8 = 0; - const Checker = struct { - fn partiallyLoaded(cx: i32, cz: i32, _: *anyopaque) bool { - return cx == 0 and cz == 0; - } - }; - - renderer.renderFrame(1, &meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.partiallyLoaded, &checker_ctx, false, null, mock_config.chunk_render_radius, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(readyDiskMaskRadius(0), mock_state.last_mask_radius); -} - -test "LODRenderer skips coarse LOD when finer coverage is ready" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var coarse_mesh = LODMesh.init(allocator, .lod2); - coarse_mesh.buffer_handle = 7; - coarse_mesh.vertex_count = 12; - coarse_mesh.ready = true; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - var coarse_chunk = LODChunk.init(2, 0, .lod2); - coarse_chunk.state = .renderable; - coarse_chunk.ready_children = 4; - const coarse_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod2 }; - try meshes[2].put(coarse_key, &coarse_mesh); - try regions[2].put(coarse_key, &coarse_chunk); - - const finer_keys = [_]LODRegionKey{ - .{ .rx = 4, .rz = 0, .lod = .lod1 }, - .{ .rx = 5, .rz = 0, .lod = .lod1 }, - .{ .rx = 4, .rz = 1, .lod = .lod1 }, - .{ .rx = 5, .rz = 1, .lod = .lod1 }, - }; - var finer_chunks: [4]LODChunk = undefined; - var finer_meshes: [4]LODMesh = undefined; - for (finer_keys, 0..) |finer_key, idx| { - finer_chunks[idx] = LODChunk.init(finer_key.rx, finer_key.rz, .lod1); - finer_chunks[idx].state = .renderable; - finer_meshes[idx] = LODMesh.init(allocator, .lod1); - finer_meshes[idx].buffer_handle = @as(u32, @intCast(10 + idx)); - finer_meshes[idx].vertex_count = 24; - finer_meshes[idx].ready = true; - try meshes[1].put(finer_key, &finer_meshes[idx]); - try regions[1].put(finer_key, &finer_chunks[idx]); - } - - var mock_config = LODConfig{ - .radii = .{ 16, 32, 64, 100, 256 }, - }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls); -} - -test "LODRenderer always renders ready LOD0 regions" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod0); - mesh.buffer_handle = 7; - mesh.vertex_count = 12; - mesh.ready = true; - - var chunk = LODChunk.init(2, 0, .lod0); - chunk.state = .renderable; - chunk.ready_children = 4; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - const key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod0 }; - try meshes[0].put(key, &mesh); - try regions[0].put(key, &chunk); - - var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100, 256 } }; - var stats = LODStats{}; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, &stats, null); - - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 1), stats.drawn[0]); - try std.testing.expectEqual(@as(u32, 1), stats.instances[0]); -} - -test "LODRenderer keeps coarse LOD when a finer child is missing" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - handle_sum: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), handle: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - self.state.handle_sum += handle; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - var coarse_mesh = LODMesh.init(allocator, .lod2); - coarse_mesh.buffer_handle = 100; - coarse_mesh.vertex_count = 12; - coarse_mesh.ready = true; - var coarse_chunk = LODChunk.init(2, 0, .lod2); - coarse_chunk.state = .renderable; - coarse_chunk.ready_children = 3; - const coarse_key = LODRegionKey{ .rx = 2, .rz = 0, .lod = .lod2 }; - try meshes[2].put(coarse_key, &coarse_mesh); - try regions[2].put(coarse_key, &coarse_chunk); - - const finer_keys = [_]LODRegionKey{ - .{ .rx = 4, .rz = 0, .lod = .lod1 }, - .{ .rx = 5, .rz = 0, .lod = .lod1 }, - .{ .rx = 4, .rz = 1, .lod = .lod1 }, - }; - var finer_chunks: [finer_keys.len]LODChunk = undefined; - var finer_meshes: [finer_keys.len]LODMesh = undefined; - for (finer_keys, 0..) |finer_key, idx| { - finer_chunks[idx] = LODChunk.init(finer_key.rx, finer_key.rz, .lod1); - finer_chunks[idx].state = .renderable; - finer_meshes[idx] = LODMesh.init(allocator, .lod1); - finer_meshes[idx].buffer_handle = @as(u32, @intCast(idx + 1)); - finer_meshes[idx].vertex_count = 24; - finer_meshes[idx].ready = true; - try meshes[1].put(finer_key, &finer_meshes[idx]); - try regions[1].put(finer_key, &finer_chunks[idx]); - } - - var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100, 256 } }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 106), mock_state.handle_sum); -} - -test "LODRenderer resolves finer coverage across negative region boundaries" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - draw_calls: u32 = 0, - handle_sum: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setModelMatrix(_: @This(), _: Mat4, _: Vec3, _: f32) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setSelectionMode(_: @This(), _: bool) void {} - pub fn draw(self: @This(), handle: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - self.state.handle_sum += handle; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - var coarse_mesh = LODMesh.init(allocator, .lod2); - coarse_mesh.buffer_handle = 100; - coarse_mesh.vertex_count = 12; - coarse_mesh.ready = true; - var coarse_chunk = LODChunk.init(-1, -1, .lod2); - coarse_chunk.state = .renderable; - coarse_chunk.ready_children = 4; - const coarse_key = LODRegionKey{ .rx = -1, .rz = -1, .lod = .lod2 }; - try meshes[2].put(coarse_key, &coarse_mesh); - try regions[2].put(coarse_key, &coarse_chunk); - - const finer_keys = [_]LODRegionKey{ - .{ .rx = -2, .rz = -2, .lod = .lod1 }, - .{ .rx = -1, .rz = -2, .lod = .lod1 }, - .{ .rx = -2, .rz = -1, .lod = .lod1 }, - .{ .rx = -1, .rz = -1, .lod = .lod1 }, - }; - var finer_chunks: [finer_keys.len]LODChunk = undefined; - var finer_meshes: [finer_keys.len]LODMesh = undefined; - for (finer_keys, 0..) |finer_key, idx| { - finer_chunks[idx] = LODChunk.init(finer_key.rx, finer_key.rz, .lod1); - finer_chunks[idx].state = .renderable; - finer_meshes[idx] = LODMesh.init(allocator, .lod1); - finer_meshes[idx].buffer_handle = @as(u32, @intCast(idx + 1)); - finer_meshes[idx].vertex_count = 24; - finer_meshes[idx].ready = true; - try meshes[1].put(finer_key, &finer_meshes[idx]); - try regions[1].put(finer_key, &finer_chunks[idx]); - } - - var mock_config = LODConfig{ .radii = .{ 16, 32, 64, 100, 256 } }; - - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null, null); - - try std.testing.expectEqual(@as(u32, 4), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 10), mock_state.handle_sum); -} - -test "LODRenderer counts only consecutive compact backend draw failures" { - const allocator = std.testing.allocator; - const MockState = struct { - draw_available: bool = true, - draw_calls: u32 = 0, - }; - const MockRHI = struct { - state: *MockState, - - pub fn createBuffer(_: @This(), _: usize, _: anytype) !u32 { - return 1; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn uploadBuffer(_: @This(), _: u32, _: []const u8) !void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setLODDescriptorStream(_: @This(), _: rhi_types.LODDescriptorStream) void {} - pub fn setLODCompactSampleBuffer(_: @This(), _: u32) void {} - pub fn drawCompactLOD(self: @This(), _: u32, _: u32, _: anytype) bool { - self.state.draw_calls += 1; - return self.state.draw_available; - } - }; - - var state = MockState{}; - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, .{ .state = &state }); - defer renderer.deinit(); - - var mesh = LODMesh.init(allocator, .lod3); - mesh.compact = true; - mesh.compact_tile_width = 3; - mesh.compact_index_count = 24; - mesh.ready = true; - mesh.vertex_count = 24; - const visible = VisibleRegion{ .key = .{ .rx = 0, .rz = 0, .lod = .lod3 }, .model = Mat4.identity, .mask_radius = 1, .lod_fade = 1 }; - - // A first frame without a ready compact sample buffer is retryable. It - // must not disable compact before a backend submission was attempted. - try std.testing.expectEqual(CompactRenderResult.unavailable, renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain)); - try std.testing.expect(mesh.isRenderable()); - - renderer.compact_pool.buffer_handle = 7; - try std.testing.expectEqual(CompactRenderResult.drawn, renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain)); - try std.testing.expect(mesh.isRenderable()); - - state.draw_available = false; - var profiling = LODProfilingCollector.init(true); - const first = renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain); - renderer.noteCompactRenderFailure(&mesh, first, &profiling); - try std.testing.expectEqual(CompactRenderResult.backend_failed, first); - try std.testing.expect(mesh.isRenderable()); - - // A confirmed draw breaks the failure streak. A transient unavailable - // frame remains a non-submission and must not affect that reset state. - state.draw_available = true; - try std.testing.expectEqual(CompactRenderResult.drawn, renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain)); - try std.testing.expectEqual(@as(u8, 0), mesh.compact_backend_draw_failures); - renderer.compact_pool.buffer_handle = 0; - const unavailable = renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain); - renderer.noteCompactRenderFailure(&mesh, unavailable, &profiling); - try std.testing.expectEqual(CompactRenderResult.unavailable, unavailable); - try std.testing.expectEqual(@as(u8, 0), mesh.compact_backend_draw_failures); - - renderer.compact_pool.buffer_handle = 7; - state.draw_available = false; - var attempt: u8 = 0; - while (attempt < LODMesh.COMPACT_BACKEND_FAILURE_LIMIT) : (attempt += 1) { - const repeated = renderer.renderCompactMesh(renderer.rhi, visible, &mesh, .terrain); - renderer.noteCompactRenderFailure(&mesh, repeated, &profiling); - try std.testing.expectEqual(CompactRenderResult.backend_failed, repeated); - try std.testing.expectEqual(attempt + 1 < LODMesh.COMPACT_BACKEND_FAILURE_LIMIT, mesh.isRenderable()); - } - try std.testing.expect(!mesh.isRenderable()); - try std.testing.expectEqual(@as(u64, LODMesh.COMPACT_BACKEND_FAILURE_LIMIT + 1), profiling.snapshot().compact_draw_failures); - try std.testing.expectEqual(@as(u64, 1), profiling.snapshot().compact_draw_unavailable); -} - -test "LODRenderer renderFrame times confirmed compact direct terrain and water submissions" { - const allocator = std.testing.allocator; - const State = struct { - next_handle: u32 = 1, - compact_draw_succeeds: bool = true, - compact_draws: u32 = 0, - terrain_timing_begins: u32 = 0, - terrain_timing_ends: u32 = 0, - water_timing_begins: u32 = 0, - water_timing_ends: u32 = 0, - }; - const MockRHI = struct { - state: *State, - - const Timing = struct { - state: *State, - - fn beginPassTiming(self: @This(), name: []const u8) void { - if (std.mem.eql(u8, name, "LODCompactTerrainPass")) self.state.terrain_timing_begins += 1; - if (std.mem.eql(u8, name, "LODCompactWaterPass")) self.state.water_timing_begins += 1; - } - fn endPassTiming(self: @This(), name: []const u8) void { - if (std.mem.eql(u8, name, "LODCompactTerrainPass")) self.state.terrain_timing_ends += 1; - if (std.mem.eql(u8, name, "LODCompactWaterPass")) self.state.water_timing_ends += 1; - } - }; - - pub fn createBuffer(self: @This(), _: usize, _: anytype) !u32 { - const handle = self.state.next_handle; - self.state.next_handle += 1; - return handle; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn uploadBuffer(_: @This(), _: u32, _: []const u8) !void {} - pub fn waitIdle(_: @This()) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn setInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setLODDescriptorStream(_: @This(), _: rhi_types.LODDescriptorStream) void {} - pub fn setLODCompactSampleBuffer(_: @This(), _: u32) void {} - pub fn drawCompactLOD(self: @This(), _: u32, _: u32, _: anytype) bool { - self.state.compact_draws += 1; - return self.state.compact_draw_succeeds; - } - pub fn timing(self: @This()) Timing { - return .{ .state = self.state }; - } - }; - - var state = State{}; - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, .{ .state = &state }); - defer renderer.deinit(); - renderer.compact_pool.buffer_handle = 99; - - var mesh = LODMesh.init(allocator, .lod3); - mesh.compact = true; - mesh.compact_tile_width = 3; - mesh.compact_index_count = 24; - mesh.compact_has_water = true; - mesh.vertex_count = 24; - mesh.ready = true; - var chunk = LODChunk.init(0, 0, .lod3); - chunk.state = .renderable; - - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - }; - const key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod3 }; - try meshes[@intFromEnum(LODLevel.lod3)].put(key, &mesh); - try regions[@intFromEnum(LODLevel.lod3)].put(key, &chunk); - - var config = LODConfig{ .radii = .{ 16, 32, 64, 128, 256 } }; - var profiling = LODProfilingCollector.init(true); - renderer.renderFrame(1, &meshes, ®ions, config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, config.chunk_render_radius, .terrain, null, &profiling); - renderer.renderFrame(1, &meshes, ®ions, config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, config.chunk_render_radius, .fluid, null, &profiling); - - try std.testing.expectEqual(@as(u32, 2), state.compact_draws); - try std.testing.expectEqual(state.terrain_timing_begins, state.terrain_timing_ends); - try std.testing.expectEqual(state.water_timing_begins, state.water_timing_ends); - try std.testing.expectEqual(@as(u32, 1), state.terrain_timing_begins); - try std.testing.expectEqual(@as(u32, 1), state.water_timing_begins); - try std.testing.expectEqual(@as(u64, 2), profiling.snapshot().compact_submissions); - - state.compact_draw_succeeds = false; - renderer.renderFrame(2, &meshes, ®ions, config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, config.chunk_render_radius, .terrain, null, &profiling); - try std.testing.expectEqual(state.terrain_timing_begins, state.terrain_timing_ends); - try std.testing.expectEqual(@as(u32, 2), state.terrain_timing_begins); - try std.testing.expectEqual(@as(u64, 2), profiling.snapshot().compact_submissions); -} - -test "LODRenderer submits fixed-capacity compact streams without indirect-count support" { - const allocator = std.testing.allocator; - const State = struct { - next_handle: u32 = 1, - indirect_succeeds: bool = true, - compact_indirect_draws: u32 = 0, - timing_begins: u32 = 0, - timing_ends: u32 = 0, - }; - const MockRHI = struct { - state: *State, - - const Timing = struct { - state: *State, - - fn beginPassTiming(self: @This(), name: []const u8) void { - if (std.mem.eql(u8, name, "LODCompactTerrainPass")) self.state.timing_begins += 1; - } - fn endPassTiming(self: @This(), name: []const u8) void { - if (std.mem.eql(u8, name, "LODCompactTerrainPass")) self.state.timing_ends += 1; - } - }; - - pub fn createBuffer(self: @This(), _: usize, _: anytype) !u32 { - const handle = self.state.next_handle; - self.state.next_handle += 1; - return handle; - } - pub fn destroyBuffer(_: @This(), _: u32) void {} - pub fn uploadBuffer(_: @This(), _: u32, _: []const u8) !void {} - pub fn waitIdle(_: @This()) void {} - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn supportsIndirectFirstInstance(_: @This()) bool { - return true; - } - pub fn supportsIndirectCount(_: @This()) bool { - return false; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn setLODDescriptorStream(_: @This(), _: rhi_types.LODDescriptorStream) void {} - pub fn setLODCompactSampleBuffer(_: @This(), _: u32) void {} - pub fn setLODCompactInstanceBuffer(_: @This(), _: u32) void {} - pub fn drawIndirectCount(_: @This(), _: u32, _: u32, _: usize, _: u32, _: u32, _: u32, _: u32) bool { - return true; - } - pub fn drawCompactLODIndirectCount(self: @This(), _: u32, _: u32, _: usize, _: u32, _: usize, _: u32) bool { - self.state.compact_indirect_draws += 1; - return self.state.indirect_succeeds; - } - pub fn timing(self: @This()) Timing { - return .{ .state = self.state }; - } - }; - const Culling = struct { - fn deinit(_: *anyopaque) void {} - fn dispatch(_: *anyopaque, _: usize, _: []const LODCullCandidate, _: LODCullDispatch) bool { - return true; - } - fn instanceBuffer(_: *anyopaque, _: usize, _: bool, _: bool) u32 { - return 11; - } - fn indirectBuffer(_: *anyopaque, _: usize, _: bool, _: bool) u32 { - return 12; - } - fn countBuffer(_: *anyopaque, _: usize) u32 { - return 13; - } - fn diagnostics(_: *anyopaque) rhi_types.LODCullDiagnostics { - return .{}; - } - const vtable = ILODCullingSystem.VTable{ - .deinit = deinit, - .dispatch = dispatch, - .instanceBuffer = instanceBuffer, - .indirectBuffer = indirectBuffer, - .countBuffer = countBuffer, - .diagnostics = diagnostics, - }; - }; - - var state = State{}; - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, .{ .state = &state }); - defer renderer.deinit(); - renderer.compact_pool.buffer_handle = 99; - renderer.gpu_culling_threshold = 1; - renderer.projection_frame = 1; - renderer.gpu_culling_ready_frame = 1; - renderer.gpu_culling = .{ .ptr = @ptrCast(&state), .vtable = &Culling.vtable }; - - var candidate = std.mem.zeroes(LODCullCandidate); - candidate.terrain_command = .{ .count = 24, .instance_count = 1, .first = 0 }; - candidate.compact_words[1] = 65; - candidate.lod_and_padding = .{ @intFromEnum(LODLevel.lod3), 1, 0, 0 }; - try renderer.gpu_candidates.append(allocator, candidate); - const first_key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod3 }; - try renderer.gpu_candidate_keys.append(allocator, first_key); - - var profiling = LODProfilingCollector.init(true); - try std.testing.expect(renderer.renderGpuCulledLayer(.terrain, null, &profiling, renderer.rhi, renderer.rhi)); - try std.testing.expectEqual(@as(u32, 1), state.compact_indirect_draws); - try std.testing.expectEqual(@as(u32, 1), state.timing_begins); - try std.testing.expectEqual(state.timing_begins, state.timing_ends); - try std.testing.expectEqual(@as(u64, 1), profiling.snapshot().compact_submissions); - - state.indirect_succeeds = false; - try std.testing.expect(!renderer.renderGpuCulledLayer(.terrain, null, &profiling, renderer.rhi, renderer.rhi)); - try std.testing.expectEqual(state.timing_begins, state.timing_ends); - try std.testing.expectEqual(@as(u64, 1), profiling.snapshot().compact_submissions); - - var second_candidate = candidate; - second_candidate.compact_words[1] = 33; - try renderer.gpu_candidates.append(allocator, second_candidate); - const second_key = LODRegionKey{ .rx = 1, .rz = 0, .lod = .lod3 }; - try renderer.gpu_candidate_keys.append(allocator, second_key); - try std.testing.expect(!renderer.gpuCandidateDraws(first_key, .terrain)); - try std.testing.expect(!renderer.gpuCandidateDraws(second_key, .terrain)); -} - -test "LODRenderer createGPUBridge and toInterface round-trip" { - const allocator = std.testing.allocator; - - const MockRHIState = struct { - upload_calls: u32 = 0, - destroy_calls: u32 = 0, - wait_idle_calls: u32 = 0, - draw_calls: u32 = 0, - set_matrix_calls: u32 = 0, - }; - - const MockRHI = struct { - state: *MockRHIState, - - pub fn createBuffer(self: @This(), _: usize, _: anytype) !u32 { - _ = self; - return 1; - } - pub fn destroyBuffer(self: @This(), _: u32) void { - self.state.destroy_calls += 1; - } - pub fn uploadBuffer(self: @This(), _: u32, _: []const u8) !void { - self.state.upload_calls += 1; - } - pub fn getFrameIndex(_: @This()) usize { - return 0; - } - pub fn waitIdle(self: @This()) void { - self.state.wait_idle_calls += 1; - } - pub fn setModelMatrix(self: @This(), _: Mat4, _: Vec3, _: f32) void { - self.state.set_matrix_calls += 1; - } - pub fn setLODInstanceBuffer(_: @This(), _: anytype) void {} - pub fn draw(self: @This(), _: u32, _: u32, _: anytype) void { - self.state.draw_calls += 1; - } - }; - - var mock_state = MockRHIState{}; - const mock_rhi = MockRHI{ .state = &mock_state }; - - const Renderer = LODRenderer(MockRHI); - const renderer = try Renderer.init(allocator, mock_rhi); - defer renderer.deinit(); - - // Test createGPUBridge round-trip - const bridge = renderer.createGPUBridge(); - - // Verify bridge.waitIdle calls through to MockRHI.waitIdle - bridge.waitIdle(); - try std.testing.expectEqual(@as(u32, 1), mock_state.wait_idle_calls); - - // Verify bridge.destroy calls through to MockRHI.destroyBuffer (via LODMesh.deinit) - var test_mesh = LODMesh.init(allocator, .lod1); - test_mesh.buffer_handle = 99; - bridge.destroy(&test_mesh); - try std.testing.expectEqual(@as(u32, 1), mock_state.destroy_calls); - try std.testing.expectEqual(@as(u32, 0), test_mesh.buffer_handle); // deinit zeroes handle - - // Test toInterface round-trip: render through type-erased interface - const iface = renderer.toInterface(); - - // Set up meshes/regions with a renderable chunk - var meshes: [LODLevel.count]MeshMap = undefined; - var regions: [LODLevel.count]RegionMap = undefined; - for (0..LODLevel.count) |i| { - meshes[i] = MeshMap.init(allocator); - regions[i] = RegionMap.init(allocator); - } - defer { - for (0..LODLevel.count) |i| { - meshes[i].deinit(); - regions[i].deinit(); - } - } - - var mesh = LODMesh.init(allocator, .lod1); - mesh.buffer_handle = 42; - mesh.vertex_count = 50; - mesh.ready = true; - - var chunk = LODChunk.init(5, 0, .lod1); - chunk.state = .renderable; - - const key = LODRegionKey{ .rx = 5, .rz = 0, .lod = .lod1 }; - try meshes[1].put(key, &mesh); - try regions[1].put(key, &chunk); - - var mock_config = LODConfig{}; - - // Render through the type-erased interface - iface.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null, null); - - // Verify the real renderer's draw was invoked through the interface - try std.testing.expectEqual(@as(u32, 1), mock_state.draw_calls); - try std.testing.expectEqual(@as(u32, 1), mock_state.set_matrix_calls); -} diff --git a/modules/world-lod/src/lod_scheduler.zig b/modules/world-lod/src/lod_scheduler.zig deleted file mode 100644 index e4ac8da8..00000000 --- a/modules/world-lod/src/lod_scheduler.zig +++ /dev/null @@ -1,753 +0,0 @@ -//! LOD region scheduling and job prioritization. - -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODConfig = lod_chunk.LODConfig; -const LODRegionKey = lod_chunk.LODRegionKey; -const ILODConfig = lod_chunk.ILODConfig; -const Vec3 = @import("engine-math").Vec3; -const engine_core = @import("engine-core"); -const JobQueue = engine_core.job_system.JobQueue; -const log = engine_core.log; -const sync = @import("sync"); -const lod_gpu = @import("lod_upload_queue.zig"); -const ChunkChecker = lod_gpu.ChunkChecker; -const RegionMap = lod_gpu.RegionMap; -const LifecycleQueue = @import("lod_manager_context.zig").LifecycleQueue; -const LifecycleToken = @import("lod_manager_context.zig").LifecycleToken; -const LODScanState = @import("lod_manager_context.zig").LODScanState; - -pub const CoverageFn = *const fn (ptr: *anyopaque, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool; - -pub const SchedulerContext = struct { - allocator: std.mem.Allocator, - config: ILODConfig, - radii: [LODLevel.count]i32, - active_lod_count: usize, - regions: *[LODLevel.count]RegionMap, - gen_queues: *[LODLevel.count]*JobQueue, - mutex: *sync.RwLock, - player_cx: i32, - player_cz: i32, - scan_states: *[LODLevel.count]LODScanState, - next_job_token: *u32, - cleanup_covered_regions: bool, - coverage_ptr: *anyopaque, - are_all_chunks_loaded: CoverageFn, - // Dynamic per-level radius reduction (hysteresis under memory pressure). - // Applied to all levels EXCEPT the coarsest (horizon is never shrunk). - radius_reduction: *const [LODLevel.count]i32, - // When true, queueLODRegions marks chunks queued_for_generation and lets - // LODManager perform main-thread cache reads before dispatching workers. - defer_generation_dispatch: bool = false, - // Shared admission count for queued or in-flight regions. The manager - // supplies this during normal updates; isolated scheduler tests may omit it. - pending_regions: ?*usize = null, - /// Resident-region and conservative logical-memory admission caps. - resident_region_limit: usize = MAX_LOD_REGIONS, - logical_memory_limit_bytes: usize = std.math.maxInt(usize), - logical_memory_bytes: ?*usize = null, - logical_region_reservation_bytes: usize = 0, - use_vertical_spans: bool = false, - /// Persistent bounded priority queue owned by the manager. `null` keeps - /// isolated scheduler tests and legacy direct-dispatch callers working. - generation_tokens: ?*LifecycleQueue = null, -}; - -const std = @import("std"); - -const QueueDiag = struct { - considered: u64 = 0, - outside_radius: u64 = 0, - covered_chunks: u64 = 0, - existing: u64 = 0, - candidates: u64 = 0, - queued: u64 = 0, -}; - -const LOD0_QUEUE_CANDIDATE_LIMIT: usize = 96; -const LOD1_QUEUE_CANDIDATE_LIMIT: usize = 64; -const HORIZON_QUEUE_CANDIDATE_LIMIT: usize = 64; -const REFINEMENT_QUEUE_CANDIDATE_LIMIT: usize = 48; -/// Hard per-update work budget. Large configured horizons advance through a -/// persistent ring cursor instead of blocking a frame on a full-area scan. -pub const MAX_LOD_SCAN_STEPS: usize = 512; -const MAX_PENDING_LOD_REGIONS = @import("lod_manager_context.zig").MAX_PENDING_LOD_REGIONS; -const MAX_LOD_REGIONS = @import("lod_manager_context.zig").MAX_LOD_REGIONS; - -fn regionCoordinateRepresentable(region: i64, scale: i32) bool { - const min_chunk = region * @as(i64, scale); - const max_chunk = min_chunk + @as(i64, scale) - 1; - return min_chunk >= std.math.minInt(i32) and max_chunk <= std.math.maxInt(i32); -} - -fn nextRingCoordinate(state: *LODScanState, player_rx: i32, player_rz: i32, region_radius: i64) [2]i64 { - if (state.next_ring > region_radius) { - state.next_ring = 0; - state.ring_index = 0; - } - if (state.next_ring == 0) { - state.next_ring = 1; - state.ring_index = 0; - return .{ player_rx, player_rz }; - } - - const ring = state.next_ring; - const side_length = ring * 2; - const perimeter_length = side_length * 4; - const index = state.ring_index; - const side = @divFloor(index, side_length); - const offset = @mod(index, side_length); - const relative = switch (side) { - 0 => [2]i64{ -ring + offset, -ring }, - 1 => [2]i64{ ring, -ring + offset }, - 2 => [2]i64{ ring - offset, ring }, - else => [2]i64{ -ring, ring - offset }, - }; - - state.ring_index += 1; - if (state.ring_index >= perimeter_length) { - state.next_ring += 1; - state.ring_index = 0; - } - return .{ @as(i64, player_rx) + relative[0], @as(i64, player_rz) + relative[1] }; -} - -fn updateScanOrigin(state: *LODScanState, player_rx: i32, player_rz: i32, effective_radius: i32, restart_on_move: bool) void { - const moved_rx = @as(i64, player_rx) - @as(i64, state.player_rx); - const moved_rz = @as(i64, player_rz) - @as(i64, state.player_rz); - if (state.effective_radius != effective_radius) { - state.* = .{ - .player_rx = player_rx, - .player_rz = player_rz, - .effective_radius = effective_radius, - }; - return; - } - if (player_rx == state.player_rx and player_rz == state.player_rz) return; - - state.player_rx = player_rx; - state.player_rz = player_rz; - // Preserve refinement progress during ordinary traversal. The coarsest - // level opts into restarting for every region-origin change so a moving - // player cannot leave an unvisited hole in the fallback disk. - if (restart_on_move or @max(@abs(moved_rx), @abs(moved_rz)) > 8) { - state.next_ring = 0; - state.ring_index = 0; - state.last_examined = 0; - } -} - -pub fn priorityRank(lod: LODLevel, active_lod_count: usize) usize { - const lod_idx: usize = @intFromEnum(lod); - const coarsest_idx = if (active_lod_count == 0) 0 else active_lod_count - 1; - return if (lod_idx == coarsest_idx) - 0 - else - lod_idx + 1; -} - -pub fn priorityLevelIndex(order_idx: usize, active_lod_count: usize) usize { - if (active_lod_count == 0) return 0; - if (order_idx == 0) return active_lod_count - 1; - return order_idx - 1; -} - -fn lodPriorityBias(lod: LODLevel, active_lod_count: usize) i32 { - const rank = priorityRank(lod, active_lod_count); - return @as(i32, @intCast(rank)) << 28; -} - -fn maxQueueCandidatesForLOD(lod: LODLevel, active_lod_count: usize) usize { - const lod_idx: usize = @intFromEnum(lod); - const coarsest_idx = if (active_lod_count == 0) 0 else active_lod_count - 1; - if (lod_idx == 0) return LOD0_QUEUE_CANDIDATE_LIMIT; - if (lod_idx == 1) return LOD1_QUEUE_CANDIDATE_LIMIT; - if (lod_idx == coarsest_idx) return HORIZON_QUEUE_CANDIDATE_LIMIT; - return REFINEMENT_QUEUE_CANDIDATE_LIMIT; -} - -pub fn priorityWeightForVelocity(velocity: Vec3, chunk_dx: i64, chunk_dz: i64) f32 { - const speed = @sqrt(velocity.x * velocity.x + velocity.z * velocity.z); - if (speed < 2.0) return 1.0; - - const cdx: f32 = @floatFromInt(chunk_dx); - const cdz: f32 = @floatFromInt(chunk_dz); - const dist = @sqrt(cdx * cdx + cdz * cdz); - if (dist < 0.001) return 0.5; - - const dir_x = velocity.x / speed; - const dir_z = velocity.z / speed; - const dot = (cdx * dir_x + cdz * dir_z) / dist; - return 1.0 - dot * 0.5; -} - -pub fn encodePriority(lod: LODLevel, chunk_dx: i64, chunk_dz: i64, velocity: Vec3, active_lod_count: usize) i32 { - const dx: f64 = @floatFromInt(chunk_dx); - const dz: f64 = @floatFromInt(chunk_dz); - const weighted = (dx * dx + dz * dz) * @as(f64, priorityWeightForVelocity(velocity, chunk_dx, chunk_dz)); - const priority: i32 = @intFromFloat(@min(weighted, @as(f64, @floatFromInt(@as(i32, 0x0FFFFFFF))))); - return (priority & 0x0FFFFFFF) | lodPriorityBias(lod, active_lod_count); -} - -/// Queue LOD regions that need generation. -pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque) !void { - // Do not rescan thousands of horizon candidates while the bounded - // lifecycle pipeline cannot admit another region. - ctx.mutex.lockShared(); - if (ctx.pending_regions) |pending| { - if (pending.* >= MAX_PENDING_LOD_REGIONS) { - ctx.mutex.unlockShared(); - return; - } - } - ctx.mutex.unlockShared(); - - const radii = ctx.radii; - const idx: u32 = @intFromEnum(lod); - // Apply dynamic radius reduction (hysteresis) to every level except the - // coarsest horizon band, which must keep filling regardless of pressure. - const is_coarsest = (idx + 1 >= LODLevel.count) or (@as(usize, idx + 1) >= ctx.active_lod_count); - const radius = if (is_coarsest) radii[idx] else @max(0, radii[idx] - ctx.radius_reduction[idx]); - - const scale: i32 = @intCast(lod.chunksPerSide()); - const region_radius = @divFloor(@as(i64, radius), @as(i64, scale)) + 1; - - const player_rx = @divFloor(ctx.player_cx, scale); - const player_rz = @divFloor(ctx.player_cz, scale); - - const diag_enabled = engine_core.envFlag("ZIGCRAFT_LOD_DIAG", false); - var diag = QueueDiag{}; - const active_lod_count = ctx.active_lod_count; - - // All LOD jobs go to the highest LOD queue. Encode the actual LOD in priority bits. - const queue = ctx.gen_queues[LODLevel.count - 1]; - const lod_idx: u3 = @intFromEnum(lod); - - // Keep only the bounded best candidates while walking the horizon. This - // avoids allocating/sorting an entry for every potential region. - const Candidate = struct { - key: LODRegionKey, - encoded_priority: i32, - scan_state_before: LODScanState, - }; - const max_candidates = maxQueueCandidatesForLOD(lod, active_lod_count); - var candidates = std.ArrayListUnmanaged(Candidate).empty; - defer candidates.deinit(ctx.allocator); - - // Existing active regions must not consume the bounded candidate window. - // A persistent concentric-ring cursor guarantees bounded frame work while - // eventually visiting every coordinate in the configured horizon. - ctx.mutex.lock(); - const candidate_storage = &ctx.regions[@intFromEnum(lod)]; - const state = &ctx.scan_states[@intFromEnum(lod)]; - updateScanOrigin(state, player_rx, player_rz, radius, is_coarsest); - if (state.next_ring > region_radius) { - state.next_ring = 0; - state.ring_index = 0; - } - - var examined: usize = 0; - while (examined < MAX_LOD_SCAN_STEPS and candidates.items.len < max_candidates) : (examined += 1) { - const scan_state_before = state.*; - const coordinate = nextRingCoordinate(state, player_rx, player_rz, region_radius); - const rx = coordinate[0]; - const rz = coordinate[1]; - diag.considered += 1; - if (!regionCoordinateRepresentable(rx, scale) or !regionCoordinateRepresentable(rz, scale)) continue; - const key = LODRegionKey{ .rx = @intCast(rx), .rz = @intCast(rz), .lod = lod }; - const chunk_bounds = key.chunkBounds(); - if (!chunk_bounds.intersectsRadius(ctx.player_cx, ctx.player_cz, radius)) { - diag.outside_radius += 1; - continue; - } - - var duplicate = false; - for (candidates.items) |candidate| { - if (candidate.key.eql(key)) { - duplicate = true; - break; - } - } - if (duplicate) continue; - - if (candidate_storage.get(key)) |chunk| { - diag.existing += 1; - if (chunk.getState() != .missing or chunk.isPinned()) continue; - } - - if (ctx.cleanup_covered_regions) { - if (chunk_checker) |checker| { - const temp_chunk = LODChunk.init(key.rx, key.rz, lod); - if (ctx.are_all_chunks_loaded(ctx.coverage_ptr, temp_chunk.worldBounds(), checker, checker_ctx.?)) { - diag.covered_chunks += 1; - continue; - } - } - } - - const center_cx = @as(i64, key.rx) * @as(i64, scale) + @divFloor(scale, 2); - const center_cz = @as(i64, key.rz) * @as(i64, scale) + @divFloor(scale, 2); - const distance_priority = encodePriority(lod, center_cx - @as(i64, ctx.player_cx), center_cz - @as(i64, ctx.player_cz), velocity, active_lod_count); - const candidate = Candidate{ - .key = key, - .encoded_priority = distance_priority, - .scan_state_before = scan_state_before, - }; - candidates.append(ctx.allocator, candidate) catch |err| { - ctx.mutex.unlock(); - return err; - }; - diag.candidates += 1; - } - state.last_examined = examined; - ctx.mutex.unlock(); - - var queued_count: usize = 0; - - ctx.mutex.lock(); - defer ctx.mutex.unlock(); - - const storage = &ctx.regions[@intFromEnum(lod)]; - var resident_regions: usize = 0; - for (ctx.regions) |region_map| resident_regions += region_map.count(); - for (candidates.items) |cand| { - // Candidate discovery advances the persistent scan cursor. If bounded - // admission cannot accept this coordinate, rewind to it so the next - // update resumes at the first actual coverage hole instead of skipping - // the remainder of a ring and producing directional strips. - if (queued_count >= max_candidates) { - state.* = cand.scan_state_before; - break; - } - if (ctx.pending_regions) |pending| { - if (pending.* >= MAX_PENDING_LOD_REGIONS) { - state.* = cand.scan_state_before; - break; - } - } - - const existing = storage.get(cand.key); - if (existing == null and resident_regions >= ctx.resident_region_limit) { - state.* = cand.scan_state_before; - break; - } - if (existing == null) if (ctx.logical_memory_bytes) |logical| { - const reservation = ctx.logical_region_reservation_bytes; - if (reservation > ctx.logical_memory_limit_bytes -| logical.*) { - state.* = cand.scan_state_before; - break; - } - }; - // A cancelled worker keeps the region pinned until it observes its - // cancellation signal. Do not reset that signal by dispatching a new - // generation for the same region concurrently. - const needs_queue = if (existing) |chunk| chunk.getState() == .missing and !chunk.isPinned() else true; - if (!needs_queue) continue; - - const chunk = if (existing) |c| c else blk: { - const c = try ctx.allocator.create(LODChunk); - errdefer ctx.allocator.destroy(c); - c.* = LODChunk.init(cand.key.rx, cand.key.rz, lod); - try storage.put(cand.key, c); - resident_regions += 1; - if (ctx.logical_memory_bytes) |logical| { - logical.* = std.math.add(usize, logical.*, ctx.logical_region_reservation_bytes) catch std.math.maxInt(usize); - } - break :blk c; - }; - - chunk.job_token = ctx.next_job_token.*; - ctx.next_job_token.* += 1; - chunk.job_priority = cand.encoded_priority; - chunk.preserve_job_priority = false; - if (ctx.defer_generation_dispatch) { - chunk.setState(.queued_for_generation); - if (ctx.generation_tokens) |tokens| { - _ = try tokens.push(ctx.allocator, .{ - .key = cand.key, - .job_token = chunk.job_token, - .source_revision = chunk.source_revision, - .priority = cand.encoded_priority, - .stage = .generation, - }); - } - } else { - chunk.resetCancellation(); - chunk.setState(.generating); - queue.push(.{ - .type = .chunk_generation, - .dist_sq = cand.encoded_priority, - .data = .{ - .chunk = .{ - .x = chunk.region_x, - .z = chunk.region_z, - .job_token = chunk.job_token, - .lod_level = lod_idx, - .coord_scale = scale, - .lod_radius = radii[idx], - .use_vertical_spans = ctx.use_vertical_spans, - }, - }, - }) catch |err| { - chunk.setState(.missing); - return err; - }; - } - diag.queued += 1; - queued_count += 1; - if (ctx.pending_regions) |pending| pending.* += 1; - } - - if (diag_enabled) { - const S = struct { - var counter: [LODLevel.count]u64 = .{0} ** LODLevel.count; - }; - const diag_lod_idx = @intFromEnum(lod); - S.counter[diag_lod_idx] += 1; - if (S.counter[diag_lod_idx] % 120 == 1) { - log.log.info("LOD_QUEUE_DIAG lod={} radius={} scale={} considered={} outside={} covered={} existing={} candidates={} queued={} player_chunk=({}, {})", .{ - diag_lod_idx, - radius, - scale, - diag.considered, - diag.outside_radius, - diag.covered_chunks, - diag.existing, - diag.candidates, - diag.queued, - ctx.player_cx, - ctx.player_cz, - }); - } - } -} - -test "LOD scheduling seeds horizon before detailed refinements" { - try std.testing.expect(lodPriorityBias(.lod4, LODLevel.count) < lodPriorityBias(.lod0, LODLevel.count)); - try std.testing.expect(lodPriorityBias(.lod0, LODLevel.count) < lodPriorityBias(.lod1, LODLevel.count)); - try std.testing.expect(lodPriorityBias(.lod1, LODLevel.count) < lodPriorityBias(.lod2, LODLevel.count)); - try std.testing.expect(lodPriorityBias(.lod2, LODLevel.count) < lodPriorityBias(.lod3, LODLevel.count)); - - try std.testing.expect(lodPriorityBias(.lod3, 4) < lodPriorityBias(.lod0, 4)); - try std.testing.expect(lodPriorityBias(.lod0, 4) < lodPriorityBias(.lod1, 4)); - - try std.testing.expectEqual(@as(usize, 4), priorityLevelIndex(0, LODLevel.count)); - try std.testing.expectEqual(@as(usize, 0), priorityLevelIndex(1, LODLevel.count)); - try std.testing.expectEqual(@as(usize, 1), priorityLevelIndex(2, LODLevel.count)); - try std.testing.expectEqual(@as(usize, 2), priorityLevelIndex(3, LODLevel.count)); - try std.testing.expectEqual(@as(usize, 3), priorityLevelIndex(4, LODLevel.count)); -} - -test "LOD scheduling preserves ring progress during ordinary movement" { - var state = LODScanState{ - .player_rx = 12, - .player_rz = -4, - .effective_radius = 1024, - .next_ring = 9, - .ring_index = 17, - .last_examined = MAX_LOD_SCAN_STEPS, - }; - - updateScanOrigin(&state, 13, -4, 1024, false); - - try std.testing.expectEqual(@as(i32, 13), state.player_rx); - try std.testing.expectEqual(@as(i32, -4), state.player_rz); - try std.testing.expectEqual(@as(i64, 9), state.next_ring); - try std.testing.expectEqual(@as(i64, 17), state.ring_index); - try std.testing.expectEqual(MAX_LOD_SCAN_STEPS, state.last_examined); - - updateScanOrigin(&state, 30, -4, 1024, false); - try std.testing.expectEqual(@as(i64, 0), state.next_ring); - try std.testing.expectEqual(@as(i64, 0), state.ring_index); - try std.testing.expectEqual(@as(usize, 0), state.last_examined); - - state = .{ .player_rx = 12, .player_rz = -4, .effective_radius = 1024, .next_ring = 9, .ring_index = 17 }; - updateScanOrigin(&state, 13, -4, 1024, true); - try std.testing.expectEqual(@as(i64, 0), state.next_ring); - try std.testing.expectEqual(@as(i64, 0), state.ring_index); -} - -test "LOD scheduling caps resident regions and logical admission memory" { - const allocator = std.testing.allocator; - - var regions: [LODLevel.count]RegionMap = undefined; - for (®ions) |*region_map| region_map.* = RegionMap.init(allocator); - defer { - for (®ions) |*region_map| { - var it = region_map.iterator(); - while (it.next()) |entry| { - const chunk = entry.value_ptr.*; - chunk.deinit(allocator); - allocator.destroy(chunk); - } - region_map.deinit(); - } - } - - var queues: [LODLevel.count]JobQueue = undefined; - var queue_ptrs: [LODLevel.count]*JobQueue = undefined; - for (&queues, 0..) |*queue, i| { - queue.* = JobQueue.init(allocator); - queue_ptrs[i] = queue; - } - defer for (&queues) |*queue| queue.deinit(); - - var config = LODConfig{ - .chunk_render_radius = 2, - .radii = .{ 5, 12, 24, 48, 96 }, - }; - const config_iface = config.interface(); - var mutex: sync.RwLock = .{}; - var next_job_token: u32 = 1; - var radius_reduction = [_]i32{0} ** LODLevel.count; - var scan_states = [_]LODScanState{LODScanState{}} ** LODLevel.count; - var pending_regions: usize = 0; - var logical_memory_bytes: usize = 0; - const reservation_bytes: usize = 1024; - var coverage_ctx: u8 = 0; - const Coverage = struct { - fn neverCovered(_: *anyopaque, _: LODChunk.WorldBounds, _: ChunkChecker, _: *anyopaque) bool { - return false; - } - }; - - try queueLODRegions(.{ - .allocator = allocator, - .config = config_iface, - .radii = config_iface.getRadii(), - .active_lod_count = lod_chunk.activeLODCount(config_iface), - .regions = ®ions, - .gen_queues = &queue_ptrs, - .mutex = &mutex, - .player_cx = 0, - .player_cz = 0, - .scan_states = &scan_states, - .next_job_token = &next_job_token, - .cleanup_covered_regions = false, - .coverage_ptr = &coverage_ctx, - .are_all_chunks_loaded = Coverage.neverCovered, - .radius_reduction = &radius_reduction, - .pending_regions = &pending_regions, - .resident_region_limit = 1, - .logical_memory_limit_bytes = reservation_bytes, - .logical_memory_bytes = &logical_memory_bytes, - .logical_region_reservation_bytes = reservation_bytes, - }, .lod0, Vec3.zero, null, null); - - const queue = queue_ptrs[LODLevel.count - 1]; - try std.testing.expectEqual(@as(usize, 1), queue.count()); - try std.testing.expectEqual(queue.count(), pending_regions); - try std.testing.expectEqual(reservation_bytes, logical_memory_bytes); - const job = queue.pop().?; - try std.testing.expectEqual(engine_core.job_system.JobType.chunk_generation, job.type); - try std.testing.expectEqual(@as(u3, 0), job.data.chunk.lod_level); - - const key = LODRegionKey{ .rx = job.data.chunk.x, .rz = job.data.chunk.z, .lod = .lod0 }; - const chunk = regions[0].get(key).?; - try std.testing.expectEqual(lod_chunk.LODState.generating, chunk.state); -} - -test "LOD scheduling fills nearby horizon fallback before distant regions" { - const allocator = std.testing.allocator; - - var regions: [LODLevel.count]RegionMap = undefined; - for (®ions) |*region_map| region_map.* = RegionMap.init(allocator); - defer { - for (®ions) |*region_map| { - var it = region_map.iterator(); - while (it.next()) |entry| { - const chunk = entry.value_ptr.*; - chunk.deinit(allocator); - allocator.destroy(chunk); - } - region_map.deinit(); - } - } - - var queues: [LODLevel.count]JobQueue = undefined; - var queue_ptrs: [LODLevel.count]*JobQueue = undefined; - for (&queues, 0..) |*queue, i| { - queue.* = JobQueue.init(allocator); - queue_ptrs[i] = queue; - } - defer for (&queues) |*queue| queue.deinit(); - - var config = LODConfig{ - .chunk_render_radius = 16, - .radii = .{ 4096, 8192, 16_384, 32_768, 131_072 }, - }; - const config_iface = config.interface(); - var mutex: sync.RwLock = .{}; - var next_job_token: u32 = 1; - var radius_reduction = [_]i32{0} ** LODLevel.count; - var scan_states = [_]LODScanState{LODScanState{}} ** LODLevel.count; - var coverage_ctx: u8 = 0; - const Coverage = struct { - fn neverCovered(_: *anyopaque, _: LODChunk.WorldBounds, _: ChunkChecker, _: *anyopaque) bool { - return false; - } - }; - const ctx = SchedulerContext{ - .allocator = allocator, - .config = config_iface, - .radii = config_iface.getRadii(), - .active_lod_count = lod_chunk.activeLODCount(config_iface), - .regions = ®ions, - .gen_queues = &queue_ptrs, - .mutex = &mutex, - .player_cx = 0, - .player_cz = 0, - .scan_states = &scan_states, - .next_job_token = &next_job_token, - .cleanup_covered_regions = false, - .coverage_ptr = &coverage_ctx, - .are_all_chunks_loaded = Coverage.neverCovered, - .radius_reduction = &radius_reduction, - }; - - try queueLODRegions(ctx, .lod0, Vec3.zero, null, null); - try queueLODRegions(ctx, .lod4, Vec3.zero, null, null); - try std.testing.expect(scan_states[@intFromEnum(LODLevel.lod0)].last_examined <= MAX_LOD_SCAN_STEPS); - try std.testing.expect(scan_states[@intFromEnum(LODLevel.lod4)].last_examined <= MAX_LOD_SCAN_STEPS); - - const queue = queue_ptrs[LODLevel.count - 1]; - const total = queue.count(); - try std.testing.expectEqual(LOD0_QUEUE_CANDIDATE_LIMIT + HORIZON_QUEUE_CANDIDATE_LIMIT, total); - - // A cold cache must not keep admitting work once the global pipeline is - // full; completed regions free capacity on subsequent manager updates. - var pending_regions = MAX_PENDING_LOD_REGIONS; - var capped_ctx = ctx; - capped_ctx.pending_regions = &pending_regions; - try queueLODRegions(capped_ctx, .lod1, Vec3.zero, null, null); - try std.testing.expectEqual(total, queue.count()); - - const first_job = queue.pop().?; - try std.testing.expectEqual(@intFromEnum(LODLevel.lod4), first_job.data.chunk.lod_level); - - var lod0_count: usize = 0; - var horizon_count: usize = 1; - var max_horizon_dist_sq: i64 = 0; - const recordHorizonDistance = struct { - fn record(max_dist_sq: *i64, job: engine_core.job_system.Job) void { - const scale: i32 = @intCast(LODLevel.lod4.chunksPerSide()); - const center_x: i64 = job.data.chunk.x * scale + @divFloor(scale, 2); - const center_z: i64 = job.data.chunk.z * scale + @divFloor(scale, 2); - max_dist_sq.* = @max(max_dist_sq.*, center_x * center_x + center_z * center_z); - } - }.record; - recordHorizonDistance(&max_horizon_dist_sq, first_job); - var i: usize = 1; - while (i < total) : (i += 1) { - const job = queue.pop().?; - if (job.data.chunk.lod_level == @intFromEnum(LODLevel.lod0)) lod0_count += 1; - if (job.data.chunk.lod_level == @intFromEnum(LODLevel.lod4)) { - horizon_count += 1; - recordHorizonDistance(&max_horizon_dist_sq, job); - } - } - - try std.testing.expectEqual(LOD0_QUEUE_CANDIDATE_LIMIT, lod0_count); - try std.testing.expectEqual(HORIZON_QUEUE_CANDIDATE_LIMIT, horizon_count); - // Coarsest fallback advances concentrically. A cold start must not spend - // its bounded admission budget on disconnected outer-horizon islands. - const nearby_limit_chunks: i64 = 6 * @as(i64, @intCast(LODLevel.lod4.chunksPerSide())); - try std.testing.expect(max_horizon_dist_sq <= nearby_limit_chunks * nearby_limit_chunks); -} - -test "LOD scheduling does not skip horizon coordinates with one admission slot" { - const allocator = std.testing.allocator; - - var regions: [LODLevel.count]RegionMap = undefined; - for (®ions) |*region_map| region_map.* = RegionMap.init(allocator); - defer { - for (®ions) |*region_map| { - var it = region_map.iterator(); - while (it.next()) |entry| { - const chunk = entry.value_ptr.*; - chunk.deinit(allocator); - allocator.destroy(chunk); - } - region_map.deinit(); - } - } - - var queues: [LODLevel.count]JobQueue = undefined; - var queue_ptrs: [LODLevel.count]*JobQueue = undefined; - for (&queues, 0..) |*queue, i| { - queue.* = JobQueue.init(allocator); - queue_ptrs[i] = queue; - } - defer for (&queues) |*queue| queue.deinit(); - - var config = LODConfig{ - .chunk_render_radius = 16, - .radii = .{ 64, 128, 256, 384, 512 }, - }; - const config_iface = config.interface(); - var mutex: sync.RwLock = .{}; - var next_job_token: u32 = 1; - var pending_regions: usize = MAX_PENDING_LOD_REGIONS - 1; - var radius_reduction = [_]i32{0} ** LODLevel.count; - var scan_states = [_]LODScanState{LODScanState{}} ** LODLevel.count; - var coverage_ctx: u8 = 0; - const Coverage = struct { - fn neverCovered(_: *anyopaque, _: LODChunk.WorldBounds, _: ChunkChecker, _: *anyopaque) bool { - return false; - } - }; - const ctx = SchedulerContext{ - .allocator = allocator, - .config = config_iface, - .radii = config_iface.getRadii(), - .active_lod_count = lod_chunk.activeLODCount(config_iface), - .regions = ®ions, - .gen_queues = &queue_ptrs, - .mutex = &mutex, - .player_cx = 0, - .player_cz = 0, - .scan_states = &scan_states, - .next_job_token = &next_job_token, - .cleanup_covered_regions = false, - .coverage_ptr = &coverage_ctx, - .are_all_chunks_loaded = Coverage.neverCovered, - .radius_reduction = &radius_reduction, - .pending_regions = &pending_regions, - }; - - // Repeatedly free exactly one pipeline slot. The scheduler must resume at - // the first unadmitted coordinate rather than advancing 64 candidates and - // leaving a directionally biased set of holes behind. - for (0..HORIZON_QUEUE_CANDIDATE_LIMIT) |_| { - try queueLODRegions(ctx, .lod4, Vec3.zero, null, null); - try std.testing.expectEqual(@as(usize, 1), queue_ptrs[LODLevel.count - 1].count()); - _ = queue_ptrs[LODLevel.count - 1].pop().?; - pending_regions = MAX_PENDING_LOD_REGIONS - 1; - } - - try std.testing.expectEqual(HORIZON_QUEUE_CANDIDATE_LIMIT, regions[@intFromEnum(LODLevel.lod4)].count()); - var z: i32 = -3; - while (z <= 3) : (z += 1) { - var x: i32 = -3; - while (x <= 3) : (x += 1) { - try std.testing.expect(regions[@intFromEnum(LODLevel.lod4)].contains(.{ .rx = x, .rz = z, .lod = .lod4 })); - } - } - var iter = regions[@intFromEnum(LODLevel.lod4)].keyIterator(); - while (iter.next()) |key| { - try std.testing.expect(@max(@abs(key.rx), @abs(key.rz)) <= 4); - } -} - -test "LOD scheduling biases priorities toward movement direction" { - const velocity = Vec3.init(10, 0, 0); - const ahead = encodePriority(.lod2, 10, 0, velocity, LODLevel.count) & 0x0FFFFFFF; - const behind = encodePriority(.lod2, -10, 0, velocity, LODLevel.count) & 0x0FFFFFFF; - const stationary_ahead = encodePriority(.lod2, 10, 0, Vec3.zero, LODLevel.count) & 0x0FFFFFFF; - const stationary_behind = encodePriority(.lod2, -10, 0, Vec3.zero, LODLevel.count) & 0x0FFFFFFF; - - try std.testing.expect(ahead < behind); - try std.testing.expectEqual(stationary_ahead, stationary_behind); -} diff --git a/modules/world-lod/src/lod_seam.zig b/modules/world-lod/src/lod_seam.zig deleted file mode 100644 index aa09ec10..00000000 --- a/modules/world-lod/src/lod_seam.zig +++ /dev/null @@ -1,154 +0,0 @@ -//! LOD transition seam stitching helpers. - -const std = @import("std"); -const LODLevel = @import("lod_types.zig").LODLevel; - -/// Edge direction for seam stitching. -pub const EdgeDir = enum { - north, // -Z - south, // +Z - east, // +X - west, // -X -}; - -/// Seam stitching configuration. -pub const SeamConfig = struct { - /// Enable seam stitching. - enabled: bool = true, - /// Number of blend cells at the edge. - blend_cells: u32 = 2, - /// Height interpolation factor (0 = this LOD, 1 = neighbor LOD). - blend_factor: f32 = 0.5, -}; - -/// Stitch LOD mesh edge to match neighbor LOD level. -/// This adjusts edge vertices to blend between LOD levels and prevent gaps. -pub fn stitchEdge( - mesh_heightmap: []f32, - mesh_width: u32, - neighbor_heightmap: []const f32, - neighbor_width: u32, - edge: EdgeDir, - this_lod: LODLevel, - neighbor_lod: LODLevel, - config: SeamConfig, -) void { - if (!config.enabled) return; - - const this_scale = this_lod.scale(); - const neighbor_scale = neighbor_lod.scale(); - - // Only stitch if neighbor is coarser (higher LOD number). - if (neighbor_scale <= this_scale) return; - - const scale_ratio = neighbor_scale / this_scale; - const blend_cells = @min(config.blend_cells, mesh_width / 4); - - switch (edge) { - .north => { - var x: u32 = 0; - while (x < mesh_width) : (x += 1) { - var z: u32 = 0; - while (z < blend_cells) : (z += 1) { - const idx = x + z * mesh_width; - if (idx >= mesh_heightmap.len) continue; - - const nx = x / scale_ratio; - const nz: u32 = 0; - const nidx = @min(nx + nz * neighbor_width, neighbor_width * neighbor_width - 1); - if (nidx >= neighbor_heightmap.len) continue; - - const this_h = mesh_heightmap[idx]; - const neighbor_h = neighbor_heightmap[nidx]; - const t = @as(f32, @floatFromInt(z)) / @as(f32, @floatFromInt(blend_cells)); - const blend = 1.0 - t; - mesh_heightmap[idx] = this_h * (1.0 - blend * config.blend_factor) + neighbor_h * blend * config.blend_factor; - } - } - }, - .south => { - var x: u32 = 0; - while (x < mesh_width) : (x += 1) { - var z: u32 = 0; - while (z < blend_cells) : (z += 1) { - const actual_z = mesh_width - 1 - z; - const idx = x + actual_z * mesh_width; - if (idx >= mesh_heightmap.len) continue; - - const nx = x / scale_ratio; - const nz = neighbor_width - 1; - const nidx = @min(nx + nz * neighbor_width, neighbor_width * neighbor_width - 1); - if (nidx >= neighbor_heightmap.len) continue; - - const this_h = mesh_heightmap[idx]; - const neighbor_h = neighbor_heightmap[nidx]; - const t = @as(f32, @floatFromInt(z)) / @as(f32, @floatFromInt(blend_cells)); - const blend = 1.0 - t; - mesh_heightmap[idx] = this_h * (1.0 - blend * config.blend_factor) + neighbor_h * blend * config.blend_factor; - } - } - }, - .west => { - var z: u32 = 0; - while (z < mesh_width) : (z += 1) { - var x: u32 = 0; - while (x < blend_cells) : (x += 1) { - const idx = x + z * mesh_width; - if (idx >= mesh_heightmap.len) continue; - - const nx: u32 = 0; - const nz = z / scale_ratio; - const nidx = @min(nx + nz * neighbor_width, neighbor_width * neighbor_width - 1); - if (nidx >= neighbor_heightmap.len) continue; - - const this_h = mesh_heightmap[idx]; - const neighbor_h = neighbor_heightmap[nidx]; - const t = @as(f32, @floatFromInt(x)) / @as(f32, @floatFromInt(blend_cells)); - const blend = 1.0 - t; - mesh_heightmap[idx] = this_h * (1.0 - blend * config.blend_factor) + neighbor_h * blend * config.blend_factor; - } - } - }, - .east => { - var z: u32 = 0; - while (z < mesh_width) : (z += 1) { - var x: u32 = 0; - while (x < blend_cells) : (x += 1) { - const actual_x = mesh_width - 1 - x; - const idx = actual_x + z * mesh_width; - if (idx >= mesh_heightmap.len) continue; - - const nx = neighbor_width - 1; - const nz = z / scale_ratio; - const nidx = @min(nx + nz * neighbor_width, neighbor_width * neighbor_width - 1); - if (nidx >= neighbor_heightmap.len) continue; - - const this_h = mesh_heightmap[idx]; - const neighbor_h = neighbor_heightmap[nidx]; - const t = @as(f32, @floatFromInt(x)) / @as(f32, @floatFromInt(blend_cells)); - const blend = 1.0 - t; - mesh_heightmap[idx] = this_h * (1.0 - blend * config.blend_factor) + neighbor_h * blend * config.blend_factor; - } - } - }, - } -} - -test "stitchEdge basic" { - var mesh_hm = [_]f32{ 100, 100, 100, 100, 90, 90, 90, 90, 80, 80, 80, 80, 70, 70, 70, 70 }; - const neighbor_hm = [_]f32{ 50, 50, 50, 50 }; - - stitchEdge( - &mesh_hm, - 4, - &neighbor_hm, - 2, - .north, - .lod1, - .lod2, - .{ .blend_cells = 2 }, - ); - - try std.testing.expect(mesh_hm[0] < 100); - try std.testing.expectEqual(@as(f32, 70), mesh_hm[12]); -} diff --git a/modules/world-lod/src/lod_stats.zig b/modules/world-lod/src/lod_stats.zig deleted file mode 100644 index 3f7efa2b..00000000 --- a/modules/world-lod/src/lod_stats.zig +++ /dev/null @@ -1,803 +0,0 @@ -//! LOD system statistics and aggregation helpers. - -const std = @import("std"); -const MonotonicTimer = @import("engine-core").time.MonotonicTimer; -const lod_types = @import("lod_types.zig"); -const LODLevel = lod_types.LODLevel; -const LODState = lod_types.LODState; - -/// Device-wide waits are never an ordinary streaming operation. Keep their -/// shutdown accounting separate so runtime streaming telemetry cannot be -/// made to look healthy by including teardown-only synchronization. -pub const LODWaitIdleReason = enum(u8) { - streaming, - shutdown, -}; - -/// Read-only LOD profiling data. Durations are cumulative CPU timings; memory -/// fields are current known allocations rather than GPU-driver measurements. -pub const LODProfilingSnapshot = struct { - enabled: bool = false, - update_ms: f64 = 0, - scheduling_ms: f64 = 0, - /// Cumulative dedicated cache-worker CPU/I/O time. This is intentionally - /// not update-thread blocking time; frame work only queues and drains. - cache_ms: f64 = 0, - generation_dispatch_ms: f64 = 0, - state_transition_ms: f64 = 0, - upload_prep_ms: f64 = 0, - upload_submission_ms: f64 = 0, - visibility_ms: f64 = 0, - coverage_ms: f64 = 0, - eviction_ms: f64 = 0, - worker_generation_ms: f64 = 0, - worker_mesh_construction_ms: f64 = 0, - /// Successful LOD3/LOD4 expanded CPU mesh builds performed by workers. - worker_far_expanded_mesh_construction_ms: f64 = 0, - /// Successful LOD3/LOD4 compact tile encodes performed by workers. - worker_compact_encode_ms: f64 = 0, - manager_lock_wait_ms: f64 = 0, - manager_lock_hold_ms: f64 = 0, - upload_bytes: u64 = 0, - /// Successful LOD3/LOD4 representation uploads. These deliberately omit - /// near pooled geometry and failed/retried submissions. - far_expanded_upload_bytes: u64 = 0, - compact_upload_bytes: u64 = 0, - pending_cpu_upload_bytes: u64 = 0, - /// Upload-budget deferrals plus RHI upload-pressure errors. - staging_pressure_count: u64 = 0, - visible_count: u64 = 0, - rejected_count: u64 = 0, - coverage_count: u64 = 0, - /// Per-LOD visibility projection telemetry. These counters are cumulative - /// and count a region once, even when terrain and water are both drawn. - visibility_levels: [LODLevel.count]LODVisibilityLevelSnapshot = [_]LODVisibilityLevelSnapshot{.{}} ** LODLevel.count, - /// Current known GPU allocation bytes retained by meshes awaiting destruction. - deferred_deletion_bytes: u64 = 0, - deferred_deletion_cpu_bytes: u64 = 0, - pool_gpu_capacity_bytes: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - pool_gpu_slack_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - compact_pool_capacity_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - compact_pool_free_bytes: u64 = 0, - compact_pool_retired_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - known_memory_bytes: u64 = 0, - wait_idle_count: u64 = 0, - wait_idle_ms: f64 = 0, - wait_idle_shutdown_count: u64 = 0, - wait_idle_shutdown_ms: f64 = 0, - /// Compact LOD lifecycle diagnostics. These are cumulative and let - /// production evidence distinguish selection from runtime fallback. - compact_selected: u64 = 0, - compact_build_rejected: u64 = 0, - compact_upload_failures: u64 = 0, - compact_draw_unavailable: u64 = 0, - compact_draw_failures: u64 = 0, - /// Confirmed compact direct draws plus successfully emitted compact GPU - /// indirect streams. This is evidence of submission, not allocation. - compact_submissions: u64 = 0, - compact_recoveries: u64 = 0, - compact_disabled: u64 = 0, - /// GPU-culling telemetry is renderer-owned and deliberately persistent: - /// manager/UI frame-stat resets must not erase benchmark evidence. - gpu_culling_requested: bool = false, - gpu_culling_threshold: u32 = 0, - gpu_culling_candidate_count: u32 = 0, - gpu_culling_candidate_count_max: u32 = 0, - gpu_culling_draw_submissions: u64 = 0, - gpu_culling_overflows: u32 = 0, - gpu_culling_validation_mismatches: u32 = 0, - gpu_culling_validation_generation: u64 = 0, - gpu_culling_validation_completed_generation: u64 = 0, - gpu_culling_validation_completed_count: u64 = 0, -}; - -pub const LODVisibilityLevelSnapshot = struct { - candidates: u64 = 0, - accepted: u64 = 0, - rejected_no_draw: u64 = 0, - rejected_not_ready: u64 = 0, - rejected_missing_region: u64 = 0, - rejected_not_renderable: u64 = 0, - rejected_finer_coverage: u64 = 0, - rejected_range: u64 = 0, - rejected_frustum: u64 = 0, - rejected_chunk_coverage: u64 = 0, - coverage_checks: u64 = 0, -}; - -const LODVisibilityLevelCounters = struct { - candidates: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - accepted: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_no_draw: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_not_ready: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_missing_region: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_not_renderable: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_finer_coverage: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_range: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_frustum: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - rejected_chunk_coverage: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - coverage_checks: std.atomic.Value(u64) = std.atomic.Value(u64).init(0), - - fn add(self: *LODVisibilityLevelCounters, value: LODVisibilityLevelSnapshot) void { - inline for (std.meta.fields(LODVisibilityLevelSnapshot)) |field| { - const amount = @field(value, field.name); - if (amount != 0) _ = @field(self, field.name).fetchAdd(amount, .monotonic); - } - } - - fn reset(self: *LODVisibilityLevelCounters) void { - inline for (std.meta.fields(LODVisibilityLevelSnapshot)) |field| { - @field(self, field.name).store(0, .monotonic); - } - } - - fn snapshot(self: *const LODVisibilityLevelCounters) LODVisibilityLevelSnapshot { - var result = LODVisibilityLevelSnapshot{}; - inline for (std.meta.fields(LODVisibilityLevelSnapshot)) |field| { - @field(result, field.name) = @field(self, field.name).load(.monotonic); - } - return result; - } -}; - -/// Thread-safe backing store for an `LODProfilingSnapshot`. -/// -/// Profiling is enabled once when an LOD manager is initialized. All timing -/// methods return before touching a clock when disabled, while worker updates -/// use atomics so they never race snapshot collection on the update thread. -pub const LODProfilingCollector = struct { - const AtomicU64 = std.atomic.Value(u64); - - pub const TimerKind = enum { - update, - scheduling, - cache, - generation_dispatch, - state_transition, - upload_prep, - upload_submission, - visibility, - coverage, - eviction, - worker_generation, - worker_mesh_construction, - worker_far_expanded_mesh_construction, - worker_compact_encode, - manager_lock_wait, - manager_lock_hold, - wait_idle, - }; - - enabled: bool = false, - update_ns: AtomicU64 = AtomicU64.init(0), - scheduling_ns: AtomicU64 = AtomicU64.init(0), - cache_ns: AtomicU64 = AtomicU64.init(0), - generation_dispatch_ns: AtomicU64 = AtomicU64.init(0), - state_transition_ns: AtomicU64 = AtomicU64.init(0), - upload_prep_ns: AtomicU64 = AtomicU64.init(0), - upload_submission_ns: AtomicU64 = AtomicU64.init(0), - visibility_ns: AtomicU64 = AtomicU64.init(0), - coverage_ns: AtomicU64 = AtomicU64.init(0), - eviction_ns: AtomicU64 = AtomicU64.init(0), - worker_generation_ns: AtomicU64 = AtomicU64.init(0), - worker_mesh_construction_ns: AtomicU64 = AtomicU64.init(0), - worker_far_expanded_mesh_construction_ns: AtomicU64 = AtomicU64.init(0), - worker_compact_encode_ns: AtomicU64 = AtomicU64.init(0), - manager_lock_wait_ns: AtomicU64 = AtomicU64.init(0), - manager_lock_hold_ns: AtomicU64 = AtomicU64.init(0), - wait_idle_ns: AtomicU64 = AtomicU64.init(0), - upload_bytes: AtomicU64 = AtomicU64.init(0), - far_expanded_upload_bytes: AtomicU64 = AtomicU64.init(0), - compact_upload_bytes: AtomicU64 = AtomicU64.init(0), - pending_cpu_upload_bytes: AtomicU64 = AtomicU64.init(0), - staging_pressure_count: AtomicU64 = AtomicU64.init(0), - visible_count: AtomicU64 = AtomicU64.init(0), - rejected_count: AtomicU64 = AtomicU64.init(0), - coverage_count: AtomicU64 = AtomicU64.init(0), - visibility_levels: [LODLevel.count]LODVisibilityLevelCounters = [_]LODVisibilityLevelCounters{.{}} ** LODLevel.count, - deferred_deletion_bytes: AtomicU64 = AtomicU64.init(0), - deferred_deletion_cpu_bytes: AtomicU64 = AtomicU64.init(0), - pool_gpu_capacity_bytes: AtomicU64 = AtomicU64.init(0), - pool_gpu_allocated_bytes: AtomicU64 = AtomicU64.init(0), - pool_gpu_slack_bytes: AtomicU64 = AtomicU64.init(0), - pool_cpu_shadow_bytes: AtomicU64 = AtomicU64.init(0), - compact_pool_capacity_bytes: AtomicU64 = AtomicU64.init(0), - compact_pool_allocated_bytes: AtomicU64 = AtomicU64.init(0), - compact_pool_free_bytes: AtomicU64 = AtomicU64.init(0), - compact_pool_retired_bytes: AtomicU64 = AtomicU64.init(0), - direct_mesh_gpu_bytes: AtomicU64 = AtomicU64.init(0), - known_memory_bytes: AtomicU64 = AtomicU64.init(0), - wait_idle_count: AtomicU64 = AtomicU64.init(0), - wait_idle_shutdown_count: AtomicU64 = AtomicU64.init(0), - wait_idle_shutdown_ns: AtomicU64 = AtomicU64.init(0), - compact_selected: AtomicU64 = AtomicU64.init(0), - compact_build_rejected: AtomicU64 = AtomicU64.init(0), - compact_upload_failures: AtomicU64 = AtomicU64.init(0), - compact_draw_unavailable: AtomicU64 = AtomicU64.init(0), - compact_draw_failures: AtomicU64 = AtomicU64.init(0), - compact_submissions: AtomicU64 = AtomicU64.init(0), - compact_recoveries: AtomicU64 = AtomicU64.init(0), - compact_disabled: AtomicU64 = AtomicU64.init(0), - gpu_culling_requested: AtomicU64 = AtomicU64.init(0), - gpu_culling_threshold: AtomicU64 = AtomicU64.init(0), - gpu_culling_candidate_count: AtomicU64 = AtomicU64.init(0), - gpu_culling_candidate_count_max: AtomicU64 = AtomicU64.init(0), - gpu_culling_draw_submissions: AtomicU64 = AtomicU64.init(0), - gpu_culling_overflows: AtomicU64 = AtomicU64.init(0), - gpu_culling_validation_mismatches: AtomicU64 = AtomicU64.init(0), - gpu_culling_validation_generation: AtomicU64 = AtomicU64.init(0), - gpu_culling_validation_completed_generation: AtomicU64 = AtomicU64.init(0), - gpu_culling_validation_completed_count: AtomicU64 = AtomicU64.init(0), - - pub fn init(enabled: bool) LODProfilingCollector { - return .{ .enabled = enabled }; - } - - pub fn begin(self: *const LODProfilingCollector) ?MonotonicTimer { - if (!self.enabled) return null; - return MonotonicTimer.start(); - } - - pub fn end(self: *LODProfilingCollector, kind: TimerKind, timer: ?MonotonicTimer) void { - const elapsed = timer orelse return; - _ = self.counterFor(kind).fetchAdd(elapsed.read(), .monotonic); - } - - pub fn add(self: *LODProfilingCollector, counter: *AtomicU64, value: u64) void { - if (!self.enabled or value == 0) return; - _ = counter.fetchAdd(value, .monotonic); - } - - pub fn addUploadBytes(self: *LODProfilingCollector, bytes: usize) void { - self.add(&self.upload_bytes, @intCast(bytes)); - } - - pub fn addFarExpandedUploadBytes(self: *LODProfilingCollector, bytes: usize) void { - self.add(&self.far_expanded_upload_bytes, @intCast(bytes)); - } - - pub fn addCompactUploadBytes(self: *LODProfilingCollector, bytes: usize) void { - self.add(&self.compact_upload_bytes, @intCast(bytes)); - } - - pub fn setPendingCpuUploadBytes(self: *LODProfilingCollector, bytes: usize) void { - if (!self.enabled) return; - self.pending_cpu_upload_bytes.store(@intCast(bytes), .monotonic); - } - - pub fn setMemoryAccounting(self: *LODProfilingCollector, pool_gpu_capacity: usize, pool_gpu_allocated: usize, pool_gpu_slack: usize, pool_cpu_shadow: usize, compact_pool_capacity: usize, compact_pool_allocated: usize, compact_pool_free: usize, compact_pool_retired: usize, direct_mesh_gpu: usize, known_memory: usize) void { - if (!self.enabled) return; - self.pool_gpu_capacity_bytes.store(@intCast(pool_gpu_capacity), .monotonic); - self.pool_gpu_allocated_bytes.store(@intCast(pool_gpu_allocated), .monotonic); - self.pool_gpu_slack_bytes.store(@intCast(pool_gpu_slack), .monotonic); - self.pool_cpu_shadow_bytes.store(@intCast(pool_cpu_shadow), .monotonic); - self.compact_pool_capacity_bytes.store(@intCast(compact_pool_capacity), .monotonic); - self.compact_pool_allocated_bytes.store(@intCast(compact_pool_allocated), .monotonic); - self.compact_pool_free_bytes.store(@intCast(compact_pool_free), .monotonic); - self.compact_pool_retired_bytes.store(@intCast(compact_pool_retired), .monotonic); - self.direct_mesh_gpu_bytes.store(@intCast(direct_mesh_gpu), .monotonic); - self.known_memory_bytes.store(@intCast(known_memory), .monotonic); - } - - pub fn addStagingPressure(self: *LODProfilingCollector) void { - self.add(&self.staging_pressure_count, 1); - } - - pub fn addVisible(self: *LODProfilingCollector) void { - self.add(&self.visible_count, 1); - } - - pub fn addCompactSelected(self: *LODProfilingCollector) void { - self.add(&self.compact_selected, 1); - } - - pub fn addCompactBuildRejected(self: *LODProfilingCollector) void { - self.add(&self.compact_build_rejected, 1); - } - - pub fn addCompactUploadFailure(self: *LODProfilingCollector) void { - self.add(&self.compact_upload_failures, 1); - } - - pub fn addCompactDrawUnavailable(self: *LODProfilingCollector) void { - self.add(&self.compact_draw_unavailable, 1); - } - - pub fn addCompactDrawFailure(self: *LODProfilingCollector) void { - self.add(&self.compact_draw_failures, 1); - } - - pub fn addCompactSubmission(self: *LODProfilingCollector) void { - self.add(&self.compact_submissions, 1); - } - - pub fn addCompactRecovery(self: *LODProfilingCollector) void { - self.add(&self.compact_recoveries, 1); - } - - pub fn addCompactDisabled(self: *LODProfilingCollector) void { - self.add(&self.compact_disabled, 1); - } - - pub fn setGpuCullingConfiguration(self: *LODProfilingCollector, requested: bool, threshold: usize) void { - if (!self.enabled) return; - self.gpu_culling_requested.store(@intFromBool(requested), .monotonic); - self.gpu_culling_threshold.store(@min(threshold, std.math.maxInt(u32)), .monotonic); - } - - /// Records the projection once per prepared frame, before terrain and water - /// consume the same candidate stream. - pub fn setGpuCullingCandidateCount(self: *LODProfilingCollector, count: usize) void { - if (!self.enabled) return; - const value: u64 = @min(count, std.math.maxInt(u32)); - self.gpu_culling_candidate_count.store(value, .monotonic); - var observed = self.gpu_culling_candidate_count_max.load(.monotonic); - while (observed < value) { - observed = self.gpu_culling_candidate_count_max.cmpxchgWeak(observed, value, .monotonic, .monotonic) orelse break; - } - } - - /// Counts one successful GPU frame submission, rather than one terrain and - /// one water stream, so paired layers cannot inflate benchmark evidence. - pub fn addGpuCullingSubmission(self: *LODProfilingCollector) void { - self.add(&self.gpu_culling_draw_submissions, 1); - } - - pub fn setGpuCullingDiagnostics(self: *LODProfilingCollector, overflows: u32, mismatches: u32, generation: u64, completed_generation: u64, completed_count: u64) void { - if (!self.enabled) return; - self.gpu_culling_overflows.store(overflows, .monotonic); - self.gpu_culling_validation_mismatches.store(mismatches, .monotonic); - self.gpu_culling_validation_generation.store(generation, .monotonic); - self.gpu_culling_validation_completed_generation.store(completed_generation, .monotonic); - self.gpu_culling_validation_completed_count.store(completed_count, .monotonic); - } - - pub fn addRejected(self: *LODProfilingCollector) void { - self.add(&self.rejected_count, 1); - } - - pub fn addCoverage(self: *LODProfilingCollector) void { - self.add(&self.coverage_count, 1); - } - - pub fn addVisibilityLevel(self: *LODProfilingCollector, lod: LODLevel, value: LODVisibilityLevelSnapshot) void { - if (!self.enabled) return; - self.visibility_levels[@intFromEnum(lod)].add(value); - } - - pub fn addDeferredDeletionBytes(self: *LODProfilingCollector, bytes: usize) void { - self.add(&self.deferred_deletion_bytes, @intCast(bytes)); - } - - pub fn removeDeferredDeletionBytes(self: *LODProfilingCollector, bytes: usize) void { - if (!self.enabled or bytes == 0) return; - _ = self.deferred_deletion_bytes.fetchSub(@intCast(bytes), .monotonic); - } - - pub fn addDeferredDeletionCpuBytes(self: *LODProfilingCollector, bytes: usize) void { - self.add(&self.deferred_deletion_cpu_bytes, @intCast(bytes)); - } - - pub fn removeDeferredDeletionCpuBytes(self: *LODProfilingCollector, bytes: usize) void { - if (!self.enabled or bytes == 0) return; - _ = self.deferred_deletion_cpu_bytes.fetchSub(@intCast(bytes), .monotonic); - } - - /// Compatibility helper for a runtime/streaming wait with no measured - /// duration. New call sites should use `recordWaitIdle`. - pub fn addWaitIdle(self: *LODProfilingCollector) void { - self.add(&self.wait_idle_count, 1); - } - - pub fn recordWaitIdle(self: *LODProfilingCollector, reason: LODWaitIdleReason, timer: ?MonotonicTimer) void { - if (!self.enabled) return; - const elapsed = timer orelse return; - switch (reason) { - .streaming => { - _ = self.wait_idle_count.fetchAdd(1, .monotonic); - _ = self.wait_idle_ns.fetchAdd(elapsed.read(), .monotonic); - }, - .shutdown => { - _ = self.wait_idle_shutdown_count.fetchAdd(1, .monotonic); - _ = self.wait_idle_shutdown_ns.fetchAdd(elapsed.read(), .monotonic); - }, - } - } - - /// Resets cumulative profiling counters. Concurrent worker completion may - /// land immediately before or after this reset, but snapshots are always - /// race-free and never contain torn values. - pub fn reset(self: *LODProfilingCollector) void { - inline for (.{ - &self.update_ns, &self.scheduling_ns, &self.cache_ns, - &self.generation_dispatch_ns, &self.state_transition_ns, &self.upload_prep_ns, - &self.upload_submission_ns, &self.visibility_ns, &self.coverage_ns, - &self.eviction_ns, &self.worker_generation_ns, &self.worker_mesh_construction_ns, - &self.worker_far_expanded_mesh_construction_ns, &self.worker_compact_encode_ns, &self.manager_lock_wait_ns, - &self.manager_lock_hold_ns, &self.wait_idle_ns, &self.upload_bytes, - &self.far_expanded_upload_bytes, &self.compact_upload_bytes, &self.pending_cpu_upload_bytes, - &self.staging_pressure_count, &self.visible_count, &self.rejected_count, - &self.coverage_count, &self.deferred_deletion_bytes, &self.deferred_deletion_cpu_bytes, - &self.pool_gpu_capacity_bytes, &self.pool_gpu_allocated_bytes, &self.pool_gpu_slack_bytes, - &self.pool_cpu_shadow_bytes, &self.compact_pool_capacity_bytes, &self.compact_pool_allocated_bytes, - &self.compact_pool_free_bytes, &self.compact_pool_retired_bytes, &self.direct_mesh_gpu_bytes, - &self.known_memory_bytes, &self.wait_idle_count, &self.wait_idle_shutdown_count, - &self.wait_idle_shutdown_ns, &self.compact_selected, &self.compact_build_rejected, - &self.compact_upload_failures, &self.compact_draw_unavailable, &self.compact_draw_failures, - &self.compact_submissions, &self.compact_recoveries, &self.compact_disabled, - &self.gpu_culling_requested, &self.gpu_culling_threshold, &self.gpu_culling_candidate_count, - &self.gpu_culling_candidate_count_max, &self.gpu_culling_draw_submissions, &self.gpu_culling_overflows, - &self.gpu_culling_validation_mismatches, &self.gpu_culling_validation_generation, &self.gpu_culling_validation_completed_generation, - &self.gpu_culling_validation_completed_count, - }) |counter| counter.store(0, .monotonic); - for (&self.visibility_levels) |*level| level.reset(); - } - - pub fn snapshot(self: *const LODProfilingCollector) LODProfilingSnapshot { - return .{ - .enabled = self.enabled, - .update_ms = nsToMs(self.update_ns.load(.monotonic)), - .scheduling_ms = nsToMs(self.scheduling_ns.load(.monotonic)), - .cache_ms = nsToMs(self.cache_ns.load(.monotonic)), - .generation_dispatch_ms = nsToMs(self.generation_dispatch_ns.load(.monotonic)), - .state_transition_ms = nsToMs(self.state_transition_ns.load(.monotonic)), - .upload_prep_ms = nsToMs(self.upload_prep_ns.load(.monotonic)), - .upload_submission_ms = nsToMs(self.upload_submission_ns.load(.monotonic)), - .visibility_ms = nsToMs(self.visibility_ns.load(.monotonic)), - .coverage_ms = nsToMs(self.coverage_ns.load(.monotonic)), - .eviction_ms = nsToMs(self.eviction_ns.load(.monotonic)), - .worker_generation_ms = nsToMs(self.worker_generation_ns.load(.monotonic)), - .worker_mesh_construction_ms = nsToMs(self.worker_mesh_construction_ns.load(.monotonic)), - .worker_far_expanded_mesh_construction_ms = nsToMs(self.worker_far_expanded_mesh_construction_ns.load(.monotonic)), - .worker_compact_encode_ms = nsToMs(self.worker_compact_encode_ns.load(.monotonic)), - .manager_lock_wait_ms = nsToMs(self.manager_lock_wait_ns.load(.monotonic)), - .manager_lock_hold_ms = nsToMs(self.manager_lock_hold_ns.load(.monotonic)), - .upload_bytes = self.upload_bytes.load(.monotonic), - .far_expanded_upload_bytes = self.far_expanded_upload_bytes.load(.monotonic), - .compact_upload_bytes = self.compact_upload_bytes.load(.monotonic), - .pending_cpu_upload_bytes = self.pending_cpu_upload_bytes.load(.monotonic), - .staging_pressure_count = self.staging_pressure_count.load(.monotonic), - .visible_count = self.visible_count.load(.monotonic), - .rejected_count = self.rejected_count.load(.monotonic), - .coverage_count = self.coverage_count.load(.monotonic), - .visibility_levels = blk: { - var levels: [LODLevel.count]LODVisibilityLevelSnapshot = undefined; - for (&levels, 0..) |*level, index| level.* = self.visibility_levels[index].snapshot(); - break :blk levels; - }, - .deferred_deletion_bytes = self.deferred_deletion_bytes.load(.monotonic), - .deferred_deletion_cpu_bytes = self.deferred_deletion_cpu_bytes.load(.monotonic), - .pool_gpu_capacity_bytes = self.pool_gpu_capacity_bytes.load(.monotonic), - .pool_gpu_allocated_bytes = self.pool_gpu_allocated_bytes.load(.monotonic), - .pool_gpu_slack_bytes = self.pool_gpu_slack_bytes.load(.monotonic), - .pool_cpu_shadow_bytes = self.pool_cpu_shadow_bytes.load(.monotonic), - .compact_pool_capacity_bytes = self.compact_pool_capacity_bytes.load(.monotonic), - .compact_pool_allocated_bytes = self.compact_pool_allocated_bytes.load(.monotonic), - .compact_pool_free_bytes = self.compact_pool_free_bytes.load(.monotonic), - .compact_pool_retired_bytes = self.compact_pool_retired_bytes.load(.monotonic), - .direct_mesh_gpu_bytes = self.direct_mesh_gpu_bytes.load(.monotonic), - .known_memory_bytes = self.known_memory_bytes.load(.monotonic), - .wait_idle_count = self.wait_idle_count.load(.monotonic), - .wait_idle_ms = nsToMs(self.wait_idle_ns.load(.monotonic)), - .wait_idle_shutdown_count = self.wait_idle_shutdown_count.load(.monotonic), - .wait_idle_shutdown_ms = nsToMs(self.wait_idle_shutdown_ns.load(.monotonic)), - .compact_selected = self.compact_selected.load(.monotonic), - .compact_build_rejected = self.compact_build_rejected.load(.monotonic), - .compact_upload_failures = self.compact_upload_failures.load(.monotonic), - .compact_draw_unavailable = self.compact_draw_unavailable.load(.monotonic), - .compact_draw_failures = self.compact_draw_failures.load(.monotonic), - .compact_submissions = self.compact_submissions.load(.monotonic), - .compact_recoveries = self.compact_recoveries.load(.monotonic), - .compact_disabled = self.compact_disabled.load(.monotonic), - .gpu_culling_requested = self.gpu_culling_requested.load(.monotonic) != 0, - .gpu_culling_threshold = @intCast(self.gpu_culling_threshold.load(.monotonic)), - .gpu_culling_candidate_count = @intCast(self.gpu_culling_candidate_count.load(.monotonic)), - .gpu_culling_candidate_count_max = @intCast(self.gpu_culling_candidate_count_max.load(.monotonic)), - .gpu_culling_draw_submissions = self.gpu_culling_draw_submissions.load(.monotonic), - .gpu_culling_overflows = @intCast(self.gpu_culling_overflows.load(.monotonic)), - .gpu_culling_validation_mismatches = @intCast(self.gpu_culling_validation_mismatches.load(.monotonic)), - .gpu_culling_validation_generation = self.gpu_culling_validation_generation.load(.monotonic), - .gpu_culling_validation_completed_generation = self.gpu_culling_validation_completed_generation.load(.monotonic), - .gpu_culling_validation_completed_count = self.gpu_culling_validation_completed_count.load(.monotonic), - }; - } - - fn counterFor(self: *LODProfilingCollector, kind: TimerKind) *AtomicU64 { - return switch (kind) { - .update => &self.update_ns, - .scheduling => &self.scheduling_ns, - .cache => &self.cache_ns, - .generation_dispatch => &self.generation_dispatch_ns, - .state_transition => &self.state_transition_ns, - .upload_prep => &self.upload_prep_ns, - .upload_submission => &self.upload_submission_ns, - .visibility => &self.visibility_ns, - .coverage => &self.coverage_ns, - .eviction => &self.eviction_ns, - .worker_generation => &self.worker_generation_ns, - .worker_mesh_construction => &self.worker_mesh_construction_ns, - .worker_far_expanded_mesh_construction => &self.worker_far_expanded_mesh_construction_ns, - .worker_compact_encode => &self.worker_compact_encode_ns, - .manager_lock_wait => &self.manager_lock_wait_ns, - .manager_lock_hold => &self.manager_lock_hold_ns, - .wait_idle => &self.wait_idle_ns, - }; - } -}; - -fn nsToMs(ns: u64) f64 { - return @as(f64, @floatFromInt(ns)) / @as(f64, std.time.ns_per_ms); -} - -/// Statistics for LOD system monitoring. -pub const LODStats = struct { - /// Cumulative opt-in CPU, upload, and pressure telemetry snapshot. - profiling: LODProfilingSnapshot = .{}, - loaded: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - generating: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - generated: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - meshing: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - mesh_ready: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - uploading: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - - memory_used_mb: u32 = 0, - /// Exact known LOD CPU/GPU allocation total. `memory_used_mb` is retained - /// for compatibility and is this value rounded down to MiB. - memory_used_bytes: u64 = 0, - pool_gpu_capacity_bytes: u64 = 0, - pool_gpu_allocated_bytes: u64 = 0, - pool_gpu_slack_bytes: u64 = 0, - pool_cpu_shadow_bytes: u64 = 0, - compact_pool_capacity_bytes: u64 = 0, - compact_pool_allocated_bytes: u64 = 0, - compact_pool_free_bytes: u64 = 0, - compact_pool_retired_bytes: u64 = 0, - direct_mesh_gpu_bytes: u64 = 0, - source_data_cpu_bytes: u64 = 0, - resident_region_count: u64 = 0, - logical_admission_reservation_bytes: u64 = 0, - logical_admission_bytes: u64 = 0, - pending_cpu_upload_bytes: u64 = 0, - deferred_deletion_gpu_bytes: u64 = 0, - deferred_deletion_cpu_bytes: u64 = 0, - mesh_count: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - mesh_vertices: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - gen_queue_depth: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - upload_queue_depth: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - /// On-disk LOD source store counters. cache_* below are retained as - /// compatibility aliases for existing diagnostics consumers. - store_hits: u32 = 0, - store_misses: u32 = 0, - cache_hits: u32 = 0, - cache_misses: u32 = 0, - evictions: u32 = 0, - drawn: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - instances: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - fluid_drawn: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - fluid_instances: [LODLevel.count]u32 = [_]u32{0} ** LODLevel.count, - /// Candidate counts submitted to device-local LOD compute streams. These - /// deliberately do not require a same-frame readback of compacted counts. - gpu_terrain_candidates: u32 = 0, - gpu_fluid_candidates: u32 = 0, - gpu_culling_requested: bool = false, - gpu_culling_threshold: u32 = 0, - gpu_culling_candidate_count: u32 = 0, - gpu_culling_draw_submissions: u32 = 0, - gpu_culling_overflows: u32 = 0, - gpu_culling_validation_mismatches: u32 = 0, - gpu_culling_validation_generation: u64 = 0, - gpu_culling_validation_completed_generation: u64 = 0, - gpu_culling_validation_completed_count: u64 = 0, - ingestion_backlog: u32 = 0, - upgrades_pending: u32 = 0, - downgrades_pending: u32 = 0, - upload_failures: u32 = 0, - /// Lifecycle tokens accepted into bounded overflow heaps. These are - /// observable pressure counters; overflow tokens are still immediately - /// prioritized for consumption rather than being dropped. - generation_token_overflows: u64 = 0, - transition_token_overflows: u64 = 0, - fade_token_overflows: u64 = 0, - /// Worker and queued jobs invalidated before stale results could publish. - cancelled_jobs: u32 = 0, - - pub fn totalLoaded(self: *const LODStats) u32 { - var total: u32 = 0; - for (self.loaded) |count| total += count; - return total; - } - - pub fn totalGenerating(self: *const LODStats) u32 { - var total: u32 = 0; - for (self.generating) |count| total += count; - return total; - } - - pub fn reset(self: *LODStats) void { - self.profiling = .{}; - self.loaded = [_]u32{0} ** LODLevel.count; - self.generating = [_]u32{0} ** LODLevel.count; - self.generated = [_]u32{0} ** LODLevel.count; - self.meshing = [_]u32{0} ** LODLevel.count; - self.mesh_ready = [_]u32{0} ** LODLevel.count; - self.uploading = [_]u32{0} ** LODLevel.count; - self.memory_used_mb = 0; - self.memory_used_bytes = 0; - self.pool_gpu_capacity_bytes = 0; - self.pool_gpu_allocated_bytes = 0; - self.pool_gpu_slack_bytes = 0; - self.pool_cpu_shadow_bytes = 0; - self.compact_pool_capacity_bytes = 0; - self.compact_pool_allocated_bytes = 0; - self.compact_pool_free_bytes = 0; - self.compact_pool_retired_bytes = 0; - self.direct_mesh_gpu_bytes = 0; - self.source_data_cpu_bytes = 0; - self.resident_region_count = 0; - self.logical_admission_reservation_bytes = 0; - self.logical_admission_bytes = 0; - self.pending_cpu_upload_bytes = 0; - self.deferred_deletion_gpu_bytes = 0; - self.deferred_deletion_cpu_bytes = 0; - self.mesh_count = [_]u32{0} ** LODLevel.count; - self.mesh_vertices = [_]u32{0} ** LODLevel.count; - self.gen_queue_depth = [_]u32{0} ** LODLevel.count; - self.upload_queue_depth = [_]u32{0} ** LODLevel.count; - self.drawn = [_]u32{0} ** LODLevel.count; - self.instances = [_]u32{0} ** LODLevel.count; - self.fluid_drawn = [_]u32{0} ** LODLevel.count; - self.fluid_instances = [_]u32{0} ** LODLevel.count; - self.gpu_terrain_candidates = 0; - self.gpu_fluid_candidates = 0; - self.gpu_culling_requested = false; - self.gpu_culling_threshold = 0; - self.gpu_culling_candidate_count = 0; - self.gpu_culling_draw_submissions = 0; - self.gpu_culling_overflows = 0; - self.gpu_culling_validation_mismatches = 0; - self.gpu_culling_validation_generation = 0; - self.gpu_culling_validation_completed_generation = 0; - self.gpu_culling_validation_completed_count = 0; - self.upgrades_pending = 0; - self.downgrades_pending = 0; - self.upload_failures = 0; - self.generation_token_overflows = 0; - self.transition_token_overflows = 0; - self.fade_token_overflows = 0; - self.ingestion_backlog = 0; - self.cancelled_jobs = 0; - } - - pub fn recordState(self: *LODStats, lod_idx: usize, state: LODState) void { - switch (state) { - .renderable => self.loaded[lod_idx] += 1, - .generating => self.generating[lod_idx] += 1, - .generated => self.generated[lod_idx] += 1, - .meshing => self.meshing[lod_idx] += 1, - .mesh_ready => self.mesh_ready[lod_idx] += 1, - .uploading => self.uploading[lod_idx] += 1, - else => {}, - } - } - - pub fn addMemory(self: *LODStats, bytes: usize) void { - self.memory_used_bytes += @intCast(bytes); - self.memory_used_mb = @intCast(self.memory_used_bytes / (1024 * 1024)); - } - - pub fn cacheHitRate(self: *const LODStats) f32 { - const total = self.store_hits + self.store_misses; - if (total == 0) return 0.0; - return @as(f32, @floatFromInt(self.store_hits)) / @as(f32, @floatFromInt(total)); - } -}; - -test "LODStats reports cache hit rate" { - var stats = LODStats{}; - try std.testing.expectEqual(@as(f32, 0.0), stats.cacheHitRate()); - - stats.store_hits = 3; - stats.store_misses = 1; - try std.testing.expectEqual(@as(f32, 0.75), stats.cacheHitRate()); -} - -test "LOD profiling collector snapshots and resets cumulative counters" { - var collector = LODProfilingCollector.init(true); - collector.addUploadBytes(128); - collector.addFarExpandedUploadBytes(96); - collector.addCompactUploadBytes(32); - collector.setPendingCpuUploadBytes(64); - collector.addVisible(); - collector.addRejected(); - collector.addCoverage(); - collector.addCompactSubmission(); - collector.addVisibilityLevel(.lod1, .{ .candidates = 3, .accepted = 1, .rejected_frustum = 1, .coverage_checks = 1 }); - collector.addDeferredDeletionBytes(32); - collector.addDeferredDeletionCpuBytes(16); - collector.setMemoryAccounting(256, 192, 64, 256, 1024, 320, 704, 0, 128, 1664); - collector.addWaitIdle(); - collector.setGpuCullingConfiguration(true, 128); - collector.setGpuCullingCandidateCount(1024); - collector.setGpuCullingCandidateCount(512); - collector.addGpuCullingSubmission(); - collector.setGpuCullingDiagnostics(2, 3, 9, 8, 7); - - const snapshot = collector.snapshot(); - try std.testing.expect(snapshot.enabled); - try std.testing.expectEqual(@as(u64, 128), snapshot.upload_bytes); - try std.testing.expectEqual(@as(u64, 96), snapshot.far_expanded_upload_bytes); - try std.testing.expectEqual(@as(u64, 32), snapshot.compact_upload_bytes); - try std.testing.expectEqual(@as(u64, 64), snapshot.pending_cpu_upload_bytes); - try std.testing.expectEqual(@as(u64, 1), snapshot.visible_count); - try std.testing.expectEqual(@as(u64, 1), snapshot.rejected_count); - try std.testing.expectEqual(@as(u64, 1), snapshot.coverage_count); - try std.testing.expectEqual(@as(u64, 3), snapshot.visibility_levels[1].candidates); - try std.testing.expectEqual(@as(u64, 1), snapshot.visibility_levels[1].rejected_frustum); - try std.testing.expectEqual(@as(u64, 32), snapshot.deferred_deletion_bytes); - try std.testing.expectEqual(@as(u64, 16), snapshot.deferred_deletion_cpu_bytes); - try std.testing.expectEqual(@as(u64, 256), snapshot.pool_gpu_capacity_bytes); - try std.testing.expectEqual(@as(u64, 192), snapshot.pool_gpu_allocated_bytes); - try std.testing.expectEqual(@as(u64, 64), snapshot.pool_gpu_slack_bytes); - try std.testing.expectEqual(@as(u64, 256), snapshot.pool_cpu_shadow_bytes); - try std.testing.expectEqual(@as(u64, 1024), snapshot.compact_pool_capacity_bytes); - try std.testing.expectEqual(@as(u64, 320), snapshot.compact_pool_allocated_bytes); - try std.testing.expectEqual(@as(u64, 704), snapshot.compact_pool_free_bytes); - try std.testing.expectEqual(@as(u64, 128), snapshot.direct_mesh_gpu_bytes); - try std.testing.expectEqual(@as(u64, 1664), snapshot.known_memory_bytes); - try std.testing.expectEqual(@as(u64, 1), snapshot.wait_idle_count); - try std.testing.expectEqual(@as(u64, 1), snapshot.compact_submissions); - try std.testing.expect(snapshot.gpu_culling_requested); - try std.testing.expectEqual(@as(u32, 128), snapshot.gpu_culling_threshold); - try std.testing.expectEqual(@as(u32, 512), snapshot.gpu_culling_candidate_count); - try std.testing.expectEqual(@as(u32, 1024), snapshot.gpu_culling_candidate_count_max); - try std.testing.expectEqual(@as(u64, 1), snapshot.gpu_culling_draw_submissions); - try std.testing.expectEqual(@as(u32, 2), snapshot.gpu_culling_overflows); - try std.testing.expectEqual(@as(u32, 3), snapshot.gpu_culling_validation_mismatches); - - collector.reset(); - const reset_snapshot = collector.snapshot(); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.upload_bytes); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.far_expanded_upload_bytes); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.visible_count); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.deferred_deletion_bytes); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.pool_gpu_capacity_bytes); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.visibility_levels[1].candidates); - try std.testing.expectEqual(@as(u64, 0), reset_snapshot.compact_submissions); - try std.testing.expect(!reset_snapshot.gpu_culling_requested); - try std.testing.expectEqual(@as(u32, 0), reset_snapshot.gpu_culling_candidate_count_max); -} - -test "per-frame LOD stats reset cannot erase collector GPU-culling evidence" { - var collector = LODProfilingCollector.init(true); - collector.setGpuCullingConfiguration(true, 128); - collector.setGpuCullingCandidateCount(1024); - collector.addGpuCullingSubmission(); - - var frame_stats = LODStats{}; - frame_stats.gpu_culling_requested = true; - frame_stats.gpu_culling_candidate_count = 1024; - frame_stats.reset(); - - const snapshot = collector.snapshot(); - try std.testing.expect(snapshot.gpu_culling_requested); - try std.testing.expectEqual(@as(u32, 1024), snapshot.gpu_culling_candidate_count_max); - try std.testing.expectEqual(@as(u64, 1), snapshot.gpu_culling_draw_submissions); -} - -test "disabled LOD profiling collector does not accumulate samples" { - var collector = LODProfilingCollector.init(false); - const timer = collector.begin(); - collector.end(.update, timer); - collector.addUploadBytes(256); - collector.addVisible(); - - const snapshot = collector.snapshot(); - try std.testing.expect(!snapshot.enabled); - try std.testing.expectEqual(@as(f64, 0), snapshot.update_ms); - try std.testing.expectEqual(@as(u64, 0), snapshot.upload_bytes); - try std.testing.expectEqual(@as(u64, 0), snapshot.visible_count); -} diff --git a/modules/world-lod/src/lod_store.zig b/modules/world-lod/src/lod_store.zig deleted file mode 100644 index 34f8c365..00000000 --- a/modules/world-lod/src/lod_store.zig +++ /dev/null @@ -1,611 +0,0 @@ -//! Persistent container store for serialized LOD source payloads. - -const std = @import("std"); -const fs = @import("fs"); - -const RegionFile = @import("world-persistence").RegionFile; -const lod_cache = @import("lod_cache.zig"); - -const REGION_GRID: i32 = 32; -pub const DEFAULT_STORE_SIZE_CAP_MB: u32 = 256; - -pub const StoreHeader = struct { - seed: u64, - generator_identity_hash: u64, - generator_version: u32, - lod_data_version: u8 = lod_cache.CACHE_VERSION, -}; - -pub const StoreError = error{ - CorruptContainer, - StoreSizeLimit, -}; - -pub fn headersMatch(a: StoreHeader, b: StoreHeader) bool { - return a.seed == b.seed and - a.generator_identity_hash == b.generator_identity_hash and - a.generator_version == b.generator_version and - a.lod_data_version == b.lod_data_version; -} - -fn containerCoord(value: i32) i32 { - return @divFloor(value, REGION_GRID); -} - -fn localCoord(value: i32) u5 { - return @intCast(@mod(value, REGION_GRID)); -} - -fn lodDirPath(allocator: std.mem.Allocator, save_dir_path: []const u8, lod: @import("lod_types.zig").LODLevel) ![]u8 { - const lod_name = try std.fmt.allocPrint(allocator, "{}", .{@intFromEnum(lod)}); - defer allocator.free(lod_name); - return fs.path.join(allocator, &.{ save_dir_path, "lod", lod_name }); -} - -pub fn containerPath(allocator: std.mem.Allocator, save_dir_path: []const u8, key: lod_cache.Key) ![]u8 { - const dir_path = try lodDirPath(allocator, save_dir_path, key.lod); - defer allocator.free(dir_path); - const filename = try std.fmt.allocPrint( - allocator, - "r.{}.{}.zlod", - .{ containerCoord(key.rx), containerCoord(key.rz) }, - ); - defer allocator.free(filename); - return fs.path.join(allocator, &.{ dir_path, filename }); -} - -pub fn writeHeader(allocator: std.mem.Allocator, save_dir_path: []const u8, header: StoreHeader) !void { - const lod_root = try fs.path.join(allocator, &.{ save_dir_path, "lod" }); - defer allocator.free(lod_root); - try fs.cwd().makePath(lod_root); - - const path = try fs.path.join(allocator, &.{ lod_root, "store.json" }); - defer allocator.free(path); - const tmp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{path}); - defer allocator.free(tmp_path); - - const bytes = try std.fmt.allocPrint( - allocator, - "{{\n \"seed\": {},\n \"generator_identity_hash\": {},\n \"generator_version\": {},\n \"lod_data_version\": {}\n}}\n", - .{ header.seed, header.generator_identity_hash, header.generator_version, header.lod_data_version }, - ); - defer allocator.free(bytes); - - const file = try fs.cwd().createFile(tmp_path, .{ .truncate = true }); - file.writeAll(bytes) catch |err| { - file.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - file.close(); - fs.cwd().rename(tmp_path, path) catch |err| { - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; -} - -pub fn readHeader(allocator: std.mem.Allocator, save_dir_path: []const u8) !?StoreHeader { - const path = try fs.path.join(allocator, &.{ save_dir_path, "lod", "store.json" }); - defer allocator.free(path); - - const bytes = fs.cwd().readFileAlloc(path, allocator, 4096) catch |err| switch (err) { - error.FileNotFound => return null, - else => return err, - }; - defer allocator.free(bytes); - - const parsed = try std.json.parseFromSlice(StoreHeader, allocator, bytes, .{ .ignore_unknown_fields = true }); - defer parsed.deinit(); - return parsed.value; -} - -pub fn deleteStore(allocator: std.mem.Allocator, save_dir_path: []const u8) !void { - const lod_root = try fs.path.join(allocator, &.{ save_dir_path, "lod" }); - defer allocator.free(lod_root); - - fs.cwd().access(lod_root, .{}) catch |err| switch (err) { - error.FileNotFound => return, - else => return err, - }; - try fs.cwd().deleteTree(lod_root); -} - -pub fn readPayload(allocator: std.mem.Allocator, save_dir_path: []const u8, key: lod_cache.Key) !?[]u8 { - const path = try containerPath(allocator, save_dir_path, key); - defer allocator.free(path); - - var region = RegionFile.open(allocator, path) catch |err| switch (err) { - error.FileNotFound => return null, - else => return StoreError.CorruptContainer, - }; - defer region.close(); - - return region.readChunk(localCoord(key.rx), localCoord(key.rz), allocator) catch |err| switch (err) { - error.ChunkNotFound => return null, - else => return err, - }; -} - -fn storeSizeCapBytes(cap_mb: u32) usize { - return @as(usize, @max(cap_mb, 1)) * 1024 * 1024; -} - -const ContainerCandidate = struct { - path: []u8, - mtime_ns: i96, -}; - -const StoreUsage = struct { - total_size: u64 = 0, - oldest_candidate: ?ContainerCandidate = null, -}; - -fn isActiveLodDirectory(name: []const u8) bool { - const lod_index = std.fmt.parseInt(u8, name, 10) catch return false; - return lod_index < @import("lod_types.zig").LODLevel.count; -} - -fn isOlderContainer(candidate: ContainerCandidate, path: []const u8, mtime_ns: i96) bool { - return mtime_ns < candidate.mtime_ns or - (mtime_ns == candidate.mtime_ns and std.mem.order(u8, path, candidate.path) == .lt); -} - -/// Returns the total size of all LOD containers and the oldest removable one. -/// -/// This only retains one candidate path, so eviction memory use remains bounded -/// regardless of how many containers are on disk. Files that cannot be opened -/// or statted are skipped; malformed region contents need not be parsed to -/// account for their on-disk size. -fn scanStoreUsage(allocator: std.mem.Allocator, lod_root: []const u8, protected_path: []const u8) !StoreUsage { - var usage = StoreUsage{}; - errdefer if (usage.oldest_candidate) |candidate| allocator.free(candidate.path); - - var root_dir = fs.cwd().openDir(lod_root, .{ .iterate = true }) catch |err| switch (err) { - error.FileNotFound => return usage, - else => return err, - }; - defer root_dir.close(); - - var lod_iter = root_dir.iterate(); - while (true) { - const maybe_lod_entry = try lod_iter.next(); - const lod_entry = maybe_lod_entry orelse break; - if (lod_entry.kind != .directory or !isActiveLodDirectory(lod_entry.name)) continue; - - var lod_dir = root_dir.openDir(lod_entry.name, .{ .iterate = true }) catch continue; - defer lod_dir.close(); - - var container_iter = lod_dir.iterate(); - while (true) { - const maybe_container_entry = try container_iter.next(); - const container_entry = maybe_container_entry orelse break; - if (container_entry.kind != .file or !std.mem.endsWith(u8, container_entry.name, ".zlod")) continue; - - const file = lod_dir.openFile(container_entry.name, .{}) catch continue; - const stat = file.stat() catch { - file.close(); - continue; - }; - file.close(); - - usage.total_size +|= stat.size; - const path = try fs.path.join(allocator, &.{ lod_root, lod_entry.name, container_entry.name }); - if (std.mem.eql(u8, path, protected_path)) { - allocator.free(path); - continue; - } - - if (usage.oldest_candidate) |candidate| { - if (!isOlderContainer(candidate, path, stat.mtime.nanoseconds)) { - allocator.free(path); - continue; - } - allocator.free(candidate.path); - } - usage.oldest_candidate = .{ - .path = path, - .mtime_ns = stat.mtime.nanoseconds, - }; - } - } - - return usage; -} - -/// Enforces the aggregate container limit after a successful atomic write. -/// The just-written container is never selected, even when it alone exceeds -/// the configured limit. -fn enforceStoreSizeCap(allocator: std.mem.Allocator, save_dir_path: []const u8, protected_path: []const u8, cap_mb: u32) !void { - const lod_root = try fs.path.join(allocator, &.{ save_dir_path, "lod" }); - defer allocator.free(lod_root); - const cap_bytes = storeSizeCapBytes(cap_mb); - - while (true) { - const usage = try scanStoreUsage(allocator, lod_root, protected_path); - defer if (usage.oldest_candidate) |candidate| allocator.free(candidate.path); - if (usage.total_size <= cap_bytes) return; - - const candidate = usage.oldest_candidate orelse return; - fs.cwd().deleteFile(candidate.path) catch |err| switch (err) { - error.FileNotFound => continue, - else => return err, - }; - } -} - -fn writeCompactedContainer(allocator: std.mem.Allocator, path: []const u8, tmp_path: []const u8, key: lod_cache.Key, bytes: []const u8) !void { - var region = RegionFile.create(allocator, tmp_path) catch |err| { - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - errdefer { - region.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - } - - var existing_region = try RegionFile.open(allocator, path); - defer existing_region.close(); - const target_x = localCoord(key.rx); - const target_z = localCoord(key.rz); - for (0..REGION_GRID) |z| { - for (0..REGION_GRID) |x| { - const local_x: u5 = @intCast(x); - const local_z: u5 = @intCast(z); - if (local_x == target_x and local_z == target_z) continue; - const existing = existing_region.readChunk(local_x, local_z, allocator) catch |err| switch (err) { - error.ChunkNotFound => continue, - else => return err, - }; - region.writeChunk(local_x, local_z, existing) catch |err| { - allocator.free(existing); - return err; - }; - allocator.free(existing); - } - } - - region.writeChunk(target_x, target_z, bytes) catch |err| { - if (err == error.FileTooShort) return StoreError.StoreSizeLimit; - return err; - }; - region.close(); -} - -pub fn writePayload(allocator: std.mem.Allocator, save_dir_path: []const u8, key: lod_cache.Key, bytes: []const u8, store_size_cap_mb: u32) !void { - const dir_path = try lodDirPath(allocator, save_dir_path, key.lod); - defer allocator.free(dir_path); - try fs.cwd().makePath(dir_path); - - const path = try containerPath(allocator, save_dir_path, key); - defer allocator.free(path); - const tmp_path = try std.fmt.allocPrint(allocator, "{s}.tmp", .{path}); - defer allocator.free(tmp_path); - fs.cwd().deleteFile(tmp_path) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; - - const use_existing = blk: { - var existing_region = RegionFile.open(allocator, path) catch |err| switch (err) { - error.FileNotFound => break :blk false, - else => break :blk false, - }; - existing_region.close(); - break :blk true; - }; - - var force_compaction = false; - // Recover boundedly from a store created by an older implementation that - // allowed one container to grow beyond the aggregate cap. - if (use_existing) { - const existing_file = try fs.cwd().openFile(path, .{}); - const existing_stat = existing_file.stat() catch |err| { - existing_file.close(); - return err; - }; - existing_file.close(); - if (existing_stat.size > storeSizeCapBytes(store_size_cap_mb)) { - force_compaction = true; - } - } - - if (force_compaction) { - try writeCompactedContainer(allocator, path, tmp_path, key, bytes); - } else { - if (use_existing) { - const existing = try fs.cwd().readFileAlloc(path, allocator, storeSizeCapBytes(store_size_cap_mb)); - defer allocator.free(existing); - const tmp_file = try fs.cwd().createFile(tmp_path, .{ .truncate = true }); - tmp_file.writeAll(existing) catch |err| { - tmp_file.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - tmp_file.close(); - } - - var region = (if (use_existing) - RegionFile.open(allocator, tmp_path) - else - RegionFile.create(allocator, tmp_path)) catch |err| { - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - region.writeChunk(localCoord(key.rx), localCoord(key.rz), bytes) catch |err| { - region.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - if (err == error.FileTooShort) return StoreError.StoreSizeLimit; - return err; - }; - region.close(); - } - - var completed_tmp = try fs.cwd().openFile(tmp_path, .{}); - var completed_stat = completed_tmp.stat() catch |err| { - completed_tmp.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - completed_tmp.close(); - if (completed_stat.size > storeSizeCapBytes(store_size_cap_mb) and use_existing and !force_compaction) { - // Sector growth crossed the cap. Rebuild only live entries on the - // cache worker, then re-check before atomically replacing the source. - try fs.cwd().deleteFile(tmp_path); - try writeCompactedContainer(allocator, path, tmp_path, key, bytes); - completed_tmp = try fs.cwd().openFile(tmp_path, .{}); - completed_stat = completed_tmp.stat() catch |err| { - completed_tmp.close(); - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - completed_tmp.close(); - } - if (completed_stat.size > storeSizeCapBytes(store_size_cap_mb)) { - fs.cwd().deleteFile(tmp_path) catch {}; - // Keep an in-budget previous atomic container. Legacy oversized - // containers are discarded because they cannot satisfy the new bound. - if (force_compaction) fs.cwd().deleteFile(path) catch {}; - try enforceStoreSizeCap(allocator, save_dir_path, path, store_size_cap_mb); - return StoreError.StoreSizeLimit; - } - - fs.cwd().rename(tmp_path, path) catch |err| { - fs.cwd().deleteFile(tmp_path) catch {}; - return err; - }; - - try enforceStoreSizeCap(allocator, save_dir_path, path, store_size_cap_mb); -} - -pub fn deletePayload(allocator: std.mem.Allocator, save_dir_path: []const u8, key: lod_cache.Key) void { - const path = containerPath(allocator, save_dir_path, key) catch return; - defer allocator.free(path); - - var region = RegionFile.open(allocator, path) catch return; - defer region.close(); - region.deleteChunk(localCoord(key.rx), localCoord(key.rz)) catch {}; -} - -const testing = std.testing; - -fn fillPseudoRandom(bytes: []u8, initial_state: u64) void { - var state = initial_state; - for (bytes) |*byte| { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - byte.* = @truncate(state); - } -} - -test "LOD store round-trips payloads through region containers" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - const key = lod_cache.Key{ .seed = 11, .generator_identity_hash = 22, .generator_version = 3, .rx = 34, .rz = -1, .lod = .lod2 }; - try writePayload(testing.allocator, save_dir, key, "payload-a", DEFAULT_STORE_SIZE_CAP_MB); - - const loaded = (try readPayload(testing.allocator, save_dir, key)).?; - defer testing.allocator.free(loaded); - try testing.expectEqualStrings("payload-a", loaded); -} - -test "LOD store overwrites existing payloads" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - const key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = -32, .rz = 63, .lod = .lod1 }; - try writePayload(testing.allocator, save_dir, key, "small", DEFAULT_STORE_SIZE_CAP_MB); - try writePayload(testing.allocator, save_dir, key, "larger replacement payload", DEFAULT_STORE_SIZE_CAP_MB); - - const loaded = (try readPayload(testing.allocator, save_dir, key)).?; - defer testing.allocator.free(loaded); - try testing.expectEqualStrings("larger replacement payload", loaded); -} - -test "LOD store atomic overwrite preserves sibling entries" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - const key_a = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - const key_b = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 1, .rz = 0, .lod = .lod1 }; - try writePayload(testing.allocator, save_dir, key_a, "payload-a", DEFAULT_STORE_SIZE_CAP_MB); - try writePayload(testing.allocator, save_dir, key_b, "payload-b", DEFAULT_STORE_SIZE_CAP_MB); - try writePayload(testing.allocator, save_dir, key_a, "payload-a-updated", DEFAULT_STORE_SIZE_CAP_MB); - - const loaded_a = (try readPayload(testing.allocator, save_dir, key_a)).?; - defer testing.allocator.free(loaded_a); - const loaded_b = (try readPayload(testing.allocator, save_dir, key_b)).?; - defer testing.allocator.free(loaded_b); - try testing.expectEqualStrings("payload-a-updated", loaded_a); - try testing.expectEqualStrings("payload-b", loaded_b); -} - -test "LOD store write replaces corrupt containers" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - - const path = try containerPath(testing.allocator, save_dir, key); - defer testing.allocator.free(path); - const parent = fs.path.dirname(path).?; - try fs.cwd().makePath(parent); - const file = try fs.cwd().createFile(path, .{ .truncate = true }); - try file.writeAll("bad"); - file.close(); - - try writePayload(testing.allocator, save_dir, key, "replacement", DEFAULT_STORE_SIZE_CAP_MB); - const loaded = (try readPayload(testing.allocator, save_dir, key)).?; - defer testing.allocator.free(loaded); - try testing.expectEqualStrings("replacement", loaded); -} - -test "LOD store evicts old containers to enforce the total size cap" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const header = StoreHeader{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3 }; - try writeHeader(testing.allocator, save_dir, header); - - const payload_size = 700 * 1024; - const old_payload = try testing.allocator.alloc(u8, payload_size); - defer testing.allocator.free(old_payload); - const new_payload = try testing.allocator.alloc(u8, payload_size); - defer testing.allocator.free(new_payload); - fillPseudoRandom(old_payload, 0x123456789abcdef0); - fillPseudoRandom(new_payload, 0xfedcba9876543210); - - const old_key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - const new_key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 32, .rz = 0, .lod = .lod1 }; - try writePayload(testing.allocator, save_dir, old_key, old_payload, 1); - try writePayload(testing.allocator, save_dir, new_key, new_payload, 1); - - try testing.expect((try readPayload(testing.allocator, save_dir, old_key)) == null); - const retained = (try readPayload(testing.allocator, save_dir, new_key)).?; - defer testing.allocator.free(retained); - try testing.expectEqualSlices(u8, new_payload, retained); - try testing.expect(headersMatch(header, (try readHeader(testing.allocator, save_dir)).?)); -} - -test "LOD store compacts live entries when sector growth reaches the cap" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const old_payload = try testing.allocator.alloc(u8, 200 * 1024); - defer testing.allocator.free(old_payload); - const sibling_payload = try testing.allocator.alloc(u8, 200 * 1024); - defer testing.allocator.free(sibling_payload); - const replacement = try testing.allocator.alloc(u8, 600 * 1024); - defer testing.allocator.free(replacement); - fillPseudoRandom(old_payload, 0x1111111111111111); - fillPseudoRandom(sibling_payload, 0x2222222222222222); - fillPseudoRandom(replacement, 0x3333333333333333); - - const target_key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - const sibling_key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 1, .rz = 0, .lod = .lod1 }; - try writePayload(testing.allocator, save_dir, target_key, old_payload, 1); - try writePayload(testing.allocator, save_dir, sibling_key, sibling_payload, 1); - try writePayload(testing.allocator, save_dir, target_key, replacement, 1); - - const loaded_target = (try readPayload(testing.allocator, save_dir, target_key)).?; - defer testing.allocator.free(loaded_target); - try testing.expectEqualSlices(u8, replacement, loaded_target); - const loaded_sibling = (try readPayload(testing.allocator, save_dir, sibling_key)).?; - defer testing.allocator.free(loaded_sibling); - try testing.expectEqualSlices(u8, sibling_payload, loaded_sibling); -} - -test "LOD store rejects a container larger than the aggregate cap" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const payload = try testing.allocator.alloc(u8, 2 * 1024 * 1024); - defer testing.allocator.free(payload); - fillPseudoRandom(payload, 0x9e3779b97f4a7c15); - - const key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - try testing.expectError(StoreError.StoreSizeLimit, writePayload(testing.allocator, save_dir, key, payload, 1)); - try testing.expect((try readPayload(testing.allocator, save_dir, key)) == null); -} - -test "LOD store missing and corrupt containers are advisory misses" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const key = lod_cache.Key{ .seed = 1, .generator_identity_hash = 2, .generator_version = 3, .rx = 0, .rz = 0, .lod = .lod1 }; - - try testing.expect((try readPayload(testing.allocator, save_dir, key)) == null); - - const path = try containerPath(testing.allocator, save_dir, key); - defer testing.allocator.free(path); - const parent = fs.path.dirname(path).?; - try fs.cwd().makePath(parent); - const file = try fs.cwd().createFile(path, .{ .truncate = true }); - try file.writeAll("bad"); - file.close(); - - try testing.expectError(StoreError.CorruptContainer, readPayload(testing.allocator, save_dir, key)); -} - -test "LOD store writes metadata header atomically" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - - try writeHeader(testing.allocator, save_dir, .{ .seed = 123, .generator_identity_hash = 456, .generator_version = 7 }); - const header_path = try fs.path.join(testing.allocator, &.{ save_dir, "lod", "store.json" }); - defer testing.allocator.free(header_path); - const bytes = try fs.cwd().readFileAlloc(header_path, testing.allocator, 4096); - defer testing.allocator.free(bytes); - - try testing.expect(std.mem.indexOf(u8, bytes, "\"seed\": 123") != null); - try testing.expect(std.mem.indexOf(u8, bytes, "\"lod_data_version\": 11") != null); -} - -test "LOD store reads and deletes metadata store" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - - const dir = fs.Dir{ .inner = tmp_dir.dir }; - var path_buf: [fs.max_path_bytes]u8 = undefined; - const save_dir = try dir.realpath(".", &path_buf); - const header = StoreHeader{ .seed = 123, .generator_identity_hash = 456, .generator_version = 7 }; - - try writeHeader(testing.allocator, save_dir, header); - const loaded = (try readHeader(testing.allocator, save_dir)).?; - try testing.expect(headersMatch(header, loaded)); - - try deleteStore(testing.allocator, save_dir); - try testing.expect((try readHeader(testing.allocator, save_dir)) == null); -} diff --git a/modules/world-lod/src/lod_streaming_coordinator.zig b/modules/world-lod/src/lod_streaming_coordinator.zig deleted file mode 100644 index 1a99d4f8..00000000 --- a/modules/world-lod/src/lod_streaming_coordinator.zig +++ /dev/null @@ -1,276 +0,0 @@ -//! LOD and predictive-loading coordination for the world streamer. - -const std = @import("std"); -const Vec3 = @import("engine-math").Vec3; -const ChunkStorage = @import("world-meshing").ChunkStorage; -const JobQueue = @import("engine-core").JobQueue; -const LODManager = @import("lod_manager.zig").LODManager; -const worldToChunkFromFloat = @import("world-core").worldToChunkFromFloat; -const log = @import("engine-core").log; - -/// Player movement tracking for predictive chunk loading. -pub const PlayerMovement = struct { - /// Normalized movement direction (0,0 if stationary) - dir_x: f32 = 0, - dir_z: f32 = 0, - /// Speed in blocks/second - speed: f32 = 0, - /// Last position for velocity calculation - last_pos: Vec3 = Vec3.init(0, 0, 0), - /// Whether we have valid velocity data - has_velocity: bool = false, - - /// Update with new position, returns true if direction changed significantly. - pub fn update(self: *PlayerMovement, pos: Vec3, dt: f32) bool { - if (dt <= 0.001) return false; - - const dx = pos.x - self.last_pos.x; - const dz = pos.z - self.last_pos.z; - self.last_pos = pos; - - const dist = @sqrt(dx * dx + dz * dz); - self.speed = dist / dt; - - if (self.speed < 2.0) { - self.has_velocity = false; - return false; - } - - const old_dx = self.dir_x; - const old_dz = self.dir_z; - - self.dir_x = dx / dist; - self.dir_z = dz / dist; - self.has_velocity = true; - - const dot = old_dx * self.dir_x + old_dz * self.dir_z; - return dot < 0.707; - } - - /// Calculate priority weight for a chunk based on movement direction. - pub fn priorityWeight(self: *const PlayerMovement, chunk_dx: i32, chunk_dz: i32) f32 { - if (!self.has_velocity) return 1.0; - - const cdx: f32 = @floatFromInt(chunk_dx); - const cdz: f32 = @floatFromInt(chunk_dz); - const dist = @sqrt(cdx * cdx + cdz * cdz); - if (dist < 0.001) return 0.5; - - const dot = (cdx * self.dir_x + cdz * self.dir_z) / dist; - return 1.0 - dot * 0.5; - } -}; - -pub const StreamingFrame = struct { - pc_x: i32, - pc_z: i32, - moved: bool, - target_render_dist: i32, - render_dist: i32, - stream_dist: i32, - movement: PlayerMovement, -}; - -pub const QueueStats = struct { - gen_queue: usize, - mesh_queue: usize, - upload_queue: usize, -}; - -pub const LODStreamingCoordinator = struct { - lod_manager: ?*LODManager = null, - player_movement: PlayerMovement = .{}, - last_pc: struct { x: i32, z: i32 } = .{ .x = 9999, .z = 9999 }, - render_distance: i32, - effective_render_dist: i32 = 0, - startup_stream_radius: i32 = 0, - startup_mesh_finalized: bool = false, - horizon_bootstrap_ready: bool = false, - - const STARTUP_RADIUS_INITIAL = 3; - const STARTUP_RADIUS_STEP = 2; - const STARTUP_PREFETCH_RINGS = 2; - const STARTUP_RADIUS_CHECK_PERIOD = 10; - const STARTUP_READINESS_GRID_RADIUS: i64 = 4; - - pub fn init(render_distance: i32) LODStreamingCoordinator { - return .{ - .render_distance = render_distance, - .startup_stream_radius = @min(render_distance, STARTUP_RADIUS_INITIAL), - }; - } - - pub fn setRenderDistance(self: *LODStreamingCoordinator, distance: i32) bool { - if (self.render_distance == distance) return false; - - const previous_active = self.getActiveRenderDistance(); - self.render_distance = distance; - // Runtime increases grow outward from the currently visible radius - // instead of collapsing back to the three-chunk startup disk. - // Decreases clamp immediately so out-of-range chunks stop rendering. - self.startup_stream_radius = @min(distance, @max(previous_active, STARTUP_RADIUS_INITIAL)); - self.effective_render_dist = self.startup_stream_radius; - self.startup_mesh_finalized = false; - self.horizon_bootstrap_ready = false; - self.forceRescan(); - return true; - } - - pub fn setLODManager(self: *LODStreamingCoordinator, lod_manager: ?*LODManager) void { - self.lod_manager = lod_manager; - if (lod_manager != null) { - self.startup_stream_radius = @min(self.render_distance, STARTUP_RADIUS_INITIAL); - self.effective_render_dist = 0; - self.horizon_bootstrap_ready = false; - } - } - - pub fn forceRescan(self: *LODStreamingCoordinator) void { - self.last_pc = .{ .x = 9999, .z = 9999 }; - } - - pub fn getActiveRenderDistance(self: *const LODStreamingCoordinator) i32 { - return if (self.effective_render_dist > 0) self.effective_render_dist else self.render_distance; - } - - pub fn targetRenderDistance(self: *const LODStreamingCoordinator) i32 { - return if (self.lod_manager) |mgr| @min(self.render_distance, mgr.config.getChunkRenderRadius()) else self.render_distance; - } - - pub fn beginFrame(self: *LODStreamingCoordinator, storage: *ChunkStorage, gen_queue: *JobQueue, mesh_queue: *JobQueue, player_pos: Vec3, dt: f32, frame_counter: u64) StreamingFrame { - _ = self.player_movement.update(player_pos, dt); - - const pc = worldToChunkFromFloat(player_pos.x, player_pos.z); - const moved = pc.chunk_x != self.last_pc.x or pc.chunk_z != self.last_pc.z; - const target_render_dist = self.targetRenderDistance(); - - self.updateStartupRadius(storage, pc.chunk_x, pc.chunk_z, target_render_dist, frame_counter); - const render_dist = if (self.startup_stream_radius > 0) self.startup_stream_radius else target_render_dist; - if (self.lod_manager) |manager| { - self.horizon_bootstrap_ready = manager.hasRenderableCoarsestNear(pc.chunk_x, pc.chunk_z); - } - // Keep generation and meshing ahead of the visible startup radius so - // expansion does not serialize one ring behind another. - const prefetch_distance = if (self.lod_manager != null and !self.horizon_bootstrap_ready) - @as(i32, 0) - else - STARTUP_RADIUS_STEP * STARTUP_PREFETCH_RINGS; - const stream_dist = @min(target_render_dist, render_dist + prefetch_distance); - self.effective_render_dist = render_dist; - - if (moved) { - self.last_pc = .{ .x = pc.chunk_x, .z = pc.chunk_z }; - gen_queue.updatePlayerPos(pc.chunk_x, pc.chunk_z) catch {}; - mesh_queue.updatePlayerPos(pc.chunk_x, pc.chunk_z) catch {}; - } - - return .{ - .pc_x = pc.chunk_x, - .pc_z = pc.chunk_z, - .moved = moved, - .target_render_dist = target_render_dist, - .render_dist = render_dist, - .stream_dist = stream_dist, - .movement = self.player_movement, - }; - } - - pub fn updateLOD(self: *LODStreamingCoordinator, player_pos: Vec3, storage: *ChunkStorage) void { - const lod_mgr = self.lod_manager orelse return; - const velocity = Vec3.init( - self.player_movement.dir_x * self.player_movement.speed, - 0, - self.player_movement.dir_z * self.player_movement.speed, - ); - lod_mgr.update(player_pos, velocity, ChunkStorage.isChunkRenderable, storage) catch |err| { - log.log.warn("LOD update error (non-fatal): {}", .{err}); - }; - } - - pub fn isStartupBusy(self: *const LODStreamingCoordinator, stats: QueueStats, target_render_dist: i32) bool { - const startup_target = if (self.lod_manager) |mgr| - @min(target_render_dist, mgr.config.getChunkRenderRadius()) - else - target_render_dist; - - if (self.getActiveRenderDistance() < startup_target) return true; - return stats.gen_queue > 0 or stats.mesh_queue > 0 or stats.upload_queue > 0; - } - - fn updateStartupRadius(self: *LODStreamingCoordinator, storage: *ChunkStorage, pc_x: i32, pc_z: i32, target_render_dist: i32, frame_counter: u64) void { - if (target_render_dist <= 0) { - self.startup_stream_radius = 0; - return; - } - - if (self.startup_stream_radius <= 0) { - self.startup_stream_radius = @min(target_render_dist, STARTUP_RADIUS_INITIAL); - return; - } - - if (self.startup_stream_radius >= target_render_dist) { - self.startup_stream_radius = target_render_dist; - return; - } - - if (frame_counter % STARTUP_RADIUS_CHECK_PERIOD != 0) return; - - var total_in_radius: u64 = 0; - var ready_in_radius: u64 = 0; - - storage.chunks_mutex.lockShared(); - defer storage.chunks_mutex.unlockShared(); - - const radius = @as(i64, self.startup_stream_radius); - var sample_z = -STARTUP_READINESS_GRID_RADIUS; - while (sample_z <= STARTUP_READINESS_GRID_RADIUS) : (sample_z += 1) { - var sample_x = -STARTUP_READINESS_GRID_RADIUS; - while (sample_x <= STARTUP_READINESS_GRID_RADIUS) : (sample_x += 1) { - if (sample_x * sample_x + sample_z * sample_z > STARTUP_READINESS_GRID_RADIUS * STARTUP_READINESS_GRID_RADIUS) continue; - const cx = @as(i64, pc_x) + @divTrunc(sample_x * radius, STARTUP_READINESS_GRID_RADIUS); - const cz = @as(i64, pc_z) + @divTrunc(sample_z * radius, STARTUP_READINESS_GRID_RADIUS); - if (cx < std.math.minInt(i32) or cx > std.math.maxInt(i32) or cz < std.math.minInt(i32) or cz > std.math.maxInt(i32)) continue; - - total_in_radius += 1; - if (storage.chunks.get(.{ .x = @intCast(cx), .z = @intCast(cz) })) |data| { - if (data.chunk.state == .renderable or data.render.mesh.solid_allocation != null or data.render.mesh.cutout_allocation != null or data.render.mesh.fluid_allocation != null) { - ready_in_radius += 1; - } - } - } - } - - if (total_in_radius == 0) return; - if (@as(u128, ready_in_radius) * 100 < @as(u128, total_in_radius) * 85) return; - - self.startup_stream_radius = @min(target_render_dist, self.startup_stream_radius + STARTUP_RADIUS_STEP); - log.log.info("STARTUP_STREAM_RADIUS: expanded to {} / {}", .{ self.startup_stream_radius, target_render_dist }); - } -}; - -test "startup streaming prefetches two rings beyond visible radius" { - var coordinator = LODStreamingCoordinator.init(22); - coordinator.startup_stream_radius = 3; - - const render_dist = coordinator.startup_stream_radius; - const stream_dist = @min( - coordinator.targetRenderDistance(), - render_dist + LODStreamingCoordinator.STARTUP_RADIUS_STEP * LODStreamingCoordinator.STARTUP_PREFETCH_RINGS, - ); - try std.testing.expectEqual(@as(i32, 3), render_dist); - try std.testing.expectEqual(@as(i32, 7), stream_dist); -} - -test "runtime render-distance changes preserve or clamp the active radius" { - var coordinator = LODStreamingCoordinator.init(12); - coordinator.startup_stream_radius = 12; - coordinator.effective_render_dist = 12; - - try std.testing.expect(coordinator.setRenderDistance(4096)); - try std.testing.expectEqual(@as(i32, 12), coordinator.getActiveRenderDistance()); - try std.testing.expectEqual(@as(i32, 12), coordinator.startup_stream_radius); - - try std.testing.expect(coordinator.setRenderDistance(8)); - try std.testing.expectEqual(@as(i32, 8), coordinator.getActiveRenderDistance()); - try std.testing.expectEqual(@as(i32, 8), coordinator.startup_stream_radius); -} diff --git a/modules/world-lod/src/lod_tile.zig b/modules/world-lod/src/lod_tile.zig deleted file mode 100644 index 2ff9b2a7..00000000 --- a/modules/world-lod/src/lod_tile.zig +++ /dev/null @@ -1,839 +0,0 @@ -//! CPU-owned compact source data for far-terrain tiles. -//! -//! `CompactLODSample` is a fixed 16-byte, little-endian ABI. It deliberately -//! contains semantic `world_core.BlockType` IDs, not atlas or GPU resource IDs, -//! so it can be built, cached, and tested without a renderer. The two 64-bit -//! words have this bit layout (least-significant bit first): -//! -//! ```text -//! bits 0..15 terrain height: signed i16, 1/8 block, [-4096, 4095.875] -//! bits 16..31 water height: signed i16, 1/8 block, [-4095.875, 4095.875]; -//! -4096 is the no-water sentinel -//! bits 32..39 water depth: unsigned u8, 1/4 block, [0, 63.75] -//! bits 40..47 water coverage: unsigned u8, [0, 1] -//! bits 48..54 surface material semantic ID -//! bits 55..61 subsurface material semantic ID -//! bits 62..68 foundation material semantic ID -//! bits 69..92 RGB color (low 24 bits of source color) -//! bits 93..96 sky light, [0, 15] -//! bits 97..100 block light, [0, 15] -//! bits 101..106 ambient occlusion, unsigned u6, [0, 1] -//! bits 107..112 vegetation coverage, unsigned u6, [0, 1] -//! bits 113..120 vegetation height, unsigned u8, 1/2 block, [0, 127.5] -//! bits 121..122 provenance (worldgen/chunk-derived/edited) -//! bits 123..127 reserved, zero -//! ``` -//! -//! All float quantizers first map non-finite input to zero, clamp to their -//! documented range, then use `@round` (nearest integer). Version 1 recognizes -//! block IDs through `stone_stairs`; later or out-of-range IDs are reduced to -//! the semantic `stone` fallback rather than becoming renderer-specific data. - -const std = @import("std"); -const Allocator = std.mem.Allocator; -const world_core = @import("world-core"); - -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; -const BlockType = world_core.BlockType; -const LODMaterialLayers = world_core.LODMaterialLayers; -const LODWaterState = world_core.LODWaterState; -const LODLightingHint = world_core.LODLightingHint; -const LODVegetationHint = world_core.LODVegetationHint; -const LODColumnProvenance = world_core.LODColumnProvenance; - -pub const COMPACT_LOD_TILE_MAGIC: u32 = 0x5444_4C43; // "CLDT" in little-endian bytes. -pub const COMPACT_LOD_TILE_VERSION: u16 = 1; -pub const COMPACT_LOD_SAMPLE_BYTES: usize = 16; -pub const COMPACT_LOD_TILE_HEADER_BYTES: usize = 20; -const WATER_HEIGHT_NONE: i16 = std.math.minInt(i16); -const HEIGHT_SCALE: f32 = 8.0; -const DEPTH_SCALE: f32 = 4.0; -const VEGETATION_HEIGHT_SCALE: f32 = 2.0; -const MAX_ENCODED_BLOCK_ID: u8 = 0x7f; -const MAX_KNOWN_BLOCK_ID: u8 = @intFromEnum(BlockType.stone_stairs); -const MAX_VALID_WIRE_BLOCK_ID: u7 = @intCast(@min(MAX_ENCODED_BLOCK_ID, MAX_KNOWN_BLOCK_ID)); - -pub const TileEdge = enum(u2) { - north = 0, - east = 1, - south = 2, - west = 3, -}; - -/// All currently defined `TileEdge` bits. This is kept separate from the -/// serialized version so a future edge/encoding extension cannot silently turn -/// an unknown bit into a seamless edge. -pub const TILE_EDGE_MASK: u8 = 0x0f; - -pub fn edgeMask(edge: TileEdge) u8 { - return @as(u8, 1) << @intFromEnum(edge); -} - -pub const TileError = error{ - InvalidLod, - InvalidSourceData, - InvalidMagic, - UnsupportedVersion, - InvalidHeader, - DataTooShort, - InvalidLength, - ChecksumMismatch, - InvalidSample, - UnsupportedSourceFeatures, -}; - -/// A renderer-independent compact sample. `bytes` is the stable wire/upload ABI. -pub const CompactLODSample = struct { - bytes: [COMPACT_LOD_SAMPLE_BYTES]u8, - - pub fn decode(self: CompactLODSample) DecodedSample { - const low = std.mem.readInt(u64, self.bytes[0..8], .little); - const high = std.mem.readInt(u64, self.bytes[8..16], .little); - const bits = @as(u128, low) | (@as(u128, high) << 64); - - const water_raw = signedField(bits, 16); - const water_present = water_raw != WATER_HEIGHT_NONE; - return .{ - .terrain_height = dequantizeHeight(signedField(bits, 0)), - .water = .{ - .is_surface = water_present, - .surface_height = if (water_present) dequantizeHeight(water_raw) else 0.0, - .depth = @as(f32, @floatFromInt(unsignedField(bits, 32, u8))) / DEPTH_SCALE, - .coverage = @as(f32, @floatFromInt(unsignedField(bits, 40, u8))) / 255.0, - }, - .material_layers = .{ - .surface = semanticMaterialFromId(unsignedField(bits, 48, u7)), - .subsurface = semanticMaterialFromId(unsignedField(bits, 55, u7)), - .foundation = semanticMaterialFromId(unsignedField(bits, 62, u7)), - }, - .color = unsignedField(bits, 69, u32) & 0x00ff_ffff, - .lighting = .{ - .sky_light = unsignedField(bits, 93, u8) & 0x0f, - .block_light = unsignedField(bits, 97, u8) & 0x0f, - .ambient_occlusion = @as(f32, @floatFromInt(unsignedField(bits, 101, u8) & 0x3f)) / 63.0, - }, - .vegetation = .{ - .tree_coverage = @as(f32, @floatFromInt(unsignedField(bits, 107, u8) & 0x3f)) / 63.0, - .avg_tree_height = @as(f32, @floatFromInt(unsignedField(bits, 113, u8) & 0xff)) / VEGETATION_HEIGHT_SCALE, - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = .air, - .leaves = .air, - }, - .provenance = provenanceFromBits(unsignedField(bits, 121, u8)), - }; - } - - fn isValid(self: CompactLODSample) bool { - const low = std.mem.readInt(u64, self.bytes[0..8], .little); - const high = std.mem.readInt(u64, self.bytes[8..16], .little); - const bits = @as(u128, low) | (@as(u128, high) << 64); - if ((high >> 59) != 0) return false; - if (((high >> 57) & 0x3) > @intFromEnum(LODColumnProvenance.edited)) return false; - return unsignedField(bits, 48, u7) <= MAX_VALID_WIRE_BLOCK_ID and - unsignedField(bits, 55, u7) <= MAX_VALID_WIRE_BLOCK_ID and - unsignedField(bits, 62, u7) <= MAX_VALID_WIRE_BLOCK_ID; - } -}; - -comptime { - std.debug.assert(@sizeOf(CompactLODSample) == COMPACT_LOD_SAMPLE_BYTES); - std.debug.assert(@alignOf(CompactLODSample) == 1); -} - -/// The lossily reconstructed source-column attributes represented by a sample. -pub const DecodedSample = struct { - terrain_height: f32, - water: LODWaterState, - material_layers: LODMaterialLayers, - color: u32, - lighting: LODLightingHint, - vegetation: LODVegetationHint, - provenance: LODColumnProvenance, -}; - -pub const ByteMetrics = struct { - /// Bytes that would be uploaded for the compact padded sample grid. - compact_upload_bytes: usize, - /// Bytes emitted by the current CPU heightfield path for top faces alone. - conservative_expanded_top_grid_bytes: usize, - - pub fn savedBytes(self: ByteMetrics) usize { - return if (self.conservative_expanded_top_grid_bytes > self.compact_upload_bytes) - self.conservative_expanded_top_grid_bytes - self.compact_upload_bytes - else - 0; - } -}; - -/// An allocator-owned top-grid plus one duplicated source edge on every side. -/// `sampleClamped` accepts `-1..width` neighbor coordinates and is stable at a -/// tile boundary, making it suitable for CPU normal derivation before neighbors -/// are available. -pub const CompactLODTile = struct { - allocator: Allocator, - lod_level: LODLevel, - width: u32, - /// Bit per `TileEdge` whose apron was populated from a same-level neighbor. - neighbor_apron_mask: u8 = 0, - samples: []CompactLODSample, - - pub const Support = enum { supported, invalid_lod, invalid_source, vertical_spans, water_topology }; - - pub fn support(source: *const LODSimplifiedData, lod_level: LODLevel) Support { - if (lod_level != .lod3 and lod_level != .lod4) return .invalid_lod; - if (!LODSimplifiedData.isSupportedGridSize(lod_level, source.width)) return .invalid_source; - if (hasUnsupportedVerticalSpans(source)) return .vertical_spans; - if (hasUnsupportedWaterTopology(source)) return .water_topology; - return .supported; - } - - pub fn initFromSimplified(allocator: Allocator, lod_level: LODLevel, source: *const LODSimplifiedData) !CompactLODTile { - switch (support(source, lod_level)) { - .supported => {}, - .invalid_lod => return TileError.InvalidLod, - .invalid_source => return TileError.InvalidSourceData, - .vertical_spans, .water_topology => return TileError.UnsupportedSourceFeatures, - } - const source_count = squareCount(source.width) orelse return TileError.InvalidSourceData; - if (source.heightmap.len != source_count or - source.biomes.len != source_count or - source.top_blocks.len != source_count or - source.colors.len != source_count or - source.material_layers.len != source_count or - source.water.len != source_count or - source.lighting.len != source_count or - source.vegetation.len != source_count or - source.provenance.len != source_count) - { - return TileError.InvalidSourceData; - } - - const padded_stride = apronStride(source.width) orelse return TileError.InvalidSourceData; - const sample_count = squareCount(padded_stride) orelse return TileError.InvalidSourceData; - const samples = try allocator.alloc(CompactLODSample, sample_count); - errdefer allocator.free(samples); - - var apron_z: u32 = 0; - while (apron_z < padded_stride) : (apron_z += 1) { - const source_z = apronToSourceCoordinate(apron_z, source.width); - var apron_x: u32 = 0; - while (apron_x < padded_stride) : (apron_x += 1) { - const source_x = apronToSourceCoordinate(apron_x, source.width); - const index = @as(usize, source_z) * @as(usize, source.width) + @as(usize, source_x); - samples[@as(usize, apron_z) * @as(usize, padded_stride) + @as(usize, apron_x)] = packSourceColumn(source, index); - } - } - - return .{ - .allocator = allocator, - .lod_level = lod_level, - .width = source.width, - .neighbor_apron_mask = 0, - .samples = samples, - }; - } - - pub fn deinit(self: *CompactLODTile) void { - self.allocator.free(self.samples); - self.* = undefined; - } - - pub fn stride(self: *const CompactLODTile) u32 { - return self.width + 2; - } - - /// Returns an interior top-grid sample, excluding the duplicated-edge apron. - pub fn sample(self: *const CompactLODTile, x: u32, z: u32) ?CompactLODSample { - if (x >= self.width or z >= self.width) return null; - const stride_value = self.stride(); - return self.samples[@as(usize, z + 1) * @as(usize, stride_value) + @as(usize, x + 1)]; - } - - /// Returns the padded sample at a raw apron coordinate (`0..width + 1`). - pub fn sampleApron(self: *const CompactLODTile, x: u32, z: u32) ?CompactLODSample { - const stride_value = self.stride(); - if (x >= stride_value or z >= stride_value) return null; - return self.samples[@as(usize, z) * @as(usize, stride_value) + @as(usize, x)]; - } - - /// Returns an interior or immediate neighbor sample from the apron. Inputs - /// outside `-1..width` are clamped to that supported normal-sampling range. - pub fn sampleClamped(self: *const CompactLODTile, x: i32, z: i32) CompactLODSample { - const max_coordinate: i32 = @intCast(self.width); - const apron_x: u32 = @intCast(std.math.clamp(x, @as(i32, -1), max_coordinate) + 1); - const apron_z: u32 = @intCast(std.math.clamp(z, @as(i32, -1), max_coordinate) + 1); - return self.sampleApron(apron_x, apron_z).?; - } - - /// Replaces one duplicated fallback apron with authoritative samples from - /// the adjacent same-level tile. Region grids include both endpoints, so - /// the shared boundary is neighbor coordinate 0/width-1 and the apron must - /// copy the first sample beyond it (1/width-2). - pub fn applyNeighborApron(self: *CompactLODTile, edge: TileEdge, neighbor: *const CompactLODTile) !void { - if (self.lod_level != neighbor.lod_level or self.width != neighbor.width) return TileError.InvalidSourceData; - const stride_value = self.stride(); - const stride_usize: usize = stride_value; - var coordinate: u32 = 0; - while (coordinate < self.width) : (coordinate += 1) { - const destination = switch (edge) { - .north => @as(usize, coordinate + 1), - .east => @as(usize, coordinate + 1) * stride_usize + @as(usize, stride_value - 1), - .south => @as(usize, stride_value - 1) * stride_usize + @as(usize, coordinate + 1), - .west => @as(usize, coordinate + 1) * stride_usize, - }; - const source_sample = switch (edge) { - .north => neighbor.sample(coordinate, neighbor.width - 2).?, - .east => neighbor.sample(1, coordinate).?, - .south => neighbor.sample(coordinate, 1).?, - .west => neighbor.sample(neighbor.width - 2, coordinate).?, - }; - self.samples[destination] = source_sample; - } - self.neighbor_apron_mask |= @as(u8, 1) << @intFromEnum(edge); - } - - pub fn hasNeighborApron(self: *const CompactLODTile, edge: TileEdge) bool { - return (self.neighbor_apron_mask & edgeMask(edge)) != 0; - } - - /// Only an edge copied from a same-level neighbor is seamless. A duplicated - /// local edge is valid compact data, but it is deliberately *not* a seam - /// claim: the renderer must retain that edge's skirt and use its normal - /// fallback until an authoritative apron was available before upload. - pub fn skirtMask(self: *const CompactLODTile) u8 { - return (~self.neighbor_apron_mask) & TILE_EDGE_MASK; - } - - /// Compares compact bytes with the current CPU top-face expansion using an - /// explicit vertex size supplied by the renderer-facing caller. - pub fn byteMetrics(self: *const CompactLODTile, expanded_vertex_bytes: usize) ByteMetrics { - const cells = @as(usize, self.width - 1) * @as(usize, self.width - 1); - return .{ - .compact_upload_bytes = self.samples.len * COMPACT_LOD_SAMPLE_BYTES, - // The current path emits two triangles (six Vertex values) - // per cell before side faces, water, or vegetation are considered. - .conservative_expanded_top_grid_bytes = cells * 6 * expanded_vertex_bytes, - }; - } - - pub fn serializedSize(self: *const CompactLODTile) usize { - return COMPACT_LOD_TILE_HEADER_BYTES + self.samples.len * COMPACT_LOD_SAMPLE_BYTES; - } - - /// Serializes the versioned little-endian tile format. The returned buffer is caller-owned. - pub fn serialize(self: *const CompactLODTile, allocator: Allocator) ![]u8 { - const bytes = try allocator.alloc(u8, self.serializedSize()); - errdefer allocator.free(bytes); - - std.mem.writeInt(u32, bytes[0..4], COMPACT_LOD_TILE_MAGIC, .little); - std.mem.writeInt(u16, bytes[4..6], COMPACT_LOD_TILE_VERSION, .little); - bytes[6] = @intFromEnum(self.lod_level); - bytes[7] = self.neighbor_apron_mask; - std.mem.writeInt(u32, bytes[8..12], self.width, .little); - std.mem.writeInt(u32, bytes[12..16], @intCast(self.samples.len), .little); - std.mem.writeInt(u32, bytes[16..20], 0, .little); - @memcpy(bytes[COMPACT_LOD_TILE_HEADER_BYTES..], std.mem.sliceAsBytes(self.samples)); - std.mem.writeInt(u32, bytes[16..20], computeTileCrc(bytes), .little); - return bytes; - } - - /// Deserializes only a complete, validated current-version tile. The result owns its samples. - pub fn deserialize(allocator: Allocator, bytes: []const u8) !CompactLODTile { - if (bytes.len < COMPACT_LOD_TILE_HEADER_BYTES) return TileError.DataTooShort; - if (std.mem.readInt(u32, bytes[0..4], .little) != COMPACT_LOD_TILE_MAGIC) return TileError.InvalidMagic; - if (std.mem.readInt(u16, bytes[4..6], .little) != COMPACT_LOD_TILE_VERSION) return TileError.UnsupportedVersion; - if ((bytes[7] & 0xf0) != 0) return TileError.InvalidHeader; - - const lod_level = std.enums.fromInt(LODLevel, bytes[6]) orelse return TileError.InvalidLod; - if (lod_level != .lod3 and lod_level != .lod4) return TileError.InvalidLod; - const width = std.mem.readInt(u32, bytes[8..12], .little); - if (!LODSimplifiedData.isSupportedGridSize(lod_level, width)) return TileError.InvalidHeader; - const stride_value = apronStride(width) orelse return TileError.InvalidHeader; - const sample_count = squareCount(stride_value) orelse return TileError.InvalidHeader; - if (std.mem.readInt(u32, bytes[12..16], .little) != sample_count) return TileError.InvalidHeader; - - const payload_len = std.math.mul(usize, sample_count, COMPACT_LOD_SAMPLE_BYTES) catch return TileError.InvalidHeader; - if (bytes.len != COMPACT_LOD_TILE_HEADER_BYTES + payload_len) return TileError.InvalidLength; - const payload = bytes[COMPACT_LOD_TILE_HEADER_BYTES..]; - if (std.mem.readInt(u32, bytes[16..20], .little) != computeTileCrc(bytes)) return TileError.ChecksumMismatch; - - const samples = try allocator.alloc(CompactLODSample, sample_count); - errdefer allocator.free(samples); - @memcpy(std.mem.sliceAsBytes(samples), payload); - for (samples) |sample_value| { - if (!sample_value.isValid()) return TileError.InvalidSample; - } - return .{ .allocator = allocator, .lod_level = lod_level, .width = width, .neighbor_apron_mask = bytes[7], .samples = samples }; - } -}; - -/// Reduces a block to the renderer-independent seven-bit semantic material ID. -/// Values not known by this build use stone until an ABI version adds them. -pub fn reduceMaterial(block: BlockType) u7 { - const id: u8 = @intFromEnum(block); - return @intCast(if (id <= @min(MAX_ENCODED_BLOCK_ID, MAX_KNOWN_BLOCK_ID)) id else @intFromEnum(BlockType.stone)); -} - -fn packSourceColumn(source: *const LODSimplifiedData, index: usize) CompactLODSample { - const water = source.water[index]; - const water_height: i16 = if (water.is_surface) quantizeWaterHeight(water.surface_height) else WATER_HEIGHT_NONE; - const layers = source.material_layers[index]; - - var bits: u128 = 0; - bits |= @as(u128, @as(u16, @bitCast(quantizeTerrainHeight(terrainHeightForColumn(source, index))))); - bits |= @as(u128, @as(u16, @bitCast(water_height))) << 16; - bits |= @as(u128, if (water.is_surface) quantizeRange(water.depth, DEPTH_SCALE, 255) else 0) << 32; - bits |= @as(u128, if (water.is_surface) quantizeUnit(water.coverage, 255) else 0) << 40; - bits |= @as(u128, reduceMaterial(layers.surface)) << 48; - bits |= @as(u128, reduceMaterial(layers.subsurface)) << 55; - bits |= @as(u128, reduceMaterial(layers.foundation)) << 62; - bits |= @as(u128, source.colors[index] & 0x00ff_ffff) << 69; - bits |= @as(u128, @min(source.lighting[index].sky_light, 15)) << 93; - bits |= @as(u128, @min(source.lighting[index].block_light, 15)) << 97; - bits |= @as(u128, quantizeUnit(source.lighting[index].ambient_occlusion, 63)) << 101; - bits |= @as(u128, quantizeUnit(source.vegetation[index].tree_coverage, 63)) << 107; - bits |= @as(u128, quantizeRange(source.vegetation[index].avg_tree_height, VEGETATION_HEIGHT_SCALE, 255)) << 113; - bits |= @as(u128, @intFromEnum(source.provenance[index])) << 121; - - var bytes: [COMPACT_LOD_SAMPLE_BYTES]u8 = undefined; - std.mem.writeInt(u64, bytes[0..8], @truncate(bits), .little); - std.mem.writeInt(u64, bytes[8..16], @truncate(bits >> 64), .little); - return .{ .bytes = bytes }; -} - -fn quantizeTerrainHeight(value: f32) i16 { - return quantizeSigned(value, std.math.minInt(i16), std.math.maxInt(i16)); -} - -fn terrainHeightForColumn(source: *const LODSimplifiedData, index: usize) f32 { - const terrain = source.heightmap[index]; - const water = source.water[index]; - if (!water.is_surface or water.coverage <= 0.0 or terrain < water.surface_height) return terrain; - // Some coarse source columns encode the water surface as their height. Keep - // the independently summarized floor in that ambiguous case. - return if (water.depth > 0.0) @max(0.0, water.surface_height - water.depth) else water.surface_height; -} - -fn hasUnsupportedVerticalSpans(source: *const LODSimplifiedData) bool { - if (!source.hasVerticalSpans()) return false; - var z: u32 = 0; - while (z < source.width) : (z += 1) { - var x: u32 = 0; - while (x < source.width) : (x += 1) { - const count = source.verticalSpanCount(x, z); - // Version 1 represents one heightfield surface plus summary water - // and vegetation fields. Any additional explicit topology must use - // the maintained CPU mesh path until a versioned encoding exists. - if (count > 1) return true; - var span_index: u8 = 0; - while (span_index < count) : (span_index += 1) { - const span = source.getVerticalSpan(x, z, span_index) orelse return true; - if (span.min_height > 0.01) return true; - } - } - } - return false; -} - -fn hasUnsupportedWaterTopology(source: *const LODSimplifiedData) bool { - var has_water = false; - var has_dry = false; - for (source.water) |water| { - if (water.is_surface and water.coverage > 0.001 and water.coverage < 0.999) return true; - if (water.is_surface and water.coverage >= 0.999) { - has_water = true; - } else { - has_dry = true; - } - // The reusable full-grid index buffer cannot reject a shoreline cell - // at primitive granularity. A triangle with one collapsed dry vertex - // still stretches toward the clip point, so mixed tiles stay expanded. - if (has_water and has_dry) return true; - } - return false; -} - -fn quantizeWaterHeight(value: f32) i16 { - return quantizeSigned(value, std.math.minInt(i16) + 1, std.math.maxInt(i16)); -} - -fn quantizeSigned(value: f32, min: i16, max: i16) i16 { - const finite = if (std.math.isFinite(value)) value else 0.0; - const min_value = @as(f32, @floatFromInt(min)) / HEIGHT_SCALE; - const max_value = @as(f32, @floatFromInt(max)) / HEIGHT_SCALE; - return @intFromFloat(@round(std.math.clamp(finite, min_value, max_value) * HEIGHT_SCALE)); -} - -fn quantizeRange(value: f32, scale: f32, max: u8) u8 { - const finite = if (std.math.isFinite(value)) value else 0.0; - const max_value = @as(f32, @floatFromInt(max)) / scale; - return @intFromFloat(@round(std.math.clamp(finite, 0.0, max_value) * scale)); -} - -fn quantizeUnit(value: f32, max: u8) u8 { - const finite = if (std.math.isFinite(value)) value else 0.0; - return @intFromFloat(@round(std.math.clamp(finite, 0.0, 1.0) * @as(f32, @floatFromInt(max)))); -} - -fn dequantizeHeight(value: i16) f32 { - return @as(f32, @floatFromInt(value)) / HEIGHT_SCALE; -} - -fn signedField(bits: u128, shift: u7) i16 { - const raw: u16 = @truncate(bits >> shift); - return @bitCast(raw); -} - -fn unsignedField(bits: u128, shift: u7, comptime T: type) T { - return @truncate(bits >> shift); -} - -fn semanticMaterialFromId(id: u7) BlockType { - return @enumFromInt(id); -} - -fn provenanceFromBits(bits: u8) LODColumnProvenance { - return std.enums.fromInt(LODColumnProvenance, bits) orelse .worldgen; -} - -fn computeTileCrc(bytes: []const u8) u32 { - var crc = std.hash.Crc32.init(); - crc.update(bytes[0..16]); - crc.update(&.{ 0, 0, 0, 0 }); - crc.update(bytes[COMPACT_LOD_TILE_HEADER_BYTES..]); - return crc.final(); -} - -fn apronStride(width: u32) ?u32 { - return std.math.add(u32, width, 2) catch null; -} - -fn squareCount(width: u32) ?usize { - const count = std.math.mul(u32, width, width) catch return null; - return @intCast(count); -} - -fn apronToSourceCoordinate(apron_coordinate: u32, width: u32) u32 { - if (apron_coordinate == 0) return 0; - return @min(apron_coordinate - 1, width - 1); -} - -test "CompactLODSample golden vector decodes signed 128-bit height fields" { - // terrain=-12.5, water=7.25. The upper word also proves that decode does - // not accidentally truncate the packed u128 before reading later fields. - var bits: u128 = 0; - bits |= @as(u128, @as(u16, @bitCast(@as(i16, -100)))); - bits |= @as(u128, @as(u16, @bitCast(@as(i16, 58)))) << 16; - bits |= @as(u128, 9) << 32; - bits |= @as(u128, 255) << 40; - bits |= @as(u128, 15) << 93; - var bytes: [16]u8 = undefined; - std.mem.writeInt(u64, bytes[0..8], @truncate(bits), .little); - std.mem.writeInt(u64, bytes[8..16], @truncate(bits >> 64), .little); - const decoded = (CompactLODSample{ .bytes = bytes }).decode(); - try std.testing.expectEqual(@as(f32, -12.5), decoded.terrain_height); - try std.testing.expectEqual(@as(f32, 7.25), decoded.water.surface_height); - try std.testing.expectEqual(@as(u8, 15), decoded.lighting.sky_light); -} - -test "CompactLODTile round trips its versioned little-endian payload" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod3); - defer source.deinit(); - for (source.water) |*water| water.* = .{ .is_surface = true, .surface_height = 124.25, .depth = 3.5, .coverage = 1.0 }; - source.setColumn(2, 3, 123.125, .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, 0xaabb_ccdd, .{ .is_surface = true, .surface_height = 124.25, .depth = 3.5, .coverage = 1.0 }, .{ .sky_light = 15, .block_light = 7, .ambient_occlusion = 0.5 }, .{ .tree_coverage = 0.5, .avg_tree_height = 12.5, .offset_x = 0.25, .offset_z = 0.75, .trunk = .wood, .leaves = .leaves }); - source.setColumnProvenance(2, 3, .edited); - - var tile = try CompactLODTile.initFromSimplified(allocator, .lod3, &source); - defer tile.deinit(); - const wire = try tile.serialize(allocator); - defer allocator.free(wire); - var restored = try CompactLODTile.deserialize(allocator, wire); - defer restored.deinit(); - - try std.testing.expectEqual(COMPACT_LOD_TILE_HEADER_BYTES + tile.samples.len * COMPACT_LOD_SAMPLE_BYTES, wire.len); - try std.testing.expectEqualDeep(tile.sample(2, 3).?, restored.sample(2, 3).?); - const decoded = restored.sample(2, 3).?.decode(); - try std.testing.expectEqual(@as(f32, 123.125), decoded.terrain_height); - try std.testing.expectEqual(@as(u32, 0x00bb_ccdd), decoded.color); - try std.testing.expectEqual(LODColumnProvenance.edited, decoded.provenance); -} - -test "CompactLODTile clamps and rounds quantized source values deterministically" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - for (source.water) |*water| water.* = .{ .is_surface = true, .surface_height = 9999.0, .depth = 9999.0, .coverage = 1.0 }; - source.setColumn(0, 0, 1.07, .plains, LODMaterialLayers.default(.stone), 0, .{ .is_surface = true, .surface_height = 9999.0, .depth = 9999.0, .coverage = 2.0 }, .{ .sky_light = 255, .block_light = 99, .ambient_occlusion = -1.0 }, .{ .tree_coverage = 2.0, .avg_tree_height = 999.0, .offset_x = 0.0, .offset_z = 0.0, .trunk = .air, .leaves = .air }); - source.setHeight(1, 0, -9999.0); - - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - const first = tile.sample(0, 0).?.decode(); - const second = tile.sample(1, 0).?.decode(); - try std.testing.expectEqual(@as(f32, 1.125), first.terrain_height); - try std.testing.expectEqual(@as(f32, 4095.875), first.water.surface_height); - try std.testing.expectEqual(@as(f32, 63.75), first.water.depth); - try std.testing.expectEqual(@as(f32, 1.0), first.water.coverage); - try std.testing.expectEqual(@as(u8, 15), first.lighting.sky_light); - try std.testing.expectEqual(@as(f32, 0.0), first.lighting.ambient_occlusion); - try std.testing.expectEqual(@as(f32, 127.5), first.vegetation.avg_tree_height); - try std.testing.expectEqual(@as(f32, -4096.0), second.terrain_height); -} - -test "CompactLODTile accepts reduced LOD3 and LOD4 density grids" { - const allocator = std.testing.allocator; - inline for ([_]struct { lod: LODLevel, density: f32, width: u32 }{ - .{ .lod = .lod3, .density = 0.5, .width = 65 }, - .{ .lod = .lod4, .density = 0.25, .width = 17 }, - }) |case| { - var source = try LODSimplifiedData.initWithSampleDensity(allocator, case.lod, case.density); - defer source.deinit(); - var tile = try CompactLODTile.initFromSimplified(allocator, case.lod, &source); - defer tile.deinit(); - try std.testing.expectEqual(case.width, tile.width); - try std.testing.expectEqual((case.width - 1) * (case.width - 1) * 6, (tile.width - 1) * (tile.width - 1) * 6); - } -} - -test "CompactLODTile rejects partial water so CPU shore geometry is retained" { - var source = try LODSimplifiedData.init(std.testing.allocator, .lod4); - defer source.deinit(); - source.setColumn(1, 1, 64.0, .plains, LODMaterialLayers.default(.sand), 0, .{ - .is_surface = true, - .surface_height = 65.0, - .depth = 1.0, - .coverage = 0.5, - }, .daylight, .empty); - try std.testing.expectError(TileError.UnsupportedSourceFeatures, CompactLODTile.initFromSimplified(std.testing.allocator, .lod4, &source)); -} - -test "CompactLODTile uses the water-height sentinel for dry columns" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - source.setColumn(0, 0, 10.0, .plains, LODMaterialLayers.default(.sand), 0, .{ .is_surface = false, .surface_height = 42.0, .depth = 8.0, .coverage = 1.0 }, LODLightingHint.daylight, LODVegetationHint.empty); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - - const decoded = tile.sample(0, 0).?.decode(); - try std.testing.expect(!decoded.water.is_surface); - try std.testing.expectEqual(@as(f32, 0.0), decoded.water.surface_height); - try std.testing.expectEqual(@as(f32, 0.0), decoded.water.depth); - try std.testing.expectEqual(@as(f32, 0.0), decoded.water.coverage); -} - -test "CompactLODTile retains column provenance" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - source.setColumnProvenance(4, 5, .chunk_derived); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - - try std.testing.expectEqual(LODColumnProvenance.chunk_derived, tile.sample(4, 5).?.decode().provenance); -} - -test "CompactLODTile duplicates boundary samples for stable neighbor normals" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - source.setHeight(0, 0, 11.0); - source.setHeight(source.width - 1, source.width - 1, 22.0); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - - try std.testing.expectEqual(@as(f32, 11.0), tile.sampleClamped(-1, -1).decode().terrain_height); - try std.testing.expectEqual(@as(f32, 22.0), tile.sampleClamped(@as(i32, @intCast(source.width)), @as(i32, @intCast(source.width))).decode().terrain_height); - try std.testing.expectEqualDeep(tile.sampleApron(0, 0).?, tile.sample(0, 0).?); -} - -test "CompactLODTile patches aprons from same-level neighbors" { - const allocator = std.testing.allocator; - var left_source = try LODSimplifiedData.init(allocator, .lod4); - defer left_source.deinit(); - var right_source = try LODSimplifiedData.init(allocator, .lod4); - defer right_source.deinit(); - var mismatched_source = try LODSimplifiedData.init(allocator, .lod3); - defer mismatched_source.deinit(); - right_source.setHeight(0, 7, 44.0); - right_source.setHeight(1, 7, 88.0); - - var left = try CompactLODTile.initFromSimplified(allocator, .lod4, &left_source); - defer left.deinit(); - var right = try CompactLODTile.initFromSimplified(allocator, .lod4, &right_source); - defer right.deinit(); - var mismatched = try CompactLODTile.initFromSimplified(allocator, .lod3, &mismatched_source); - defer mismatched.deinit(); - try left.applyNeighborApron(.east, &right); - - try std.testing.expect(left.hasNeighborApron(.east)); - try std.testing.expectEqual(@as(f32, 88.0), left.sampleApron(left.stride() - 1, 8).?.decode().terrain_height); - try std.testing.expectEqual(@as(f32, 88.0), left.sampleClamped(@intCast(left.width), 7).decode().terrain_height); - try std.testing.expectError(TileError.InvalidSourceData, left.applyNeighborApron(.east, &mismatched)); -} - -test "CompactLODTile neighbor aprons use samples beyond shared endpoints" { - const allocator = std.testing.allocator; - var center_source = try LODSimplifiedData.init(allocator, .lod4); - defer center_source.deinit(); - var neighbor_source = try LODSimplifiedData.init(allocator, .lod4); - defer neighbor_source.deinit(); - const last = neighbor_source.width - 1; - neighbor_source.setHeight(3, last - 1, 11.0); - neighbor_source.setHeight(1, 3, 22.0); - neighbor_source.setHeight(3, 1, 33.0); - neighbor_source.setHeight(last - 1, 3, 44.0); - - inline for (.{ - .{ TileEdge.north, @as(f32, 11.0) }, - .{ TileEdge.east, @as(f32, 22.0) }, - .{ TileEdge.south, @as(f32, 33.0) }, - .{ TileEdge.west, @as(f32, 44.0) }, - }) |case| { - var center = try CompactLODTile.initFromSimplified(allocator, .lod4, ¢er_source); - defer center.deinit(); - var neighbor = try CompactLODTile.initFromSimplified(allocator, .lod4, &neighbor_source); - defer neighbor.deinit(); - try center.applyNeighborApron(case[0], &neighbor); - const sample = switch (case[0]) { - .north => center.sampleApron(4, 0).?, - .east => center.sampleApron(center.stride() - 1, 4).?, - .south => center.sampleApron(4, center.stride() - 1).?, - .west => center.sampleApron(0, 4).?, - }; - try std.testing.expectEqual(case[1], sample.decode().terrain_height); - } -} - -test "CompactLODTile stores terrain floor separately from water surface" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - for (source.water) |*water| water.* = .{ .is_surface = true, .surface_height = 64.0, .depth = 10.0, .coverage = 1.0 }; - source.setColumn(1, 1, 64.0, .ocean, .{ .surface = .water, .subsurface = .sand, .foundation = .stone }, 0, .{ .is_surface = true, .surface_height = 64.0, .depth = 10.0, .coverage = 1.0 }, .daylight, .empty); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - - const decoded = tile.sample(1, 1).?.decode(); - try std.testing.expectEqual(@as(f32, 54.0), decoded.terrain_height); - try std.testing.expectEqual(@as(f32, 64.0), decoded.water.surface_height); -} - -test "CompactLODTile rejects mixed wet and dry samples without per-cell water indices" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - source.setColumn(1, 1, 64.0, .ocean, .{ .surface = .water, .subsurface = .sand, .foundation = .stone }, 0, .{ .is_surface = true, .surface_height = 64.0, .depth = 4.0, .coverage = 1.0 }, .daylight, .empty); - try std.testing.expectError(TileError.UnsupportedSourceFeatures, CompactLODTile.initFromSimplified(allocator, .lod4, &source)); -} - -test "CompactLODTile rejects unsupported overhang spans" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod4); - defer source.deinit(); - try std.testing.expect(source.setVerticalSpan(1, 1, 0, .{ - .min_height = 12.0, - .max_height = 20.0, - .biome = .plains, - .material_layers = .{ .surface = .stone, .subsurface = .stone, .foundation = .stone }, - .color = 0, - .water = .empty, - .lighting = .daylight, - .vegetation = .empty, - })); - - try std.testing.expectError(TileError.UnsupportedSourceFeatures, CompactLODTile.initFromSimplified(allocator, .lod4, &source)); -} - -test "CompactLODTile rejects additional water spans until topology is versioned" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.initWithVerticalSpans(allocator, .lod4); - defer source.deinit(); - try std.testing.expect(source.setVerticalSpan(1, 1, 0, .{ - .min_height = 0.0, - .max_height = 54.0, - .biome = .ocean, - .material_layers = .{ .surface = .sand, .subsurface = .sand, .foundation = .stone }, - .color = 0, - .water = .empty, - .lighting = .daylight, - .vegetation = .empty, - })); - try std.testing.expect(source.setVerticalSpan(1, 1, 1, .{ - .min_height = 54.0, - .max_height = 64.0, - .biome = .ocean, - .material_layers = .{ .surface = .water, .subsurface = .water, .foundation = .stone }, - .color = 0, - .water = .{ .is_surface = true, .surface_height = 64.0, .depth = 10.0, .coverage = 1.0 }, - .lighting = .daylight, - .vegetation = .empty, - })); - - try std.testing.expectError(TileError.UnsupportedSourceFeatures, CompactLODTile.initFromSimplified(allocator, .lod4, &source)); -} - -test "CompactLODTile rejects a corrupted serialized payload" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - const wire = try tile.serialize(allocator); - defer allocator.free(wire); - wire[COMPACT_LOD_TILE_HEADER_BYTES] ^= 0x80; - - try std.testing.expectError(TileError.ChecksumMismatch, CompactLODTile.deserialize(allocator, wire)); -} - -test "CompactLODTile rejects unknown semantic materials with a valid checksum" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - const wire = try tile.serialize(allocator); - defer allocator.free(wire); - - const sample = wire[COMPACT_LOD_TILE_HEADER_BYTES..][0..COMPACT_LOD_SAMPLE_BYTES]; - var low = std.mem.readInt(u64, sample[0..8], .little); - low &= ~(@as(u64, 0x7f) << 48); - low |= @as(u64, MAX_KNOWN_BLOCK_ID + 1) << 48; - std.mem.writeInt(u64, sample[0..8], low, .little); - std.mem.writeInt(u32, wire[16..20], computeTileCrc(wire), .little); - - try std.testing.expectError(TileError.InvalidSample, CompactLODTile.deserialize(allocator, wire)); -} - -test "CompactLODTile reduces future material IDs and reports compact byte metrics" { - const future_block: BlockType = @enumFromInt(200); - try std.testing.expectEqual(@as(u7, @intFromEnum(BlockType.stone)), reduceMaterial(future_block)); - try std.testing.expectEqual(@as(u7, @intFromEnum(BlockType.blue_ice)), reduceMaterial(.blue_ice)); - - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - const metrics = tile.byteMetrics(32); - try std.testing.expectEqual(tile.samples.len * COMPACT_LOD_SAMPLE_BYTES, metrics.compact_upload_bytes); - try std.testing.expect(metrics.compact_upload_bytes < metrics.conservative_expanded_top_grid_bytes); - try std.testing.expectEqual(metrics.conservative_expanded_top_grid_bytes - metrics.compact_upload_bytes, metrics.savedBytes()); - try std.testing.expectEqual(@as(usize, 0), (ByteMetrics{ .compact_upload_bytes = 16, .conservative_expanded_top_grid_bytes = 0 }).savedBytes()); -} - -test "CompactLODTile checksum covers header metadata" { - const allocator = std.testing.allocator; - var source = try LODSimplifiedData.init(allocator, .lod4); - defer source.deinit(); - var tile = try CompactLODTile.initFromSimplified(allocator, .lod4, &source); - defer tile.deinit(); - const wire = try tile.serialize(allocator); - defer allocator.free(wire); - wire[7] ^= 0x01; - - try std.testing.expectError(TileError.ChecksumMismatch, CompactLODTile.deserialize(allocator, wire)); -} diff --git a/modules/world-lod/src/lod_types.zig b/modules/world-lod/src/lod_types.zig deleted file mode 100644 index d2e929cb..00000000 --- a/modules/world-lod/src/lod_types.zig +++ /dev/null @@ -1,15 +0,0 @@ -pub const LODLevel = @import("engine-core").LODLevel; - -/// State for LOD chunks/regions. -pub const LODState = enum { - missing, - queued_for_generation, - generating, - generated, - queued_for_mesh, - meshing, - mesh_ready, - uploading, - renderable, - unloading, -}; diff --git a/modules/world-lod/src/lod_upload_queue.zig b/modules/world-lod/src/lod_upload_queue.zig deleted file mode 100644 index f7cca390..00000000 --- a/modules/world-lod/src/lod_upload_queue.zig +++ /dev/null @@ -1,246 +0,0 @@ -//! LOD GPU Bridge - callback interfaces that decouple LOD logic from GPU operations. -//! -//! Extracted from LODManager (Issue #246) to satisfy Single Responsibility Principle. -//! LODManager uses these interfaces instead of holding a direct RHI reference. - -const std = @import("std"); -const log = @import("engine-core").log; -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const LODChunk = lod_chunk.LODChunk; -const LODRegionKey = lod_chunk.LODRegionKey; -const LODRegionKeyContext = lod_chunk.LODRegionKeyContext; -const ILODConfig = lod_chunk.ILODConfig; -const LODStats = @import("lod_stats.zig").LODStats; -const LODProfilingCollector = @import("lod_stats.zig").LODProfilingCollector; -const LODMesh = @import("lod_mesh.zig").LODMesh; -const Vec3 = @import("engine-math").Vec3; -const Mat4 = @import("engine-math").Mat4; -const rhi_types = @import("engine-rhi"); -const RhiError = rhi_types.RhiError; -const LODStagingCost = @import("lod_mesh_resources.zig").LODStagingCost; -const LODWaitIdleReason = @import("lod_stats.zig").LODWaitIdleReason; - -/// Callback interface for GPU data operations (upload, destroy, sync). -/// Created by the caller who owns the concrete RHI, passed to LODManager. -pub const LODGPUBridge = struct { - /// Upload pending vertex data for a mesh to GPU buffers. - on_upload: *const fn (mesh: *LODMesh, ctx: *anyopaque) RhiError!void, - /// Destroy GPU resources owned by a mesh. - on_destroy: *const fn (mesh: *LODMesh, ctx: *anyopaque) void, - /// Wait for GPU to finish all pending work (needed before batch deletion). - on_wait_idle: *const fn (ctx: *anyopaque) void, - /// Preflight staging bytes. Non-pooled bridge implementations can omit - /// this and are charged for the mesh payload alone. - on_upload_cost: ?*const fn (mesh: *LODMesh, ctx: *anyopaque) LODStagingCost = null, - /// Opaque context pointer (typically the concrete RHI instance). - ctx: *anyopaque, - /// Optional capability probe for compact storage-buffer vertex pulling. - on_supports_compact: ?*const fn (ctx: *anyopaque) bool = null, - /// Stronger production capability: compact streams may be GPU-culled with - /// immutable terrain/water descriptor snapshots. - on_supports_compact_gpu_culling: ?*const fn (ctx: *anyopaque) bool = null, - - fn hasInvalidCtx(self: LODGPUBridge) bool { - const ctx_addr = @intFromPtr(self.ctx); - return ctx_addr == 0 or ctx_addr == 0xaaaa_aaaa_aaaa_aaaa; - } - - /// Validate that ctx is not undefined/null. - fn assertValidCtx(self: LODGPUBridge) void { - std.debug.assert(!self.hasInvalidCtx()); - } - - pub fn upload(self: LODGPUBridge, mesh: *LODMesh) RhiError!void { - if (self.hasInvalidCtx()) return error.InvalidState; - self.assertValidCtx(); - return self.on_upload(mesh, self.ctx); - } - - pub fn destroy(self: LODGPUBridge, mesh: *LODMesh) void { - if (self.hasInvalidCtx()) { - log.log.err("LODGPUBridge.destroy called with invalid context pointer", .{}); - return; - } - self.assertValidCtx(); - self.on_destroy(mesh, self.ctx); - mesh.clearPendingVertices(); - } - - pub fn waitIdle(self: LODGPUBridge) void { - if (self.hasInvalidCtx()) { - log.log.err("LODGPUBridge.waitIdle called with invalid context pointer", .{}); - return; - } - self.assertValidCtx(); - self.on_wait_idle(self.ctx); - } - - /// Attribute an unavoidable device-wide wait to its caller. Streaming - /// paths should never need this; renderer shutdown is recorded separately. - pub fn waitIdleTracked(self: LODGPUBridge, profiling: *LODProfilingCollector, reason: LODWaitIdleReason) void { - if (self.hasInvalidCtx()) return; - const timer = profiling.begin(); - self.waitIdle(); - profiling.recordWaitIdle(reason, timer); - } - - pub fn uploadCost(self: LODGPUBridge, mesh: *LODMesh) LODStagingCost { - if (self.hasInvalidCtx()) return .{ .payload_bytes = mesh.pendingUploadBytes() }; - const estimate = self.on_upload_cost orelse return .{ .payload_bytes = mesh.pendingUploadBytes() }; - return estimate(mesh, self.ctx); - } - - pub fn supportsCompact(self: LODGPUBridge) bool { - if (self.hasInvalidCtx()) return false; - const probe = self.on_supports_compact orelse return false; - return probe(self.ctx); - } - - pub fn supportsCompactGpuCulling(self: LODGPUBridge) bool { - if (self.hasInvalidCtx()) return false; - const probe = self.on_supports_compact_gpu_culling orelse return false; - return probe(self.ctx); - } -}; - -/// Type aliases used by LODRenderInterface for mesh/region maps. -pub const MeshMap = std.HashMap(LODRegionKey, *LODMesh, LODRegionKeyContext, 80); -pub const RegionMap = std.HashMap(LODRegionKey, *LODChunk, LODRegionKeyContext, 80); - -/// Callback type to check if a regular chunk is loaded and renderable. -pub const ChunkChecker = *const fn (chunk_x: i32, chunk_z: i32, ctx: *anyopaque) bool; - -pub const LODRenderLayer = enum { - terrain, - fluid, -}; - -/// Renderer-owned LOD pool accounting. Pool allocation and slack are reported -/// separately because every pool buffer also has a same-sized CPU shadow. -pub const LODRendererMemoryStats = struct { - pool_gpu_capacity_bytes: usize = 0, - pool_gpu_allocated_bytes: usize = 0, - pool_gpu_slack_bytes: usize = 0, - pool_cpu_shadow_bytes: usize = 0, - compact_pool_capacity_bytes: usize = 0, - compact_pool_allocated_bytes: usize = 0, - compact_pool_free_bytes: usize = 0, - compact_pool_retired_bytes: usize = 0, -}; - -/// Type-erased interface for LOD rendering. -/// Allows LODManager to delegate rendering without knowing the concrete RHI type. -pub const LODRenderInterface = struct { - /// Render LOD meshes using the provided data. - render_fn: *const fn ( - self_ptr: *anyopaque, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void, - /// Frame-aware render entry point. Optional to preserve small test and - /// alternate renderer implementations; production renderers use it to - /// share a value-only visibility projection across terrain and water. - render_frame_fn: ?*const fn ( - self_ptr: *anyopaque, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - detail_render_radius: i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void = null, - prepare_frame_fn: ?*const fn ( - self_ptr: *anyopaque, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - max_distance_chunks: ?i32, - detail_render_radius: i32, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void = null, - memory_stats_fn: ?*const fn (self_ptr: *anyopaque) LODRendererMemoryStats = null, - /// Destroy renderer resources. - deinit_fn: *const fn (self_ptr: *anyopaque) void, - /// Opaque pointer to the concrete renderer. - ptr: *anyopaque, - - pub fn render( - self: LODRenderInterface, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - self.render_fn(self.ptr, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer, stats, profiling); - } - - pub fn renderFrame( - self: LODRenderInterface, - frame_serial: u64, - meshes: *const [LODLevel.count]MeshMap, - regions: *const [LODLevel.count]RegionMap, - config: ILODConfig, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - max_distance_chunks: ?i32, - detail_render_radius: i32, - layer: LODRenderLayer, - stats: ?*LODStats, - profiling: ?*LODProfilingCollector, - ) void { - if (self.render_frame_fn) |render_frame| { - render_frame(self.ptr, frame_serial, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, detail_render_radius, layer, stats, profiling); - } else { - self.render(meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, layer, stats, profiling); - } - } - - pub fn deinit(self: LODRenderInterface) void { - self.deinit_fn(self.ptr); - } - - pub fn prepareFrame(self: LODRenderInterface, frame_serial: u64, meshes: *const [LODLevel.count]MeshMap, regions: *const [LODLevel.count]RegionMap, config: ILODConfig, view_proj: Mat4, camera_pos: Vec3, chunk_checker: ?ChunkChecker, checker_ctx: ?*anyopaque, max_distance_chunks: ?i32, detail_render_radius: i32, stats: ?*LODStats, profiling: ?*LODProfilingCollector) void { - if (self.prepare_frame_fn) |prepare| prepare(self.ptr, frame_serial, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, max_distance_chunks, detail_render_radius, stats, profiling); - } - - pub fn memoryStats(self: LODRenderInterface) LODRendererMemoryStats { - if (self.memory_stats_fn) |memory_stats| return memory_stats(self.ptr); - return .{}; - } -}; diff --git a/modules/world-lod/src/lod_vegetation.zig b/modules/world-lod/src/lod_vegetation.zig deleted file mode 100644 index cd55e532..00000000 --- a/modules/world-lod/src/lod_vegetation.zig +++ /dev/null @@ -1,11 +0,0 @@ -//! Vegetation and canopy helper surface for LOD mesh generation. - -const geom = @import("lod_geometry.zig"); - -pub const LOD_TREE_COVERAGE_THRESHOLD = geom.LOD_TREE_COVERAGE_THRESHOLD; - -pub const addTreeCanopyColumn = geom.addTreeCanopyColumn; -pub const addTreeColumn = geom.addTreeColumn; -pub const foldedCanopyColumnForLOD = geom.foldedCanopyColumnForLOD; -pub const representativeVegetationForLOD = geom.representativeVegetationForLOD; -pub const shouldRenderLODTree = geom.shouldRenderLODTree; diff --git a/modules/world-lod/src/lod_vertex_pool.zig b/modules/world-lod/src/lod_vertex_pool.zig deleted file mode 100644 index 39e34399..00000000 --- a/modules/world-lod/src/lod_vertex_pool.zig +++ /dev/null @@ -1,849 +0,0 @@ -//! Shared per-level vertex pool for distant LOD meshes. - -const std = @import("std"); -const sync = @import("sync"); -const lod_chunk = @import("lod_chunk.zig"); -const LODLevel = lod_chunk.LODLevel; -const lod_mesh = @import("lod_mesh.zig"); -const LODMesh = lod_mesh.LODMesh; -const LODMeshResources = lod_mesh.LODMeshResources; -const rhi_types = @import("engine-rhi"); -const Vertex = rhi_types.Vertex; -const BufferHandle = rhi_types.BufferHandle; -const RhiError = rhi_types.RhiError; -const log = @import("engine-core").log; -const LODStagingCost = @import("lod_mesh_resources.zig").LODStagingCost; - -const DEFAULT_INITIAL_CAPACITY_BYTES: usize = 8 * 1024 * 1024; -const COMPACTION_FRAGMENTATION_THRESHOLD: f32 = 0.35; - -const FreeBlock = struct { - offset: usize, - size: usize, -}; - -const AllocationRecord = struct { - mesh: *LODMesh, - offset: usize, - size: usize, -}; - -const RetiredRange = struct { - offset: usize, - size: usize, - serial: u64, - frame_slot: usize, -}; - -const MeshDrawState = LODMesh.DrawState; - -/// Owns one large vertex buffer for a single LOD level and sub-allocates mesh ranges. -pub const LODVertexPool = struct { - allocator: std.mem.Allocator, - lod_level: LODLevel, - buffer_handle: BufferHandle = 0, - capacity_bytes: usize = 0, - initial_capacity_bytes: usize = DEFAULT_INITIAL_CAPACITY_BYTES, - free_blocks: std.ArrayListUnmanaged(FreeBlock) = .empty, - allocations: std.ArrayListUnmanaged(AllocationRecord) = .empty, - retired_ranges: std.ArrayListUnmanaged(RetiredRange) = .empty, - current_serial: u64 = 0, - current_frame_slot: usize = 0, - last_completed_serial: ?u64 = null, - shadow: []u8 = undefined, - mutex: sync.Mutex = .{}, - - pub fn init(allocator: std.mem.Allocator, lod_level: LODLevel, initial_capacity_bytes: usize) LODVertexPool { - return .{ - .allocator = allocator, - .lod_level = lod_level, - .initial_capacity_bytes = initial_capacity_bytes, - }; - } - - pub fn deinit(self: *LODVertexPool, resources: LODMeshResources) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.buffer_handle != 0) { - resources.destroyBuffer(self.buffer_handle); - self.buffer_handle = 0; - } - if (self.capacity_bytes != 0) { - self.allocator.free(self.shadow); - } - self.free_blocks.deinit(self.allocator); - self.allocations.deinit(self.allocator); - self.retired_ranges.deinit(self.allocator); - self.capacity_bytes = 0; - } - - pub fn uploadMesh(self: *LODVertexPool, mesh: *LODMesh, resources: LODMeshResources) RhiError!void { - self.mutex.lock(); - defer self.mutex.unlock(); - - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - const pending = mesh.pending_vertices orelse { - if (mesh.bufferHandle() != 0 and mesh.vertexCount() > 0) { - mesh.setDrawStateUnlocked(.{ - .buffer_handle = mesh.bufferHandle(), - .vertex_offset = mesh.vertexOffset(), - .vertex_count = mesh.vertexCount(), - .capacity = @intCast(mesh.byteSize() / @sizeOf(Vertex)), - .pooled = mesh.isPooled(), - .ready = true, - }); - } - return; - }; - - if (pending.len == 0) { - if (self.findRecordIndexUnlocked(mesh)) |idx| self.retireRecordUnlocked(idx, true, mesh); - mesh.markEmptyUploadedUnlocked(); - self.allocator.free(pending); - mesh.pending_vertices = null; - return; - } - - const bytes = std.mem.sliceAsBytes(pending); - - const old_record_index = self.findRecordIndexUnlocked(mesh); - const old_record = if (old_record_index) |idx| self.allocations.items[idx] else null; - var record_index = old_record_index; - var using_new_record = false; - - const large_update = bytes.len > lod_mesh.MAX_STAGING_UPDATE_BYTES; - if (record_index == null or self.allocations.items[record_index.?].size < bytes.len or (large_update and old_record_index != null)) { - const offset = try self.allocateBlockUnlocked(resources, bytes.len, mesh); - errdefer self.releaseOffsetUnlocked(offset, bytes.len) catch {}; - try self.allocations.append(self.allocator, .{ .mesh = mesh, .offset = offset, .size = bytes.len }); - record_index = self.allocations.items.len - 1; - using_new_record = true; - } - - const record = &self.allocations.items[record_index.?]; - lod_mesh.updateBufferChunked(resources, self.buffer_handle, record.offset, bytes) catch |err| { - if (using_new_record) { - self.freeRecordUnlocked(record_index.?, false, mesh); - } - if (old_record) |old| { - applyMeshDrawState(mesh, .{ - .buffer_handle = self.buffer_handle, - .vertex_offset = old.offset, - .vertex_count = mesh.vertexCount(), - .capacity = @intCast(old.size / @sizeOf(Vertex)), - .pooled = true, - .ready = true, - }); - } else { - clearMeshDrawState(mesh); - } - return err; - }; - @memcpy(self.shadow[record.offset .. record.offset + bytes.len], bytes); - - const new_offset = record.offset; - const new_size = record.size; - if (using_new_record) { - if (old_record_index) |idx| { - self.retireRecordUnlocked(idx, false, mesh); - } - } - - applyMeshDrawState(mesh, .{ - .buffer_handle = self.buffer_handle, - .vertex_offset = new_offset, - .vertex_count = @intCast(pending.len), - .capacity = @intCast(new_size / @sizeOf(Vertex)), - .pooled = true, - .ready = true, - }); - self.allocator.free(pending); - mesh.pending_vertices = null; - } - - pub fn destroyMesh(self: *LODVertexPool, mesh: *LODMesh) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - self.freeMeshUnlocked(mesh); - - if (mesh.pending_vertices) |pending| { - self.allocator.free(pending); - mesh.pending_vertices = null; - } - mesh.clearDrawStateUnlocked(); - } - - /// Removes a mesh from future submissions while keeping its byte range - /// unavailable until the frame slot that last referenced it completes. - pub fn destroyMeshDeferred(self: *LODVertexPool, mesh: *LODMesh, serial: u64, frame_slot: usize) void { - self.mutex.lock(); - defer self.mutex.unlock(); - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - self.current_serial = serial; - self.current_frame_slot = frame_slot; - if (self.findRecordIndexUnlocked(mesh)) |idx| self.retireRecordUnlocked(idx, true, mesh); - if (mesh.pending_vertices) |pending| { - self.allocator.free(pending); - mesh.pending_vertices = null; - } - mesh.clearDrawStateUnlocked(); - } - - pub fn collectRetired(self: *LODVertexPool, completed_serial: u64, completed_frame_slot: usize) void { - self.mutex.lock(); - defer self.mutex.unlock(); - if (self.last_completed_serial) |last| if (completed_serial <= last) return; - self.last_completed_serial = completed_serial; - self.current_serial = completed_serial; - self.current_frame_slot = completed_frame_slot; - - var i: usize = 0; - while (i < self.retired_ranges.items.len) { - const retired = self.retired_ranges.items[i]; - if (retired.frame_slot == completed_frame_slot and completed_serial > retired.serial) { - self.releaseOffsetUnlocked(retired.offset, retired.size) catch { - i += 1; - continue; - }; - _ = self.retired_ranges.swapRemove(i); - } else i += 1; - } - } - - pub fn allocatedBytes(self: *LODVertexPool) usize { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.allocatedBytesUnlocked(); - } - - pub fn gpuMemoryBytes(self: *LODVertexPool) usize { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.capacity_bytes; - } - - /// Returns every staging byte `uploadMesh` will need for its current - /// payload. Replacing a pool buffer republishes its CPU shadow, so callers - /// must reserve the migration writes before beginning the upload. - pub fn uploadCost(self: *LODVertexPool, mesh: *LODMesh) LODStagingCost { - self.mutex.lock(); - defer self.mutex.unlock(); - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - - const pending = mesh.pending_vertices orelse return .{}; - const payload_bytes = std.mem.sliceAsBytes(pending).len; - if (payload_bytes == 0) return .{}; - - const old_index = self.findRecordIndexUnlocked(mesh); - const old_size = if (old_index) |index| self.allocations.items[index].size else 0; - const needs_new_range = old_index == null or old_size < payload_bytes or - (payload_bytes > lod_mesh.MAX_STAGING_UPDATE_BYTES and old_index != null); - if (!needs_new_range or self.capacity_bytes == 0 or self.findBestFitUnlocked(payload_bytes) != null) { - return .{ .payload_bytes = payload_bytes }; - } - - // Fragmented allocation compacts, while insufficient total free space - // grows. Both paths create a replacement and upload every live record. - return .{ - .payload_bytes = payload_bytes, - .migration_bytes = self.allocatedBytesUnlocked(), - }; - } - - pub fn freeBytes(self: *LODVertexPool) usize { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.freeBytesUnlocked(); - } - - pub fn fragmentationRatio(self: *LODVertexPool) f32 { - self.mutex.lock(); - defer self.mutex.unlock(); - return self.fragmentationRatioUnlocked(); - } - - pub fn compact(self: *LODVertexPool, resources: LODMeshResources) RhiError!void { - self.mutex.lock(); - defer self.mutex.unlock(); - try self.compactUnlocked(resources, null); - } - - fn allocateBlockUnlocked(self: *LODVertexPool, resources: LODMeshResources, size: usize, locked_mesh: ?*LODMesh) RhiError!usize { - if (size == 0) return error.InvalidState; - try self.ensureCapacityUnlocked(resources, size, locked_mesh); - - if (self.findBestFitUnlocked(size)) |idx| return self.takeBlockUnlocked(idx, size); - - if (self.freeBytesUnlocked() >= size) { - try self.compactUnlocked(resources, locked_mesh); - if (self.findBestFitUnlocked(size)) |idx| return self.takeBlockUnlocked(idx, size); - } - - try self.ensureCapacityUnlocked(resources, self.capacity_bytes + size, locked_mesh); - if (self.findBestFitUnlocked(size)) |idx| return self.takeBlockUnlocked(idx, size); - return error.OutOfMemory; - } - - fn ensureCapacityUnlocked(self: *LODVertexPool, resources: LODMeshResources, min_capacity: usize, locked_mesh: ?*LODMesh) RhiError!void { - if (self.capacity_bytes >= min_capacity) return; - - var new_capacity = @max(self.initial_capacity_bytes, @as(usize, 1024)); - while (new_capacity < min_capacity) { - new_capacity *= 2; - } - - const new_shadow = self.allocator.alloc(u8, new_capacity) catch return error.OutOfMemory; - errdefer self.allocator.free(new_shadow); - if (self.capacity_bytes > 0) { - @memcpy(new_shadow[0..self.capacity_bytes], self.shadow); - } - @memset(new_shadow[self.capacity_bytes..new_capacity], 0); - - const new_handle = try resources.createBuffer(new_capacity, .vertex); - errdefer resources.destroyBuffer(new_handle); - if (self.capacity_bytes > 0) { - for (self.allocations.items) |record| { - try lod_mesh.updateBufferChunked(resources, new_handle, record.offset, new_shadow[record.offset .. record.offset + record.size]); - } - } - - const old_handle = self.buffer_handle; - const old_capacity = self.capacity_bytes; - const old_shadow = if (old_capacity != 0) self.shadow else null; - - try self.releaseOffsetUnlocked(old_capacity, new_capacity - old_capacity); - - self.buffer_handle = new_handle; - self.shadow = new_shadow; - self.capacity_bytes = new_capacity; - for (self.allocations.items) |record| { - setMeshBufferHandle(record.mesh, new_handle, locked_mesh); - } - - if (old_handle != 0) { - resources.destroyBuffer(old_handle); - } - if (old_shadow) |shadow| self.allocator.free(shadow); - } - - fn findBestFitUnlocked(self: *const LODVertexPool, size: usize) ?usize { - var best_idx: ?usize = null; - var best_size: usize = std.math.maxInt(usize); - for (self.free_blocks.items, 0..) |block, idx| { - if (block.size >= size and block.size < best_size) { - best_idx = idx; - best_size = block.size; - if (block.size == size) break; - } - } - return best_idx; - } - - fn takeBlockUnlocked(self: *LODVertexPool, idx: usize, size: usize) usize { - const block = self.free_blocks.items[idx]; - const offset = block.offset; - if (block.size == size) { - _ = self.free_blocks.orderedRemove(idx); - } else { - self.free_blocks.items[idx].offset += size; - self.free_blocks.items[idx].size -= size; - } - return offset; - } - - fn freeMeshUnlocked(self: *LODVertexPool, mesh: *LODMesh) void { - if (self.findRecordIndexUnlocked(mesh)) |idx| { - self.freeRecordUnlocked(idx, true, mesh); - } - } - - fn freeRecordUnlocked(self: *LODVertexPool, idx: usize, reset_mesh: bool, locked_mesh: ?*LODMesh) void { - const record = self.allocations.items[idx]; - self.releaseOffsetUnlocked(record.offset, record.size) catch |err| { - log.log.warn("LOD vertex pool failed to release {} bytes at {}: {}", .{ record.size, record.offset, err }); - }; - if (reset_mesh) { - setMeshDrawState(record.mesh, .empty, locked_mesh); - } - _ = self.allocations.orderedRemove(idx); - } - - fn retireRecordUnlocked(self: *LODVertexPool, idx: usize, reset_mesh: bool, locked_mesh: ?*LODMesh) void { - const record = self.allocations.items[idx]; - self.retired_ranges.append(self.allocator, .{ - .offset = record.offset, - .size = record.size, - .serial = self.current_serial, - .frame_slot = self.current_frame_slot, - }) catch { - // Bookkeeping failure intentionally leaks pool capacity rather than - // risking an in-flight overwrite. - }; - if (reset_mesh) setMeshDrawState(record.mesh, .empty, locked_mesh); - _ = self.allocations.orderedRemove(idx); - } - - fn releaseOffsetUnlocked(self: *LODVertexPool, offset: usize, size: usize) !void { - if (size == 0) return; - - var insert_idx = self.free_blocks.items.len; - for (self.free_blocks.items, 0..) |block, idx| { - if (block.offset > offset) { - insert_idx = idx; - break; - } - } - try self.free_blocks.insert(self.allocator, insert_idx, .{ .offset = offset, .size = size }); - self.coalesceAroundUnlocked(insert_idx); - } - - fn coalesceAroundUnlocked(self: *LODVertexPool, start_idx: usize) void { - var idx = start_idx; - if (idx > 0 and self.free_blocks.items[idx - 1].offset + self.free_blocks.items[idx - 1].size == self.free_blocks.items[idx].offset) { - self.free_blocks.items[idx - 1].size += self.free_blocks.items[idx].size; - _ = self.free_blocks.orderedRemove(idx); - idx -= 1; - } - while (idx + 1 < self.free_blocks.items.len and self.free_blocks.items[idx].offset + self.free_blocks.items[idx].size == self.free_blocks.items[idx + 1].offset) { - self.free_blocks.items[idx].size += self.free_blocks.items[idx + 1].size; - _ = self.free_blocks.orderedRemove(idx + 1); - } - } - - fn compactUnlocked(self: *LODVertexPool, resources: LODMeshResources, locked_mesh: ?*LODMesh) RhiError!void { - if (self.allocations.items.len == 0) { - if (self.retired_ranges.items.len != 0) return; - self.free_blocks.clearRetainingCapacity(); - try self.releaseOffsetUnlocked(0, self.capacity_bytes); - return; - } - - std.mem.sort(AllocationRecord, self.allocations.items, {}, struct { - fn lt(_: void, a: AllocationRecord, b: AllocationRecord) bool { - return a.offset < b.offset; - } - }.lt); - - self.free_blocks.ensureTotalCapacity(self.allocator, 1) catch return error.OutOfMemory; - - // Never relocate ranges inside the live backing buffer. Draws already - // submitted for earlier frames may still read the old offsets while a - // transfer writes the compacted data. Build a replacement buffer, - // publish all new locations only after every upload succeeds, and let - // the RHI retire the old handle through its frame-fence deletion path. - const new_shadow = self.allocator.alloc(u8, self.capacity_bytes) catch return error.OutOfMemory; - errdefer self.allocator.free(new_shadow); - @memset(new_shadow, 0); - - const new_handle = try resources.createBuffer(self.capacity_bytes, .vertex); - errdefer resources.destroyBuffer(new_handle); - - var cursor: usize = 0; - for (self.allocations.items) |record| { - const source = self.shadow[record.offset .. record.offset + record.size]; - @memcpy(new_shadow[cursor .. cursor + record.size], source); - try lod_mesh.updateBufferChunked(resources, new_handle, cursor, source); - cursor += record.size; - } - - const old_handle = self.buffer_handle; - const old_shadow = self.shadow; - self.buffer_handle = new_handle; - self.shadow = new_shadow; - - cursor = 0; - for (self.allocations.items) |*record| { - record.offset = cursor; - setMeshPoolLocation(record.mesh, new_handle, cursor, locked_mesh); - cursor += record.size; - } - - // Retired offsets belonged to the old backing buffer. The replacement - // is freshly packed and the old handle is fence-retired by the RHI, so - // only the new packed tail is free here. - self.retired_ranges.clearRetainingCapacity(); - self.free_blocks.clearRetainingCapacity(); - if (cursor < self.capacity_bytes) { - try self.releaseOffsetUnlocked(cursor, self.capacity_bytes - cursor); - } - - resources.destroyBuffer(old_handle); - self.allocator.free(old_shadow); - } - - fn findRecordIndexUnlocked(self: *const LODVertexPool, mesh: *const LODMesh) ?usize { - for (self.allocations.items, 0..) |record, idx| { - if (record.mesh == mesh) return idx; - } - return null; - } - - fn allocatedBytesUnlocked(self: *const LODVertexPool) usize { - var total: usize = 0; - for (self.allocations.items) |record| total += record.size; - for (self.retired_ranges.items) |record| total += record.size; - return total; - } - - fn freeBytesUnlocked(self: *const LODVertexPool) usize { - var total: usize = 0; - for (self.free_blocks.items) |block| total += block.size; - return total; - } - - fn fragmentationRatioUnlocked(self: *const LODVertexPool) f32 { - const total_free = self.freeBytesUnlocked(); - if (total_free == 0) return 0.0; - var largest_free: usize = 0; - for (self.free_blocks.items) |block| largest_free = @max(largest_free, block.size); - return 1.0 - (@as(f32, @floatFromInt(largest_free)) / @as(f32, @floatFromInt(total_free))); - } -}; - -fn clearMeshDrawState(mesh: *LODMesh) void { - mesh.clearDrawStateUnlocked(); -} - -fn setMeshBufferHandle(mesh: *LODMesh, buffer_handle: BufferHandle, locked_mesh: ?*LODMesh) void { - if (locked_mesh) |locked| { - if (locked == mesh) { - mesh.setBufferHandleUnlocked(buffer_handle); - return; - } - } - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - mesh.setBufferHandleUnlocked(buffer_handle); -} - -fn setMeshPoolLocation(mesh: *LODMesh, buffer_handle: BufferHandle, vertex_offset: usize, locked_mesh: ?*LODMesh) void { - if (locked_mesh) |locked| { - if (locked == mesh) { - mesh.setPoolLocationUnlocked(buffer_handle, vertex_offset); - return; - } - } - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - mesh.setPoolLocationUnlocked(buffer_handle, vertex_offset); -} - -fn setMeshDrawState(mesh: *LODMesh, state: MeshDrawState, locked_mesh: ?*LODMesh) void { - if (locked_mesh) |locked| { - if (locked == mesh) { - mesh.setDrawStateUnlocked(state); - return; - } - } - mesh.mutex.lock(); - defer mesh.mutex.unlock(); - mesh.setDrawStateUnlocked(state); -} - -fn applyMeshDrawState(mesh: *LODMesh, state: MeshDrawState) void { - mesh.setDrawStateUnlocked(state); -} - -fn shouldCompactAfterEviction(pool: *LODVertexPool) bool { - return pool.fragmentationRatio() > COMPACTION_FRAGMENTATION_THRESHOLD; -} - -const TestResources = struct { - next_handle: BufferHandle = 1, - created: u32 = 0, - destroyed: u32 = 0, - uploaded: u32 = 0, - updated: u32 = 0, - total_updated_bytes: usize = 0, - max_update_bytes: usize = 0, - wait_idle: u32 = 0, - fail_updates: bool = false, - - fn resources(self: *TestResources) LODMeshResources { - return .{ .ptr = self, .vtable = &vtable }; - } - - fn createBuffer(ptr: *anyopaque, _: usize, _: rhi_types.BufferUsage) RhiError!BufferHandle { - const self: *TestResources = @ptrCast(@alignCast(ptr)); - const handle = self.next_handle; - self.next_handle += 1; - self.created += 1; - return handle; - } - - fn uploadBuffer(ptr: *anyopaque, _: BufferHandle, _: []const u8) RhiError!void { - const self: *TestResources = @ptrCast(@alignCast(ptr)); - self.uploaded += 1; - } - - fn updateBuffer(ptr: *anyopaque, _: BufferHandle, _: usize, data: []const u8) RhiError!void { - const self: *TestResources = @ptrCast(@alignCast(ptr)); - if (self.fail_updates) return error.GpuLost; - self.updated += 1; - self.total_updated_bytes += data.len; - self.max_update_bytes = @max(self.max_update_bytes, data.len); - } - - fn destroyBuffer(ptr: *anyopaque, _: BufferHandle) void { - const self: *TestResources = @ptrCast(@alignCast(ptr)); - self.destroyed += 1; - } - - fn waitIdle(ptr: *anyopaque) void { - const self: *TestResources = @ptrCast(@alignCast(ptr)); - self.wait_idle += 1; - } - - const vtable = LODMeshResources.VTable{ - .createBuffer = createBuffer, - .uploadBuffer = uploadBuffer, - .updateBuffer = updateBuffer, - .destroyBuffer = destroyBuffer, - .waitIdle = waitIdle, - }; -}; - -fn setPending(mesh: *LODMesh, allocator: std.mem.Allocator, count: usize) !void { - const vertices = try allocator.alloc(Vertex, count); - @memset(std.mem.sliceAsBytes(vertices), 0); - mesh.pending_vertices = vertices; -} - -test "LODVertexPool reuses freed ranges" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var first = LODMesh.init(allocator, .lod1); - var second = LODMesh.init(allocator, .lod1); - var third = LODMesh.init(allocator, .lod1); - try setPending(&first, allocator, 4); - try setPending(&second, allocator, 4); - try pool.uploadMesh(&first, resources.resources()); - try pool.uploadMesh(&second, resources.resources()); - - const reused_offset = first.vertex_offset; - pool.destroyMesh(&first); - try setPending(&third, allocator, 4); - try pool.uploadMesh(&third, resources.resources()); - - try std.testing.expectEqual(reused_offset, third.vertex_offset); - pool.destroyMesh(&second); - pool.destroyMesh(&third); -} - -test "LODVertexPool grows and preserves pooled handles" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 64); - defer pool.deinit(resources.resources()); - - var first = LODMesh.init(allocator, .lod1); - var second = LODMesh.init(allocator, .lod1); - try setPending(&first, allocator, 4); - try pool.uploadMesh(&first, resources.resources()); - const old_handle = first.buffer_handle; - - try setPending(&second, allocator, 64); - try pool.uploadMesh(&second, resources.resources()); - - try std.testing.expect(pool.gpuMemoryBytes() > 64); - try std.testing.expect(first.buffer_handle != old_handle); - try std.testing.expectEqual(first.buffer_handle, second.buffer_handle); - try std.testing.expectEqual(@as(u32, 0), resources.uploaded); - try std.testing.expect(resources.updated >= 1); - try std.testing.expectEqual(@as(u32, 0), resources.wait_idle); - pool.destroyMesh(&first); - pool.destroyMesh(&second); -} - -test "LODVertexPool upload cost includes replacement migration staging" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 64); - defer pool.deinit(resources.resources()); - - var first = LODMesh.init(allocator, .lod1); - var second = LODMesh.init(allocator, .lod1); - try setPending(&first, allocator, 1); - const initial_cost = pool.uploadCost(&first); - try std.testing.expectEqual(@sizeOf(Vertex), initial_cost.payload_bytes); - try std.testing.expectEqual(@as(usize, 0), initial_cost.migration_bytes); - try pool.uploadMesh(&first, resources.resources()); - - try setPending(&second, allocator, 64); - const growth_cost = pool.uploadCost(&second); - try std.testing.expectEqual(64 * @sizeOf(Vertex), growth_cost.payload_bytes); - try std.testing.expectEqual(@sizeOf(Vertex), growth_cost.migration_bytes); - try std.testing.expectEqual(65 * @sizeOf(Vertex), growth_cost.total()); - pool.destroyMesh(&first); - pool.destroyMesh(&second); -} - -test "LODVertexPool splits oversized staging updates" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, lod_mesh.MAX_STAGING_UPDATE_BYTES * 2); - defer pool.deinit(resources.resources()); - - var mesh = LODMesh.init(allocator, .lod1); - const vertex_count = lod_mesh.MAX_STAGING_UPDATE_BYTES / @sizeOf(Vertex) + 1; - try setPending(&mesh, allocator, vertex_count); - try pool.uploadMesh(&mesh, resources.resources()); - - try std.testing.expect(resources.updated >= 2); - try std.testing.expect(resources.max_update_bytes <= lod_mesh.MAX_STAGING_UPDATE_BYTES); - try std.testing.expectEqual(vertex_count * @sizeOf(Vertex), resources.total_updated_bytes); - pool.destroyMesh(&mesh); -} - -test "LODVertexPool compacts fragmented ranges" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var a = LODMesh.init(allocator, .lod1); - var b = LODMesh.init(allocator, .lod1); - var c = LODMesh.init(allocator, .lod1); - try setPending(&a, allocator, 4); - try setPending(&b, allocator, 4); - try setPending(&c, allocator, 4); - try pool.uploadMesh(&a, resources.resources()); - try pool.uploadMesh(&b, resources.resources()); - try pool.uploadMesh(&c, resources.resources()); - - pool.destroyMesh(&b); - const old_c_offset = c.vertex_offset; - const old_handle = c.buffer_handle; - try pool.compact(resources.resources()); - - try std.testing.expect(c.vertex_offset < old_c_offset); - try std.testing.expect(c.buffer_handle != old_handle); - try std.testing.expectEqual(c.buffer_handle, a.buffer_handle); - try std.testing.expectEqual(@as(u32, 2), resources.created); - try std.testing.expectEqual(@as(u32, 1), resources.destroyed); - try std.testing.expectEqual(pool.capacity_bytes - pool.allocatedBytes(), pool.freeBytes()); - pool.destroyMesh(&a); - pool.destroyMesh(&c); -} - -test "LODVertexPool compaction failure preserves live allocation locations" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var a = LODMesh.init(allocator, .lod1); - var b = LODMesh.init(allocator, .lod1); - var c = LODMesh.init(allocator, .lod1); - try setPending(&a, allocator, 4); - try setPending(&b, allocator, 4); - try setPending(&c, allocator, 4); - try pool.uploadMesh(&a, resources.resources()); - try pool.uploadMesh(&b, resources.resources()); - try pool.uploadMesh(&c, resources.resources()); - - pool.destroyMesh(&b); - const old_a_handle = a.buffer_handle; - const old_a_offset = a.vertex_offset; - const old_c_handle = c.buffer_handle; - const old_c_offset = c.vertex_offset; - resources.fail_updates = true; - - try std.testing.expectError(error.GpuLost, pool.compact(resources.resources())); - try std.testing.expectEqual(old_a_handle, a.buffer_handle); - try std.testing.expectEqual(old_a_offset, a.vertex_offset); - try std.testing.expectEqual(old_c_handle, c.buffer_handle); - try std.testing.expectEqual(old_c_offset, c.vertex_offset); - try std.testing.expectEqual(@as(u32, 2), resources.created); - try std.testing.expectEqual(@as(u32, 1), resources.destroyed); - - resources.fail_updates = false; - pool.destroyMesh(&a); - pool.destroyMesh(&c); -} - -test "LODVertexPool upload failure keeps pending vertices retryable" { - const allocator = std.testing.allocator; - var resources = TestResources{ .fail_updates = true }; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var mesh = LODMesh.init(allocator, .lod1); - defer if (mesh.pending_vertices) |pending| allocator.free(pending); - try setPending(&mesh, allocator, 4); - try std.testing.expectError(error.GpuLost, pool.uploadMesh(&mesh, resources.resources())); - - try std.testing.expect(mesh.pending_vertices != null); - try std.testing.expect(!mesh.ready); - try std.testing.expectEqual(@as(u32, 0), mesh.vertex_count); -} - -test "LODVertexPool upload failure preserves existing renderable allocation" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var mesh = LODMesh.init(allocator, .lod1); - defer if (mesh.pending_vertices) |pending| allocator.free(pending); - try setPending(&mesh, allocator, 4); - try pool.uploadMesh(&mesh, resources.resources()); - - const old_handle = mesh.buffer_handle; - const old_offset = mesh.vertex_offset; - const old_count = mesh.vertex_count; - resources.fail_updates = true; - try setPending(&mesh, allocator, 4); - try std.testing.expectError(error.GpuLost, pool.uploadMesh(&mesh, resources.resources())); - - try std.testing.expect(mesh.pending_vertices != null); - try std.testing.expect(mesh.ready); - try std.testing.expect(mesh.pooled); - try std.testing.expectEqual(old_handle, mesh.buffer_handle); - try std.testing.expectEqual(old_offset, mesh.vertex_offset); - try std.testing.expectEqual(old_count, mesh.vertex_count); - try std.testing.expectEqual(@as(usize, old_count) * @sizeOf(Vertex), pool.allocatedBytes()); -} - -test "LODVertexPool exposes compaction threshold helper" { - _ = shouldCompactAfterEviction; -} - -test "LODVertexPool defers range reuse until its frame slot completes" { - const allocator = std.testing.allocator; - var resources = TestResources{}; - var pool = LODVertexPool.init(allocator, .lod1, 1024); - defer pool.deinit(resources.resources()); - - var a = LODMesh.init(allocator, .lod1); - var b = LODMesh.init(allocator, .lod1); - try setPending(&a, allocator, 4); - try setPending(&b, allocator, 4); - try pool.uploadMesh(&a, resources.resources()); - try pool.uploadMesh(&b, resources.resources()); - const free_before = pool.freeBytes(); - - pool.destroyMeshDeferred(&a, 10, 0); - try std.testing.expectEqual(@as(usize, 1), pool.retired_ranges.items.len); - try std.testing.expectEqual(free_before, pool.freeBytes()); - - pool.collectRetired(10, 1); - try std.testing.expectEqual(@as(usize, 1), pool.retired_ranges.items.len); - pool.collectRetired(11, 0); - try std.testing.expectEqual(@as(usize, 0), pool.retired_ranges.items.len); - try std.testing.expect(pool.freeBytes() > free_before); - pool.destroyMesh(&b); -} diff --git a/modules/world-lod/src/root.zig b/modules/world-lod/src/root.zig deleted file mode 100644 index fe8a8e63..00000000 --- a/modules/world-lod/src/root.zig +++ /dev/null @@ -1,54 +0,0 @@ -const builtin = @import("builtin"); - -pub const biome_color_provider = @import("biome_color_provider.zig"); -pub const lod_chunk = @import("lod_chunk.zig"); -pub const lod_cache = @import("lod_cache.zig"); -pub const lod_geometry = @import("lod_geometry.zig"); -pub const lod_materials = @import("lod_materials.zig"); -pub const lod_mesh = @import("lod_mesh.zig"); -pub const lod_mesh_resources = @import("lod_mesh_resources.zig"); -pub const lod_mesh_tests = if (builtin.is_test) @import("lod_mesh_tests.zig") else struct {}; -pub const lod_generator = @import("lod_generator.zig"); -pub const lod_ingest = @import("lod_ingest.zig"); -pub const lod_manager = @import("lod_manager.zig"); -pub const lod_manager_tests = if (builtin.is_test) @import("lod_manager_tests.zig") else struct {}; -pub const lod_manager_internal_tests = if (builtin.is_test) @import("lod_manager_internal_tests.zig") else struct {}; -pub const lod_manager_benchmark_fixture_ops = @import("lod_manager_benchmark_fixture_ops.zig"); -pub const lod_renderer = @import("lod_renderer.zig"); -pub const lod_scheduler = @import("lod_scheduler.zig"); -pub const lod_seam = @import("lod_seam.zig"); -pub const lod_streaming_coordinator = @import("lod_streaming_coordinator.zig"); -pub const lod_types = @import("lod_types.zig"); -pub const lod_upload_queue = @import("lod_upload_queue.zig"); -pub const lod_vegetation = @import("lod_vegetation.zig"); -pub const lod_vertex_pool = @import("lod_vertex_pool.zig"); -pub const lod_stats = @import("lod_stats.zig"); -pub const lod_store = @import("lod_store.zig"); -pub const lod_tile = @import("lod_tile.zig"); -pub const lod_compact_pool = @import("lod_compact_pool.zig"); -pub const world_lod = @import("world_lod.zig"); - -pub const LODChunk = lod_chunk.LODChunk; -pub const LODLevel = lod_types.LODLevel; -pub const LODMesh = lod_mesh.LODMesh; -pub const LODMeshBuilder = lod_mesh.LODMeshBuilder; -pub const LODMeshResources = lod_mesh_resources.LODMeshResources; -pub const LODMeshRenderContext = lod_mesh_resources.LODMeshRenderContext; -pub const LODRenderer = lod_renderer.LODRenderer; -pub const LODGPUBridge = lod_upload_queue.LODGPUBridge; -pub const LODGenerator = lod_generator.LODGenerator; -pub const LODManager = lod_manager.LODManager; -pub const ChunkResolver = lod_manager.ChunkResolver; -pub const LODRenderInterface = lod_upload_queue.LODRenderInterface; -pub const LODRenderLayer = lod_upload_queue.LODRenderLayer; -pub const LODStreamingCoordinator = lod_streaming_coordinator.LODStreamingCoordinator; -pub const LODState = lod_types.LODState; -pub const LODStats = lod_stats.LODStats; -pub const LODProfilingSnapshot = lod_stats.LODProfilingSnapshot; -pub const CompactLODTile = lod_tile.CompactLODTile; -pub const CompactLODSample = lod_tile.CompactLODSample; -pub const CompactLODDecodedSample = lod_tile.DecodedSample; -pub const CompactLODTileByteMetrics = lod_tile.ByteMetrics; -pub const CompactLODTileEdge = lod_tile.TileEdge; -pub const LODVertexPool = lod_vertex_pool.LODVertexPool; -pub const WorldLOD = world_lod.WorldLOD; diff --git a/modules/world-lod/src/tests.zig b/modules/world-lod/src/tests.zig deleted file mode 100644 index e1483112..00000000 --- a/modules/world-lod/src/tests.zig +++ /dev/null @@ -1,14 +0,0 @@ -//! Dedicated world-lod test root. - -test { - _ = @import("lod_cache.zig"); - _ = @import("lod_manager_internal_tests.zig"); - _ = @import("lod_manager_tests.zig"); - _ = @import("lod_tile.zig"); - _ = @import("lod_compact_pool.zig"); - _ = @import("lod_mesh.zig"); - _ = @import("lod_vertex_pool.zig"); - _ = @import("lod_store.zig"); - _ = @import("lod_streaming_coordinator.zig"); - _ = @import("lod_scheduler.zig"); -} diff --git a/modules/world-lod/src/world_lod.zig b/modules/world-lod/src/world_lod.zig deleted file mode 100644 index 7ad25928..00000000 --- a/modules/world-lod/src/world_lod.zig +++ /dev/null @@ -1,169 +0,0 @@ -//! WorldLOD - Unified LOD subsystem combining LODManager and LODRenderer. -//! -//! This struct encapsulates the entire LOD system lifecycle, simplifying -//! World's LOD handling from two separate fields to a single unified interface. -//! -//! ## Architecture -//! -//! ``` -//! WorldLOD -//! ├── LODRenderer (GPU draw resources, instance buffers) -//! └── LODManager (chunk generation, meshing, state management) -//! └── LODGPUBridge → callbacks to LODRenderer's RHI -//! ``` -//! -//! The renderer owns GPU buffers and provides callback interfaces. -//! The manager orchestrates LOD chunk lifecycle using those interfaces. - -const std = @import("std"); -const lod_chunk = @import("lod_chunk.zig"); -const ILODConfig = lod_chunk.ILODConfig; -const LODLevel = lod_chunk.LODLevel; - -const LODManager = @import("lod_manager.zig").LODManager; -const LODStats = @import("lod_manager.zig").LODStats; -const lod_gpu = @import("lod_upload_queue.zig"); -const ChunkChecker = lod_gpu.ChunkChecker; -const LODRenderLayer = lod_gpu.LODRenderLayer; -const MeshMap = lod_gpu.MeshMap; -const RegionMap = lod_gpu.RegionMap; - -const math = @import("engine-math"); -const Vec3 = math.Vec3; -const Mat4 = math.Mat4; -const LODGenerator = @import("lod_generator.zig").LODGenerator; -const TextureAtlas = @import("engine-assets").TextureAtlas; -const log = @import("engine-core").log; - -pub fn WorldLOD(comptime RHI: type) type { - const LODRenderer = @import("lod_renderer.zig").LODRenderer(RHI); - - return struct { - const Self = @This(); - - allocator: std.mem.Allocator, - manager: *LODManager, - renderer: *LODRenderer, - - pub fn init( - allocator: std.mem.Allocator, - rhi: RHI, - config: ILODConfig, - generator: LODGenerator, - atlas: *const TextureAtlas, - ) !*Self { - const renderer = try LODRenderer.init(allocator, rhi); - errdefer renderer.deinit(); - - const gpu_bridge = renderer.createGPUBridge(); - const render_iface = renderer.toInterface(); - - const manager = try LODManager.init(allocator, config, gpu_bridge, render_iface, generator, atlas); - errdefer manager.deinit(); - - const lod = try allocator.create(Self); - lod.* = .{ - .allocator = allocator, - .manager = manager, - .renderer = renderer, - }; - - const radii = config.getRadii(); - log.log.info("WorldLOD initialized (horizon radius: {} chunks)", .{radii[LODLevel.count - 1]}); - - return lod; - } - - pub fn deinit(self: *Self) void { - self.manager.deinit(); - self.renderer.deinit(); - self.allocator.destroy(self); - } - - pub fn pause(self: *Self) void { - self.manager.pause(); - } - - pub fn unpause(self: *Self) void { - self.manager.unpause(); - } - - pub fn enableCache(self: *Self, save_dir_path: []const u8) !void { - try self.manager.enableCache(save_dir_path); - } - - pub fn update( - self: *Self, - player_pos: Vec3, - player_velocity: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - ) !void { - try self.manager.update(player_pos, player_velocity, chunk_checker, checker_ctx); - } - - /// Benchmark-only forwarding path for the fixed GPU-culling source - /// set. The manager remains the sole owner of regions and meshes. - pub fn installGpuCullingScaleFixture(self: *Self) !void { - try self.manager.installGpuCullingScaleFixture(); - } - - pub fn render( - self: *Self, - view_proj: Mat4, - camera_pos: Vec3, - chunk_checker: ?ChunkChecker, - checker_ctx: ?*anyopaque, - use_frustum: bool, - layer: LODRenderLayer, - ) void { - self.manager.render(view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, null, layer); - } - - pub fn setLOD0Radius(self: *Self, radius: i32) void { - self.manager.mutex.lock(); - defer self.manager.mutex.unlock(); - self.manager.config.setLOD0Radius(radius); - log.log.info("LOD0 radius updated: {}", .{radius}); - } - - pub fn setChunkRenderRadius(self: *Self, radius: i32) void { - self.manager.mutex.lock(); - defer self.manager.mutex.unlock(); - self.manager.config.setChunkRenderRadius(radius); - log.log.info("Chunk render radius updated: {}", .{radius}); - } - - pub fn setRadii(self: *Self, radii: [LODLevel.count]i32) void { - self.manager.mutex.lock(); - defer self.manager.mutex.unlock(); - self.manager.config.setRadii(radii); - log.log.info("LOD radii updated: {any}", .{radii}); - } - - pub fn setActiveLODCount(self: *Self, count: u32) void { - self.manager.mutex.lock(); - defer self.manager.mutex.unlock(); - self.manager.config.setActiveLODCount(count); - log.log.info("Active LOD count updated: {}", .{self.manager.config.getActiveLODCount()}); - } - - pub fn getStats(self: *const Self) LODStats { - return self.manager.getStats(); - } - - pub fn isInRange(self: *const Self, chunk_x: i32, chunk_z: i32) bool { - return self.manager.isInRange(chunk_x, chunk_z); - } - - pub fn getLODForDistance(self: *const Self, chunk_x: i32, chunk_z: i32) LODLevel { - return self.manager.getLODForDistance(chunk_x, chunk_z); - } - - pub fn getRadii(self: *const Self) [LODLevel.count]i32 { - self.manager.mutex.lockShared(); - defer self.manager.mutex.unlockShared(); - return self.manager.config.getRadii(); - } - }; -} diff --git a/modules/world-meshing/src/chunk_mesh.zig b/modules/world-meshing/src/chunk_mesh.zig index 568d2eee..5ae1dd1a 100644 --- a/modules/world-meshing/src/chunk_mesh.zig +++ b/modules/world-meshing/src/chunk_mesh.zig @@ -241,7 +241,7 @@ pub const ChunkMesh = struct { total += @intCast(verts.len); for (verts) |v| { const tid: u16 = @intCast(v.packed_meta & 0xFFFF); - if (tid == 0 and tid != Vertex.LOD_TILE_ID) tile0 += 1; + if (tid == 0) tile0 += 1; } } } diff --git a/modules/world-meshing/src/chunk_storage.zig b/modules/world-meshing/src/chunk_storage.zig index 90145068..18607812 100644 --- a/modules/world-meshing/src/chunk_storage.zig +++ b/modules/world-meshing/src/chunk_storage.zig @@ -264,23 +264,6 @@ pub const ChunkStorage = struct { return false; } - /// Returns whether detailed terrain is authoritative for LOD handoff. - /// A ready empty mesh intentionally replaces coarse terrain with empty - /// space. Existing terrain allocations remain authoritative while a chunk - /// is remeshed, preventing transient states from flashing detail off/on. - pub fn isChunkTerrainReadyForHandoff(cx: i32, cz: i32, ctx: *anyopaque) bool { - const self: *ChunkStorage = @ptrCast(@alignCast(ctx)); - self.chunks_mutex.lockShared(); - defer self.chunks_mutex.unlockShared(); - - if (self.chunks.get(.{ .x = cx, .z = cz })) |data| { - return data.render.mesh.ready or - data.render.mesh.solid_allocation != null or - data.render.mesh.cutout_allocation != null; - } - return false; - } - /// Diagnostic: get chunk state as a string for logging (not for hot path). pub fn getChunkState(cx: i32, cz: i32, ctx: *anyopaque) ?Chunk.State { const self: *ChunkStorage = @ptrCast(@alignCast(ctx)); diff --git a/modules/world-meshing/src/meshing/quadric_simplifier.zig b/modules/world-meshing/src/meshing/quadric_simplifier.zig deleted file mode 100644 index 085d87cd..00000000 --- a/modules/world-meshing/src/meshing/quadric_simplifier.zig +++ /dev/null @@ -1,855 +0,0 @@ -//! Quadric-based mesh simplification for LOD terrain. -//! -//! Implements the Quadric Error Metrics (QEM) algorithm (Garland & Heckbert, 1997) -//! for mesh simplification. Iteratively collapses edges based on quadric error cost -//! to produce simplified meshes that preserve silhouettes and geometric features -//! better than uniform downsampling. -//! -//! Memory ownership: All slices returned in `SimplifiedMesh` are owned by the caller -//! and must be freed with the same allocator passed to `simplify()`. - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -const rhi_types = @import("engine-rhi"); -const Vertex = rhi_types.Vertex; - -pub const SimplifiedMesh = struct { - /// Caller owns these slices and must free them with the same allocator. - vertices: []Vertex, - /// Caller owns these slices and must free them with the same allocator. - indices: []u32, - /// Number of triangles in the input mesh. - original_triangle_count: u32, - /// Number of triangles in the simplified output. - simplified_triangle_count: u32, - /// Maximum quadric error observed during simplification. - error_estimate: f64, - /// Number of candidate collapses rejected by normal-flip checks. - skipped_collapses: u32, -}; - -const QuadricMatrix = struct { - m: [10]f64 = [1]f64{0} ** 10, - - const zero: QuadricMatrix = .{ .m = [1]f64{0} ** 10 }; - - fn fromPlane(nx: f64, ny: f64, nz: f64, d: f64) QuadricMatrix { - return .{ .m = .{ - nx * nx, nx * ny, nx * nz, nx * d, - ny * ny, ny * nz, ny * d, nz * nz, - nz * d, d * d, - } }; - } - - fn add(a: QuadricMatrix, b: QuadricMatrix) QuadricMatrix { - var r: QuadricMatrix = .{}; - for (&r.m, &a.m, &b.m) |*dst, xa, xb| dst.* = xa + xb; - return r; - } - - fn evaluate(q: QuadricMatrix, x: f64, y: f64, z: f64) f64 { - const m = q.m; - const r0 = m[0] * x + m[1] * y + m[2] * z + m[3]; - const r1 = m[1] * x + m[4] * y + m[5] * z + m[6]; - const r2 = m[2] * x + m[5] * y + m[7] * z + m[8]; - const r3 = m[3] * x + m[6] * y + m[8] * z + m[9]; - return x * r0 + y * r1 + z * r2 + r3; - } - - const OptimalResult = struct { - pos: [3]f64, - solvable: bool, - }; - - fn optimalPosition(q: QuadricMatrix, fa: [3]f64, fb: [3]f64) OptimalResult { - const m = q.m; - const a00 = m[0]; - const a01 = m[1]; - const a02 = m[2]; - const a11 = m[4]; - const a12 = m[5]; - const a22 = m[7]; - const b0 = -m[3]; - const b1 = -m[6]; - const b2 = -m[8]; - - const det = a00 * (a11 * a22 - a12 * a12) - - a01 * (a01 * a22 - a12 * a02) + - a02 * (a01 * a12 - a11 * a02); - - if (@abs(det) < DETERMINANT_EPSILON) { - return .{ - .pos = .{ - (fa[0] + fb[0]) * 0.5, - (fa[1] + fb[1]) * 0.5, - (fa[2] + fb[2]) * 0.5, - }, - .solvable = false, - }; - } - - const inv = 1.0 / det; - const dx = (b0 * (a11 * a22 - a12 * a12) - - a01 * (b1 * a22 - a12 * b2) + - a02 * (b1 * a12 - a11 * b2)) * inv; - const dy = (a00 * (b1 * a22 - a12 * b2) - - b0 * (a01 * a22 - a12 * a02) + - a02 * (a01 * b2 - b1 * a02)) * inv; - const dz = (a00 * (a11 * b2 - b1 * a12) - - a01 * (a01 * b2 - b1 * a02) + - b0 * (a01 * a12 - a11 * a02)) * inv; - - return .{ .pos = .{ dx, dy, dz }, .solvable = true }; - } -}; - -/// Epsilon for determinant check in optimal position solver. -/// Chosen for double-precision stability with unit-scale meshes. -/// Safe for mesh vertices in [0.001, 1000.0] range -- voxel chunks use block-unit coordinates. -const DETERMINANT_EPSILON: f64 = 1e-10; - -/// Skip-collapse guard multiplier. The simplifier stops after -/// `num_triangles * MAX_SKIP_MULTIPLIER` rejected collapses. -const MAX_SKIP_MULTIPLIER: u32 = 2; - -fn edgeKey(v1: u32, v2: u32) u64 { - const lo = @min(v1, v2); - const hi = @max(v1, v2); - return (@as(u64, @intCast(lo)) << 32) | @as(u64, @intCast(hi)); -} - -const EdgeEntry = struct { - v1: u32, - v2: u32, - cost: f64, - version: u32, -}; - -fn compareEdges(_: void, a: EdgeEntry, b: EdgeEntry) std.math.Order { - if (a.cost < b.cost) return .lt; - if (a.cost > b.cost) return .gt; - return .eq; -} - -pub const QuadricSimplifier = struct { - pub fn simplify( - allocator: Allocator, - vertices: []const Vertex, - indices: []const u32, - target_triangles: u32, - ) !SimplifiedMesh { - return simplifyWithOptions(allocator, vertices, indices, target_triangles, MAX_SKIP_MULTIPLIER); - } - - pub fn simplifyWithOptions( - allocator: Allocator, - vertices: []const Vertex, - indices: []const u32, - target_triangles: u32, - max_skip_multiplier: u32, - ) !SimplifiedMesh { - if (indices.len % 3 != 0) return error.InvalidIndexCount; - const num_triangles: u32 = @intCast(indices.len / 3); - - if (vertices.len == 0 or num_triangles == 0) { - return .{ - .vertices = &.{}, - .indices = &.{}, - .original_triangle_count = 0, - .simplified_triangle_count = 0, - .error_estimate = 0, - .skipped_collapses = 0, - }; - } - - if (target_triangles >= num_triangles) { - const out_v = try allocator.dupe(Vertex, vertices); - errdefer allocator.free(out_v); - const out_i = try allocator.dupe(u32, indices); - errdefer allocator.free(out_i); - return .{ - .vertices = out_v, - .indices = out_i, - .original_triangle_count = num_triangles, - .simplified_triangle_count = num_triangles, - .error_estimate = 0, - .skipped_collapses = 0, - }; - } - - const n: u32 = @intCast(vertices.len); - - var pos = try allocator.alloc([3]f64, n); - defer allocator.free(pos); - for (pos, vertices) |*p, v| { - p.* = .{ - @floatCast(v.pos[0]), - @floatCast(v.pos[1]), - @floatCast(v.pos[2]), - }; - } - - var active = try allocator.alloc(bool, n); - defer allocator.free(active); - @memset(active, true); - - var quadrics = try allocator.alloc(QuadricMatrix, n); - defer allocator.free(quadrics); - @memset(quadrics, QuadricMatrix.zero); - - var tris = try allocator.alloc([3]u32, num_triangles); - defer allocator.free(tris); - for (tris, 0..) |*t, i| { - t.* = .{ indices[i * 3], indices[i * 3 + 1], indices[i * 3 + 2] }; - } - - var tri_active = try allocator.alloc(bool, num_triangles); - defer allocator.free(tri_active); - @memset(tri_active, true); - - computeQuadrics(quadrics, pos, tris, tri_active); - - var edge_versions = std.AutoHashMap(u64, u32).init(allocator); - defer edge_versions.deinit(); - - var heap = std.PriorityQueue(EdgeEntry, void, compareEdges).initContext({}); - defer heap.deinit(allocator); - - { - var edge_set = std.AutoHashMap(u64, void).init(allocator); - defer edge_set.deinit(); - - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - const pairs = [3][2]u32{ - .{ tri[0], tri[1] }, - .{ tri[1], tri[2] }, - .{ tri[2], tri[0] }, - }; - for (&pairs) |pair| { - if (pair[0] == pair[1]) continue; - const key = edgeKey(pair[0], pair[1]); - if (edge_set.contains(key)) continue; - try edge_set.put(key, {}); - - const combined = QuadricMatrix.add(quadrics[pair[0]], quadrics[pair[1]]); - const result = combined.optimalPosition(pos[pair[0]], pos[pair[1]]); - const cost = @max(QuadricMatrix.evaluate(combined, result.pos[0], result.pos[1], result.pos[2]), 0); - - try edge_versions.put(key, 0); - try heap.push(allocator, .{ - .v1 = pair[0], - .v2 = pair[1], - .cost = cost, - .version = 0, - }); - } - } - } - - var neighbor_buf = try allocator.alloc(u32, n); - defer allocator.free(neighbor_buf); - - var current_tri_count = num_triangles; - var max_error: f64 = 0; - var skipped_collapses: u32 = 0; - - while (current_tri_count > target_triangles) { - if (skipped_collapses > num_triangles * max_skip_multiplier) break; - - var entry: ?EdgeEntry = null; - while (heap.pop()) |e| { - if (!active[e.v1] or !active[e.v2]) continue; - if (e.v1 == e.v2) continue; - const key = edgeKey(e.v1, e.v2); - const ver = edge_versions.get(key) orelse continue; - if (e.version != ver) continue; - if (!try canCollapse(allocator, tris, tri_active, e.v1, e.v2)) continue; - entry = e; - break; - } - - const e = entry orelse break; - - const combined = QuadricMatrix.add(quadrics[e.v1], quadrics[e.v2]); - const result = combined.optimalPosition(pos[e.v1], pos[e.v2]); - - if (wouldFlipNormal(pos, tris, tri_active, e.v1, e.v2, result.pos)) { - const mid: [3]f64 = .{ - (pos[e.v1][0] + pos[e.v2][0]) * 0.5, - (pos[e.v1][1] + pos[e.v2][1]) * 0.5, - (pos[e.v1][2] + pos[e.v2][2]) * 0.5, - }; - if (wouldFlipNormal(pos, tris, tri_active, e.v1, e.v2, mid)) { - skipped_collapses += 1; - continue; - } - pos[e.v1] = mid; - } else { - pos[e.v1] = result.pos; - } - - const cost = @max(QuadricMatrix.evaluate(combined, pos[e.v1][0], pos[e.v1][1], pos[e.v1][2]), 0); - if (cost > max_error) max_error = cost; - - quadrics[e.v1] = combined; - active[e.v2] = false; - - var neighbor_count: usize = 0; - - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - var has_v1 = false; - var has_v2 = false; - for (tri) |v| { - if (v == e.v1) has_v1 = true; - if (v == e.v2) has_v2 = true; - } - - if (has_v1 and has_v2) { - tri_active[ti] = false; - current_tri_count -= 1; - } else if (has_v2) { - for (tri) |v| { - if (v != e.v1 and v != e.v2) { - try addUnique(neighbor_buf, &neighbor_count, v); - } - } - for (0..3) |j| { - if (tris[ti][j] == e.v2) tris[ti][j] = e.v1; - } - } - } - - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - var has_v1 = false; - for (tri) |v| { - if (v == e.v1) { - has_v1 = true; - break; - } - } - if (!has_v1) continue; - for (tri) |v| { - if (v != e.v1) { - try addUnique(neighbor_buf, &neighbor_count, v); - } - } - } - - for (neighbor_buf[0..neighbor_count]) |neighbor| { - if (!active[neighbor]) continue; - if (neighbor == e.v1) continue; - const key = edgeKey(e.v1, neighbor); - const new_combined = QuadricMatrix.add(quadrics[e.v1], quadrics[neighbor]); - const new_result = new_combined.optimalPosition(pos[e.v1], pos[neighbor]); - const new_cost = @max(new_combined.evaluate(new_result.pos[0], new_result.pos[1], new_result.pos[2]), 0); - const cur_ver = edge_versions.get(key) orelse 0; - const new_ver = cur_ver + 1; - try edge_versions.put(key, new_ver); - try heap.push(allocator, .{ - .v1 = e.v1, - .v2 = neighbor, - .cost = new_cost, - .version = new_ver, - }); - } - } - - return collectResults(allocator, vertices, pos, active, tris, tri_active, num_triangles, current_tri_count, max_error, skipped_collapses); - } -}; - -fn computeQuadrics( - quadrics: []QuadricMatrix, - pos: []const [3]f64, - tris: []const [3]u32, - tri_active: []const bool, -) void { - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - - const p0 = pos[tri[0]]; - const p1 = pos[tri[1]]; - const p2 = pos[tri[2]]; - - const e1x = p1[0] - p0[0]; - const e1y = p1[1] - p0[1]; - const e1z = p1[2] - p0[2]; - const e2x = p2[0] - p0[0]; - const e2y = p2[1] - p0[1]; - const e2z = p2[2] - p0[2]; - - const nx = e1y * e2z - e1z * e2y; - const ny = e1z * e2x - e1x * e2z; - const nz = e1x * e2y - e1y * e2x; - - const len = @sqrt(nx * nx + ny * ny + nz * nz); - if (len < 1e-20) continue; - - const inv_len = 1.0 / len; - const nnx = nx * inv_len; - const nny = ny * inv_len; - const nnz = nz * inv_len; - - const d = -(nnx * p0[0] + nny * p0[1] + nnz * p0[2]); - const q = QuadricMatrix.fromPlane(nnx, nny, nnz, d); - quadrics[tri[0]] = QuadricMatrix.add(quadrics[tri[0]], q); - quadrics[tri[1]] = QuadricMatrix.add(quadrics[tri[1]], q); - quadrics[tri[2]] = QuadricMatrix.add(quadrics[tri[2]], q); - } -} - -fn canCollapse( - allocator: Allocator, - tris: []const [3]u32, - tri_active: []const bool, - v1: u32, - v2: u32, -) !bool { - var link_edge = std.ArrayListUnmanaged(u32).empty; - defer link_edge.deinit(allocator); - var link_v1 = std.ArrayListUnmanaged(u32).empty; - defer link_v1.deinit(allocator); - var link_v2 = std.ArrayListUnmanaged(u32).empty; - defer link_v2.deinit(allocator); - - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - - const has_v1 = tri[0] == v1 or tri[1] == v1 or tri[2] == v1; - const has_v2 = tri[0] == v2 or tri[1] == v2 or tri[2] == v2; - - if (!has_v1 and !has_v2) continue; - - for (tri) |v| { - if (v == v1 or v == v2) continue; - - if (has_v1 and has_v2) try addUniqueGrowable(allocator, &link_edge, v); - if (has_v1) try addUniqueGrowable(allocator, &link_v1, v); - if (has_v2) try addUniqueGrowable(allocator, &link_v2, v); - } - } - - for (link_v1.items) |n1| { - for (link_v2.items) |n2| { - if (n1 != n2) continue; - var found = false; - for (link_edge.items) |le| { - if (le == n1) { - found = true; - break; - } - } - if (!found) return false; - } - } - - return true; -} - -fn addUniqueGrowable(allocator: Allocator, list: *std.ArrayListUnmanaged(u32), val: u32) !void { - for (list.items) |v| { - if (v == val) return; - } - try list.append(allocator, val); -} - -fn addUnique(buf: []u32, len: *usize, val: u32) !void { - if (len.* >= buf.len) return error.BufferOverflow; - for (buf[0..len.*]) |v| { - if (v == val) return; - } - buf[len.*] = val; - len.* += 1; -} - -fn wouldFlipNormal( - pos: []const [3]f64, - tris: []const [3]u32, - tri_active: []const bool, - v1: u32, - v2: u32, - new_pos: [3]f64, -) bool { - for (tris, 0..) |tri, ti| { - if (!tri_active[ti]) continue; - - const has_v1 = tri[0] == v1 or tri[1] == v1 or tri[2] == v1; - const has_v2 = tri[0] == v2 or tri[1] == v2 or tri[2] == v2; - if (!has_v1 or has_v2) continue; - - var old_pts: [3][3]f64 = undefined; - var new_pts: [3][3]f64 = undefined; - - for (tri, 0..) |v, j| { - if (v == v1) { - old_pts[j] = pos[v1]; - new_pts[j] = new_pos; - } else { - old_pts[j] = pos[v]; - new_pts[j] = pos[v]; - } - } - - const old_n = triNormal(old_pts[0], old_pts[1], old_pts[2]); - const new_n = triNormal(new_pts[0], new_pts[1], new_pts[2]); - const dot = old_n[0] * new_n[0] + old_n[1] * new_n[1] + old_n[2] * new_n[2]; - if (dot < 0) return true; - } - return false; -} - -fn triNormal(a: [3]f64, b: [3]f64, c: [3]f64) [3]f64 { - const e1x = b[0] - a[0]; - const e1y = b[1] - a[1]; - const e1z = b[2] - a[2]; - const e2x = c[0] - a[0]; - const e2y = c[1] - a[1]; - const e2z = c[2] - a[2]; - const nx = e1y * e2z - e1z * e2y; - const ny = e1z * e2x - e1x * e2z; - const nz = e1x * e2y - e1y * e2x; - const len = @sqrt(nx * nx + ny * ny + nz * nz); - if (len < 1e-20) return .{ 0, 0, 0 }; - const inv = 1.0 / len; - return .{ nx * inv, ny * inv, nz * inv }; -} - -fn collectResults( - allocator: Allocator, - original_vertices: []const Vertex, - pos: []const [3]f64, - active: []const bool, - tris: []const [3]u32, - tri_active: []const bool, - num_tris: u32, - current_tri_count: u32, - max_error: f64, - skipped: u32, -) !SimplifiedMesh { - const n = active.len; - - var remap = try allocator.alloc(u32, n); - defer allocator.free(remap); - - var vertex_count: u32 = 0; - for (active, 0..) |is_active, i| { - if (is_active) { - remap[i] = vertex_count; - vertex_count += 1; - } else { - remap[i] = std.math.maxInt(u32); - } - } - - if (vertex_count == 0) { - return .{ - .vertices = &.{}, - .indices = &.{}, - .original_triangle_count = num_tris, - .simplified_triangle_count = 0, - .error_estimate = max_error, - .skipped_collapses = skipped, - }; - } - - const out_verts = try allocator.alloc(Vertex, vertex_count); - errdefer allocator.free(out_verts); - var vi: u32 = 0; - for (active, 0..) |is_active, i| { - if (!is_active) continue; - out_verts[vi] = original_vertices[i]; - out_verts[vi].pos = .{ - @floatCast(pos[i][0]), - @floatCast(pos[i][1]), - @floatCast(pos[i][2]), - }; - vi += 1; - } - - const index_count: usize = @as(usize, @intCast(current_tri_count)) * 3; - if (index_count == 0) { - allocator.free(out_verts); - return .{ - .vertices = &.{}, - .indices = &.{}, - .original_triangle_count = num_tris, - .simplified_triangle_count = 0, - .error_estimate = max_error, - .skipped_collapses = skipped, - }; - } - - const out_indices = try allocator.alloc(u32, index_count); - var ii: usize = 0; - for (tris, 0..) |tri, ti| { - if (ti >= num_tris) break; - if (!tri_active[ti]) continue; - out_indices[ii] = remap[tri[0]]; - out_indices[ii + 1] = remap[tri[1]]; - out_indices[ii + 2] = remap[tri[2]]; - ii += 3; - } - - return .{ - .vertices = out_verts[0..vertex_count], - .indices = out_indices[0..ii], - .original_triangle_count = num_tris, - .simplified_triangle_count = current_tri_count, - .error_estimate = max_error, - .skipped_collapses = skipped, - }; -} - -fn makeVertex(x: f32, y: f32, z: f32) Vertex { - return Vertex.init(.{ x, y, z }, .{ 1.0, 1.0, 1.0 }, .{ 0.0, 1.0, 0.0 }, .{ 0.0, 0.0 }, 0, 1.0, .{ 0.0, 0.0, 0.0 }, 1.0); -} - -fn createCube(allocator: Allocator) !struct { vertices: []Vertex, indices: []u32 } { - const verts = try allocator.alloc(Vertex, 8); - verts[0] = makeVertex(0, 0, 0); - verts[1] = makeVertex(1, 0, 0); - verts[2] = makeVertex(1, 1, 0); - verts[3] = makeVertex(0, 1, 0); - verts[4] = makeVertex(0, 0, 1); - verts[5] = makeVertex(1, 0, 1); - verts[6] = makeVertex(1, 1, 1); - verts[7] = makeVertex(0, 1, 1); - - const idx = try allocator.dupe(u32, &[_]u32{ - 4, 5, 6, 4, 6, 7, - 1, 0, 3, 1, 3, 2, - 5, 1, 2, 5, 2, 6, - 0, 4, 7, 0, 7, 3, - 7, 6, 2, 7, 2, 3, - 0, 1, 5, 0, 5, 4, - }); - - return .{ .vertices = verts, .indices = idx }; -} - -fn createSphere(allocator: Allocator, lat_divs: u32, lon_divs: u32) !struct { vertices: []Vertex, indices: []u32 } { - const num_rings = lat_divs - 1; - const num_verts: usize = 2 + @as(usize, num_rings) * lon_divs; - - const verts = try allocator.alloc(Vertex, num_verts); - - verts[0] = makeVertex(0.0, 1.0, 0.0); - - var vi: usize = 1; - for (1..lat_divs) |ring| { - const phi = std.math.pi * @as(f64, @floatFromInt(ring)) / @as(f64, @floatFromInt(lat_divs)); - for (0..lon_divs) |seg| { - const theta = 2.0 * std.math.pi * @as(f64, @floatFromInt(seg)) / @as(f64, @floatFromInt(lon_divs)); - const x: f32 = @floatCast(@sin(phi) * @cos(theta)); - const y: f32 = @floatCast(@cos(phi)); - const z: f32 = @floatCast(@sin(phi) * @sin(theta)); - verts[vi] = makeVertex(x, y, z); - vi += 1; - } - } - - verts[vi] = makeVertex(0.0, -1.0, 0.0); - - const south_pole_idx: u32 = @intCast(num_verts - 1); - const num_tris: usize = @as(usize, lon_divs) * 2 + @as(usize, num_rings - 1) * @as(usize, lon_divs) * 2; - const indices = try allocator.alloc(u32, num_tris * 3); - var ii: usize = 0; - - for (0..lon_divs) |seg| { - const next_seg = @as(u32, @intCast((seg + 1) % lon_divs)); - const seg_u32: u32 = @intCast(seg); - indices[ii] = 0; - indices[ii + 1] = 1 + seg_u32; - indices[ii + 2] = 1 + next_seg; - ii += 3; - } - - for (0..@intCast(num_rings - 1)) |ring| { - const ring_u32: u32 = @intCast(ring); - for (0..lon_divs) |seg| { - const next_seg = @as(u32, @intCast((seg + 1) % lon_divs)); - const seg_u32: u32 = @intCast(seg); - const curr = 1 + ring_u32 * lon_divs + seg_u32; - const curr_next = 1 + ring_u32 * lon_divs + next_seg; - const below = 1 + (ring_u32 + 1) * lon_divs + seg_u32; - const below_next = 1 + (ring_u32 + 1) * lon_divs + next_seg; - - indices[ii] = curr; - indices[ii + 1] = below; - indices[ii + 2] = below_next; - ii += 3; - - indices[ii] = curr; - indices[ii + 1] = below_next; - indices[ii + 2] = curr_next; - ii += 3; - } - } - - const last_ring_start: u32 = 1 + @as(u32, @intCast(num_rings - 1)) * lon_divs; - for (0..lon_divs) |seg| { - const next_seg = @as(u32, @intCast((seg + 1) % lon_divs)); - const seg_u32: u32 = @intCast(seg); - indices[ii] = south_pole_idx; - indices[ii + 1] = last_ring_start + next_seg; - indices[ii + 2] = last_ring_start + seg_u32; - ii += 3; - } - - return .{ .vertices = verts, .indices = indices[0..ii] }; -} - -test "simplify cube 12 to 4 triangles" { - const testing = std.testing; - const allocator = testing.allocator; - - const cube = try createCube(allocator); - defer { - allocator.free(cube.vertices); - allocator.free(cube.indices); - } - - const result = try QuadricSimplifier.simplify(allocator, cube.vertices, cube.indices, 4); - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expectEqual(@as(u32, 12), result.original_triangle_count); - try testing.expect(result.simplified_triangle_count <= 4); - try testing.expect(result.error_estimate >= 0); - try testing.expect(result.indices.len % 3 == 0); -} - -test "simplify sphere preserves shape" { - const testing = std.testing; - const allocator = testing.allocator; - - const sphere = try createSphere(allocator, 10, 20); - defer { - allocator.free(sphere.vertices); - allocator.free(sphere.indices); - } - - const original_tris: u32 = @intCast(sphere.indices.len / 3); - const target = original_tris / 2; - - const result = try QuadricSimplifier.simplify(allocator, sphere.vertices, sphere.indices, target); - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expect(result.simplified_triangle_count <= target); - try testing.expect(result.simplified_triangle_count > 0); - - for (result.vertices) |v| { - const dist = @sqrt(v.pos[0] * v.pos[0] + v.pos[1] * v.pos[1] + v.pos[2] * v.pos[2]); - try testing.expect(dist < 2.0); - } -} - -test "no-op when target >= actual triangles" { - const testing = std.testing; - const allocator = testing.allocator; - - const cube = try createCube(allocator); - defer { - allocator.free(cube.vertices); - allocator.free(cube.indices); - } - - const result = try QuadricSimplifier.simplify(allocator, cube.vertices, cube.indices, 20); - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expectEqual(@as(u32, 12), result.simplified_triangle_count); - try testing.expectEqualSlices(u32, cube.indices, result.indices); - try testing.expectEqual(@as(u32, 0), @as(u32, @intFromFloat(result.error_estimate))); -} - -test "target zero triangles does not crash" { - const testing = std.testing; - const allocator = testing.allocator; - - const cube = try createCube(allocator); - defer { - allocator.free(cube.vertices); - allocator.free(cube.indices); - } - - const result = try QuadricSimplifier.simplify(allocator, cube.vertices, cube.indices, 0); - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expect(result.simplified_triangle_count < 12); - try testing.expect(result.error_estimate >= 0); -} - -test "single triangle input produces single triangle output" { - const testing = std.testing; - const allocator = testing.allocator; - - const verts = try allocator.alloc(Vertex, 3); - defer allocator.free(verts); - verts[0] = makeVertex(0, 0, 0); - verts[1] = makeVertex(1, 0, 0); - verts[2] = makeVertex(0, 1, 0); - - const indices = try allocator.dupe(u32, &[_]u32{ 0, 1, 2 }); - defer allocator.free(indices); - - const result = try QuadricSimplifier.simplify(allocator, verts, indices, 1); - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expectEqual(@as(u32, 1), result.simplified_triangle_count); - try testing.expectEqual(@as(usize, 3), result.indices.len); -} - -test "empty mesh returns empty result" { - const testing = std.testing; - const allocator = testing.allocator; - - const result = try QuadricSimplifier.simplify(allocator, &.{}, &.{}, 0); - try testing.expectEqual(@as(usize, 0), result.vertices.len); - try testing.expectEqual(@as(usize, 0), result.indices.len); -} - -test "performance simplify mesh" { - const testing = std.testing; - const allocator = testing.allocator; - - const sphere = try createSphere(allocator, 20, 40); - defer { - allocator.free(sphere.vertices); - allocator.free(sphere.indices); - } - - const original_tris: u32 = @intCast(sphere.indices.len / 3); - const target = original_tris / 4; - - const start = std.Io.Clock.awake.now(std.Options.debug_io); - const result = try QuadricSimplifier.simplify(allocator, sphere.vertices, sphere.indices, target); - const end = std.Io.Clock.awake.now(std.Options.debug_io); - const elapsed_ns = start.durationTo(end).toNanoseconds(); - const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0; - - defer { - allocator.free(result.vertices); - allocator.free(result.indices); - } - - try testing.expect(result.simplified_triangle_count <= target); - try testing.expect(elapsed_ms < 2000.0); -} diff --git a/modules/world-meshing/src/root.zig b/modules/world-meshing/src/root.zig index 17f256c3..5beb2bd5 100644 --- a/modules/world-meshing/src/root.zig +++ b/modules/world-meshing/src/root.zig @@ -19,7 +19,6 @@ pub const meshing = struct { pub const custom_mesh_mesher = @import("meshing/custom_mesh_mesher.zig"); pub const greedy_mesher = @import("meshing/greedy_mesher.zig"); pub const lighting_sampler = @import("meshing/lighting_sampler.zig"); - pub const quadric_simplifier = @import("meshing/quadric_simplifier.zig"); pub const wall_attached_mesher = @import("meshing/wall_attached_mesher.zig"); }; diff --git a/modules/world-runtime/src/chunk_queue_coordinator.zig b/modules/world-runtime/src/chunk_queue_coordinator.zig index f3450bec..4cdbd8b5 100644 --- a/modules/world-runtime/src/chunk_queue_coordinator.zig +++ b/modules/world-runtime/src/chunk_queue_coordinator.zig @@ -20,8 +20,6 @@ const SaveManager = @import("world-persistence").SaveManager; const LoadResult = @import("world-persistence").LoadResult; const GpuAccelerationCoordinator = @import("gpu_acceleration_coordinator.zig").GpuAccelerationCoordinator; const WorldLightingEngine = @import("lighting_engine.zig").WorldLightingEngine; -const LODManager = @import("world-lod").LODManager; -const LODColumnProvenance = @import("world-core").LODColumnProvenance; /// A pending chunk transition reference captured by a worker when it flips a /// chunk into the `.generated` state. The main thread later drains the queue @@ -87,17 +85,6 @@ const MeshInputRevisions = struct { const RECOVERY_SCAN_PERIOD: u64 = 60; const MAX_MISSING_SCAN_STEPS: usize = 1024; -/// Persisted chunks are authoritative over advisory LOD source caches, even -/// when an older cache snapshot already carries edited provenance. Freshly -/// generated chunks retain the opt-in ingestion path until it is qualified for -/// the default streaming workload. -fn lodIngestionProvenance(load_result: LoadResult, ingest_generated_chunks: bool) ?LODColumnProvenance { - return switch (load_result) { - .success, .success_relight_required => .edited, - else => if (ingest_generated_chunks) .chunk_derived else null, - }; -} - pub const ChunkQueueCoordinator = struct { allocator: std.mem.Allocator, storage: *ChunkStorage, @@ -110,8 +97,6 @@ pub const ChunkQueueCoordinator = struct { gpu: *GpuAccelerationCoordinator, max_uploads_per_frame: usize, save_manager: ?*SaveManager = null, - // Optional LOD manager fed chunk-derived data after generation/load. - lod_manager: ?*LODManager = null, chunks_generated_total: std.atomic.Value(u64) = .init(0), chunks_meshed_total: std.atomic.Value(u64) = .init(0), @@ -164,10 +149,6 @@ pub const ChunkQueueCoordinator = struct { self.save_manager = sm; } - pub fn setLODManager(self: *ChunkQueueCoordinator, mgr: ?*LODManager) void { - self.lod_manager = mgr; - } - pub fn setView(self: *ChunkQueueCoordinator, pc_x: i32, pc_z: i32, render_dist: i32) void { self.last_pc_x.store(pc_x, .release); self.last_pc_z.store(pc_z, .release); @@ -747,13 +728,6 @@ pub const ChunkQueueCoordinator = struct { if (chunk_data.chunk.state == .generated and chunk_data.chunk.job_token == job.data.chunk.job_token) { self.markNeighborsForRemesh(cx, cz); self.enqueueReadyNeighborhood(cx, cz); - // Saved chunks always override advisory LOD cache data. Fresh - // generated chunks keep the separately qualified opt-in path. - if (self.lod_manager) |mgr| { - if (lodIngestionProvenance(load_result, engine_core.envFlag("ZIGCRAFT_LOD_CHUNK_INGEST", false))) |provenance| { - mgr.ingestChunk(cx, cz, &chunk_data.chunk, provenance); - } - } } } } @@ -1062,13 +1036,6 @@ test "runtime edits enqueue dirty renderable chunks immediately" { try testing.expectEqual(@as(usize, 1), coordinator.pending_mesh_incoming.items.len); } -test "saved chunks always override advisory LOD source snapshots" { - try std.testing.expectEqual(LODColumnProvenance.edited, lodIngestionProvenance(.success, false).?); - try std.testing.expectEqual(LODColumnProvenance.edited, lodIngestionProvenance(.success_relight_required, false).?); - try std.testing.expectEqual(@as(?LODColumnProvenance, null), lodIngestionProvenance(.not_found, false)); - try std.testing.expectEqual(LODColumnProvenance.chunk_derived, lodIngestionProvenance(.not_found, true).?); -} - test "missing chunk scan cursor covers concentric square rings without duplicates" { var coordinator: ChunkQueueCoordinator = undefined; coordinator.resetMissingScan(10, -5, 2); diff --git a/modules/world-runtime/src/world.zig b/modules/world-runtime/src/world.zig index 52129943..49b7447d 100644 --- a/modules/world-runtime/src/world.zig +++ b/modules/world-runtime/src/world.zig @@ -20,13 +20,10 @@ const WorldMap = gen_interface.WorldMap; const registry = @import("world-worldgen").registry; const rhi_mod = @import("engine-rhi").rhi; const RHI = rhi_mod.RHI; -const WorldLOD = @import("world-lod").WorldLOD(RHI); -const LODGenerator = @import("world-lod").LODGenerator; fn getenv(name: [:0]const u8) ?[]const u8 { return runtime_env.getenv(name); } -const LODManager = @import("world-lod").LODManager; const math = @import("engine-math"); const Vec3 = math.Vec3; const Mat4 = math.Mat4; @@ -66,11 +63,9 @@ pub const WorldOrchestration = struct { world.checkAutoSave(); } - /// Renders all enabled world layers for the current camera. - /// LOD rendering is controlled by `render_lod` and the world configuration; call after `update` has advanced queues. - pub fn render(renderer: anytype, streamer: anytype, lod_manager: anytype, lod_enabled: bool, view_proj: Mat4, camera_pos: Vec3, render_lod: bool, layer: anytype) void { - const allow_lod = lod_enabled and render_lod; - renderer.render(view_proj, camera_pos, streamer.getActiveRenderDistance(), lod_manager, allow_lod, layer); + /// Renders the full-detail chunk stream for the current camera. + pub fn render(renderer: anytype, streamer: anytype, view_proj: Mat4, camera_pos: Vec3, layer: anytype) void { + renderer.render(view_proj, camera_pos, streamer.getActiveRenderDistance(), layer); } }; const engine_core = @import("engine-core"); @@ -81,26 +76,12 @@ const RingBuffer = engine_core.ring_buffer.RingBuffer; const log = engine_core.log; const runtime_env = engine_core.runtime_env; -const LODConfig = @import("world-lod").lod_chunk.LODConfig; -const ILODConfig = @import("world-lod").lod_chunk.ILODConfig; -const LODLevel = @import("world-lod").LODLevel; const CHUNK_UNLOAD_BUFFER = world_core.CHUNK_UNLOAD_BUFFER; const SaveManager = @import("world-persistence").SaveManager; const LoadResult = @import("world-persistence").LoadResult; const GpuBlockBuffer = world_meshing.GpuBlockBuffer; const WorldMutationCoordinator = @import("world_mutation.zig").WorldMutationCoordinator; -fn lodGeneratorFromGenerator(generator: Generator) LODGenerator { - return .{ - .ptr = generator.ptr, - .generate_heightmap_only = generator.vtable.generateHeightmapOnly, - .maybe_recenter_cache = generator.vtable.maybeRecenterCache, - .seed = generator.getSeed(), - .identity_hash = std.hash.Wyhash.hash(0, generator.info.name), - .version = generator.info.version, - }; -} - /// Buffer distance beyond render_distance for chunk unloading. /// Prevents thrashing when player moves near chunk boundaries. // const CHUNK_UNLOAD_BUFFER: i32 = 1; @@ -120,14 +101,12 @@ pub const IWorld = struct { pub const VTable = struct { update: *const fn (ptr: *anyopaque, player_pos: Vec3, dt: f32) anyerror!void, - render: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, - renderOpaque: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, - renderFluid: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void, + render: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, + renderOpaque: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, + renderFluid: *const fn (ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void, deinit: *const fn (ptr: *anyopaque) void, getRenderStats: *const fn (ptr: *anyopaque) RenderStats, getStats: *const fn (ptr: *anyopaque) WorldStatsData, - getLODStats: *const fn (ptr: *anyopaque) ?@import("world-lod").LODStats, - isLODEnabled: *const fn (ptr: *anyopaque) bool, shadowScene: *const fn (ptr: *anyopaque) IShadowScene, enableSaveManager: *const fn (ptr: *anyopaque, save_dir_path: []const u8, world_name: []const u8) anyerror!void, takeSaveFailureWarningCount: *const fn (ptr: *anyopaque) usize, @@ -143,10 +122,6 @@ pub const IWorld = struct { getGeneratorName: *const fn (ptr: *anyopaque) []const u8, getRenderDistance: *const fn (ptr: *anyopaque) i32, setRenderDistance: *const fn (ptr: *anyopaque, distance: i32) void, - getHorizonDistance: *const fn (ptr: *anyopaque) i32, - setHorizonDistance: *const fn (ptr: *anyopaque, distance: i32) void, - isLODRenderingEnabled: *const fn (ptr: *anyopaque) bool, - toggleLODRendering: *const fn (ptr: *anyopaque) bool, getChunkStateCounts: *const fn (ptr: *anyopaque) ChunkStateCounts, isStartupBusy: *const fn (ptr: *anyopaque) bool, getWorldStateData: *const fn (ptr: *anyopaque) WorldStateData, @@ -163,24 +138,23 @@ pub const IWorld = struct { } /// Renders all enabled world layers for the current camera. - /// LOD rendering is controlled by `render_lod` and the world configuration; call after `update` has advanced queues. - pub fn render(self: IWorld, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.render(self.ptr, view_proj, camera_pos, render_lod); + pub fn render(self: IWorld, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.render(self.ptr, view_proj, camera_pos); } /// Renders opaque terrain and world geometry for the current camera. /// Fluid and transparent passes are intentionally excluded. - pub fn renderOpaque(self: IWorld, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.renderOpaque(self.ptr, view_proj, camera_pos, render_lod); + pub fn renderOpaque(self: IWorld, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.renderOpaque(self.ptr, view_proj, camera_pos); } /// Renders fluid surfaces for the current camera. /// Opaque depth and reflection resources should already be prepared by the renderer. - pub fn renderFluid(self: IWorld, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.vtable.renderFluid(self.ptr, view_proj, camera_pos, render_lod); + pub fn renderFluid(self: IWorld, view_proj: Mat4, camera_pos: Vec3) void { + self.vtable.renderFluid(self.ptr, view_proj, camera_pos); } - /// Stops world jobs and releases streaming, meshing, LOD, rendering, and persistence resources. + /// Stops world jobs and releases streaming, meshing, rendering, and persistence resources. /// No borrowed world sub-interfaces may be used after this returns. pub fn deinit(self: IWorld) void { self.vtable.deinit(self.ptr); @@ -198,18 +172,6 @@ pub const IWorld = struct { return self.vtable.getStats(self.ptr); } - /// Returns distant-terrain LOD statistics when LOD is available. - /// Returns `null` when the world has no active LOD manager. - pub fn getLODStats(self: IWorld) ?@import("world-lod").LODStats { - return self.vtable.getLODStats(self.ptr); - } - - /// Reports whether the world was constructed with LOD support. - /// This is a capability flag, separate from runtime LOD render toggles. - pub fn isLODEnabled(self: IWorld) bool { - return self.vtable.isLODEnabled(self.ptr); - } - /// Returns the world shadow-scene interface used by the shadow renderer. /// The returned interface borrows the world and must not outlive it. pub fn shadowScene(self: IWorld) IShadowScene { @@ -300,30 +262,6 @@ pub const IWorld = struct { self.vtable.setRenderDistance(self.ptr, distance); } - /// Returns the distant-terrain horizon distance in chunks. - /// Used by LOD scheduling and settings UI. - pub fn getHorizonDistance(self: IWorld) i32 { - return self.vtable.getHorizonDistance(self.ptr); - } - - /// Changes the distant-terrain horizon distance. - /// LOD queues and visibility update on subsequent world ticks. - pub fn setHorizonDistance(self: IWorld, distance: i32) void { - self.vtable.setHorizonDistance(self.ptr, distance); - } - - /// Reports whether LOD drawing is currently enabled. - /// This runtime toggle is separate from LOD subsystem availability. - pub fn isLODRenderingEnabled(self: IWorld) bool { - return self.vtable.isLODRenderingEnabled(self.ptr); - } - - /// Toggles LOD drawing and returns the new enabled state. - /// Does not destroy LOD data; it only changes render participation. - pub fn toggleLODRendering(self: IWorld) bool { - return self.vtable.toggleLODRendering(self.ptr); - } - /// Returns counts of chunks in each streaming/meshing state. /// Used for diagnostics and startup-busy checks. pub fn getChunkStateCounts(self: IWorld) ChunkStateCounts { @@ -393,7 +331,7 @@ pub const IWorldSimulation = struct { try self.world.update(player_pos, dt); } - /// Stops world jobs and releases streaming, meshing, LOD, rendering, and persistence resources. + /// Stops world jobs and releases streaming, meshing, rendering, and persistence resources. /// No borrowed world sub-interfaces may be used after this returns. pub fn deinit(self: IWorldSimulation) void { self.world.deinit(); @@ -446,21 +384,20 @@ pub const IWorldRenderView = struct { world: IWorld, /// Renders all enabled world layers for the current camera. - /// LOD rendering is controlled by `render_lod` and the world configuration; call after `update` has advanced queues. - pub fn render(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.world.render(view_proj, camera_pos, render_lod); + pub fn render(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.world.render(view_proj, camera_pos); } /// Renders opaque terrain and world geometry for the current camera. /// Fluid and transparent passes are intentionally excluded. - pub fn renderOpaque(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.world.renderOpaque(view_proj, camera_pos, render_lod); + pub fn renderOpaque(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.world.renderOpaque(view_proj, camera_pos); } /// Renders fluid surfaces for the current camera. /// Opaque depth and reflection resources should already be prepared by the renderer. - pub fn renderFluid(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - self.world.renderFluid(view_proj, camera_pos, render_lod); + pub fn renderFluid(self: IWorldRenderView, view_proj: Mat4, camera_pos: Vec3) void { + self.world.renderFluid(view_proj, camera_pos); } /// Returns the world shadow-scene interface used by the shadow renderer. @@ -503,18 +440,6 @@ pub const IWorldTelemetry = struct { return self.world.getStats(); } - /// Returns distant-terrain LOD statistics when LOD is available. - /// Returns `null` when the world has no active LOD manager. - pub fn getLODStats(self: IWorldTelemetry) ?@import("world-lod").LODStats { - return self.world.getLODStats(); - } - - /// Reports whether the world was constructed with LOD support. - /// This is a capability flag, separate from runtime LOD render toggles. - pub fn isLODEnabled(self: IWorldTelemetry) bool { - return self.world.isLODEnabled(); - } - /// Returns the active chunk render distance in chunks. /// Used by streaming, renderer masks, and settings UI. pub fn getRenderDistance(self: IWorldTelemetry) i32 { @@ -527,30 +452,6 @@ pub const IWorldTelemetry = struct { self.world.setRenderDistance(distance); } - /// Returns the distant-terrain horizon distance in chunks. - /// Used by LOD scheduling and settings UI. - pub fn getHorizonDistance(self: IWorldTelemetry) i32 { - return self.world.getHorizonDistance(); - } - - /// Changes the distant-terrain horizon distance. - /// LOD queues and visibility update on subsequent world ticks. - pub fn setHorizonDistance(self: IWorldTelemetry, distance: i32) void { - self.world.setHorizonDistance(distance); - } - - /// Reports whether LOD drawing is currently enabled. - /// This runtime toggle is separate from LOD subsystem availability. - pub fn isLODRenderingEnabled(self: IWorldTelemetry) bool { - return self.world.isLODRenderingEnabled(); - } - - /// Toggles LOD drawing and returns the new enabled state. - /// Does not destroy LOD data; it only changes render participation. - pub fn toggleLODRendering(self: IWorldTelemetry) bool { - return self.world.toggleLODRendering(); - } - /// Returns counts of chunks in each streaming/meshing state. /// Used for diagnostics and startup-busy checks. pub fn getChunkStateCounts(self: IWorldTelemetry) ChunkStateCounts { @@ -610,7 +511,6 @@ pub const World = struct { rhi: RHI, atlas: *const TextureAtlas, generator_index: usize = 0, - lod_config: ?ILODConfig = null, }; storage: ChunkStorage, @@ -619,18 +519,12 @@ pub const World = struct { allocator: std.mem.Allocator, generator: Generator, render_distance: i32, - lod_chunk_render_radius_limit: i32, - horizon_distance: i32, rhi: RHI, paused: bool = false, safe_mode: bool, safe_render_distance: i32, map_mutation_revision: std.atomic.Value(u64) = .init(0), - // LOD System (Issue #114, #293) - lod: ?*WorldLOD, - lod_enabled: bool, // Runtime toggle for LOD rendering - // Save system (Issue #380) save_manager: ?*SaveManager, @@ -643,7 +537,7 @@ pub const World = struct { // LPV lighting grid builder (Issue #789) lpv_grid_builder: LpvGridBuilder, - /// Creates a world runtime with chunk storage, streaming, meshing, rendering, and optional LOD support. + /// Creates a world runtime with full-detail chunk streaming, meshing, rendering, and persistence. /// The allocator, generator, and RHI-backed resources must remain valid for the world lifetime. Propagates errors from streaming, persistence, meshing, or mutation subsystems. pub fn init(options: InitOptions) !*World { const allocator = options.allocator; @@ -654,10 +548,7 @@ pub const World = struct { const safe_mode = runtime_env.safeModeEnabled(); const strict_safe_mode = runtime_env.strictSafeModeEnabled(); const safe_render_distance: i32 = @max(options.render_distance, 2); - const streamer_render_distance: i32 = if (options.lod_config) |lod_config| - effectiveChunkRenderRadius(safe_render_distance, lod_config.getChunkRenderRadius(), true) - else - effectiveChunkRenderRadius(safe_render_distance, safe_render_distance, false); + const streamer_render_distance = safe_render_distance; const max_uploads: usize = if (strict_safe_mode) @as(usize, 4) else if (safe_mode) @@ -674,16 +565,12 @@ pub const World = struct { .renderer = undefined, .allocator = allocator, .render_distance = safe_render_distance, - .lod_chunk_render_radius_limit = streamer_render_distance, - .horizon_distance = if (options.lod_config) |lod_config| lod_config.getRadii()[LODLevel.count - 1] else LODConfig.default_horizon_radius, .generator = try registry.createGenerator(options.generator_index, options.seed, allocator), .rhi = options.rhi, .paused = false, .safe_mode = safe_mode, .safe_render_distance = safe_render_distance, .map_mutation_revision = .init(0), - .lod = null, - .lod_enabled = false, .save_manager = null, .gpu_block_buffer = null, .mutation = undefined, @@ -716,23 +603,15 @@ pub const World = struct { ); log.log.info("World.init: initializing WorldStreamer (render_distance={}, requested={})", .{ streamer_render_distance, safe_render_distance }); - world.streamer = try WorldStreamer.init(allocator, &world.storage, world.generator, options.atlas, streamer_render_distance, options.lod_config != null, world.renderer.vertex_allocator, max_uploads, world.gpu_block_buffer, world.renderer.getGpuMesher()); + world.streamer = try WorldStreamer.init(allocator, &world.storage, world.generator, options.atlas, streamer_render_distance, world.renderer.vertex_allocator, max_uploads, world.gpu_block_buffer, world.renderer.getGpuMesher()); errdefer world.streamer.deinit(); - if (options.lod_config) |lod_config| { - world.lod = try WorldLOD.init(allocator, options.rhi, lod_config, lodGeneratorFromGenerator(world.generator), options.atlas); - world.lod_enabled = true; - world.streamer.setLODManager(world.lod.?.manager); - } return world; } - /// Stops world jobs and releases streaming, meshing, LOD, rendering, and persistence resources. + /// Stops world jobs and releases streaming, meshing, rendering, and persistence resources. /// No borrowed world sub-interfaces may be used after this returns. pub fn deinit(self: *World) void { - // Pause generation first: clears the gen/mesh/LOD job queues so worker - // threads stop pulling new jobs. (In-flight LOD heightmap jobs are - // aborted later by LODManager.stop_flag, set at the top of lod.deinit().) self.pauseGeneration(); self.rhi.query().waitIdle(); @@ -750,10 +629,6 @@ pub const World = struct { self.storage.deinitWithoutRHI(); self.renderer.deinit(); - if (self.lod) |lod| { - lod.deinit(); - } - self.generator.deinit(self.allocator); self.allocator.destroy(self); @@ -764,10 +639,6 @@ pub const World = struct { pub fn pauseGeneration(self: *World) void { self.paused = true; self.streamer.setPaused(true); - - if (self.lod) |lod| { - lod.pause(); - } } /// Resumes background chunk generation after a pause. @@ -775,10 +646,6 @@ pub const World = struct { pub fn resumeGeneration(self: *World) void { self.paused = false; self.streamer.setPaused(false); - - if (self.lod) |lod| { - lod.unpause(); - } } /// Attaches persistence to the world using a save directory and world name. @@ -788,9 +655,6 @@ pub const World = struct { const gen_name = self.generator.info.name; self.save_manager = try SaveManager.init(self.allocator, save_dir_path, world_name, seed, gen_name); self.streamer.setSaveManager(self.save_manager); - if (self.lod) |lod| { - try lod.enableCache(save_dir_path); - } } /// Returns and clears the accumulated save-failure warning count. @@ -834,37 +698,11 @@ pub const World = struct { self.storage.chunks_mutex.unlock(); } - /// Applies pending block edits to LOD source data and waits for the - /// corresponding source-store writes. Full-detail save points call this - /// while resident chunks are still available to the ingestion resolver. - fn flushLODEditsForPersistence(self: *World) void { - const lod = self.lod orelse return; - lod.manager.flushEditedChunksNow(); - lod.manager.drainPendingIngestionsNow(); - lod.manager.flushDirtyStoresNow(); - // In-flight or currently missing target regions cannot accept the - // authoritative edit yet. Remove their settled old payloads so reload - // regenerates them instead of briefly displaying stale distant terrain. - lod.manager.invalidatePendingEditedStoresNow(); - } - - /// Starts bounded LOD persistence work without waiting for cache storage. - /// Autosave uses this path to avoid turning a slow source-store write into - /// an unbounded frame stall; explicit saves still use the full barrier. - fn queueLODEditsForPersistence(self: *World) void { - const lod = self.lod orelse return; - lod.manager.flushEditedChunksBounded(); - lod.manager.drainPendingIngestions(); - lod.manager.flushDirtyStores(); - } - /// Synchronously saves chunks marked dirty by mutations or streaming. /// Returns errors from persistence and leaves unsaved chunks dirty for later retry. pub fn saveAllModifiedChunks(self: *World) void { const sm = self.save_manager orelse return; - self.flushLODEditsForPersistence(); - var dirty_keys = self.enqueueModifiedChunks(sm); defer dirty_keys.deinit(self.allocator); @@ -882,8 +720,6 @@ pub const World = struct { const sm = self.save_manager orelse return; if (!sm.shouldAutoSave()) return; - self.queueLODEditsForPersistence(); - var dirty_keys = self.enqueueModifiedChunks(sm); defer dirty_keys.deinit(self.allocator); @@ -918,57 +754,8 @@ pub const World = struct { } } - /// Updates the full-detail streaming radius limit. Presets seed this value - /// during startup; the live World setting then synchronizes it to the - /// explicitly requested full-detail radius. - pub fn setLODChunkRenderRadiusLimit(self: *World, limit: i32) void { - const target = @max(limit, 1); - if (self.lod_chunk_render_radius_limit == target) return; - self.lod_chunk_render_radius_limit = target; - self.applyRenderDistance(); - } - fn applyRenderDistance(self: *World) void { - const chunk_render_radius = effectiveChunkRenderRadius(self.render_distance, self.lod_chunk_render_radius_limit, self.lod != null); - self.streamer.setRenderDistance(chunk_render_radius); - - if (self.lod) |lod| { - const radii = effectiveLODRadii(self.render_distance, self.lod_chunk_render_radius_limit, self.horizon_distance); - lod.setChunkRenderRadius(chunk_render_radius); - lod.setRadii(radii); - } - } - - pub fn effectiveChunkRenderRadius(render_distance: i32, preset_limit: i32, lod_enabled: bool) i32 { - const requested = @max(render_distance, 2); - return if (lod_enabled) @max(@min(requested, preset_limit), 2) else requested; - } - - /// Builds the live LOD ladder from the same capped near-detail radius used - /// by chunk streaming. The requested horizon remains independent. - pub fn effectiveLODRadii(render_distance: i32, preset_limit: i32, horizon_distance: i32) [LODLevel.count]i32 { - const chunk_render_radius = effectiveChunkRenderRadius(render_distance, preset_limit, true); - return LODConfig.radiiForDistances(chunk_render_radius, horizon_distance); - } - - /// Changes the distant-terrain horizon distance. - /// LOD queues and visibility update on subsequent world ticks. - pub fn setHorizonDistance(self: *World, distance: i32) void { - const target = LODConfig.normalizeHorizonDistance(self.render_distance, distance); - if (self.horizon_distance == target) return; - log.log.info("Horizon distance changed: {} -> {}", .{ self.horizon_distance, target }); - self.horizon_distance = target; - if (self.lod) |lod| { - const radii = effectiveLODRadii(self.render_distance, self.lod_chunk_render_radius_limit, target); - lod.setRadii(radii); - } - } - - /// Installs the bounded benchmark-only LOD source set used to exercise the - /// production compute/indirect culling path at high cardinality. - pub fn installGpuCullingScaleFixture(self: *World) !void { - const lod = self.lod orelse return error.LODDisabled; - try lod.installGpuCullingScaleFixture(); + self.streamer.setRenderDistance(self.render_distance); } /// Returns a resident chunk or creates storage for it. @@ -1024,12 +811,6 @@ pub const World = struct { try self.mutation.updateLighting(result); self.streamer.requestDirtyRemesh(result.chunk_x, result.chunk_z); }; - // Notify the LOD system so distant terrain reflects player edits after - // the player teleports away. Coalesced on a debounce inside LODManager. - if (self.lod) |lod| { - const wc = world_core.worldToChunk(world_x, world_z); - lod.manager.markChunkEdited(wc.chunk_x, wc.chunk_z); - } } pub fn getMapSurfaceRevision(self: *const World) u64 { @@ -1103,34 +884,20 @@ pub const World = struct { try WorldOrchestration.update(self.renderer, self.streamer, self, player_pos, dt); } - /// Renders all enabled world layers for the current camera. - /// LOD rendering is controlled by `render_lod` and the world configuration; call after `update` has advanced queues. - pub fn render(self: *World, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - const lod_mgr: ?*LODManager = if (self.lod) |lod| lod.manager else null; - WorldOrchestration.render(self.renderer, self.streamer, lod_mgr, self.lod_enabled, view_proj, camera_pos, render_lod, .all); - } - - /// Render-graph prepass entry point: dispatch LOD compute before a graphics - /// render pass becomes active. Normal rendering remains a CPU fallback. - pub fn prepareLODCulling(self: *World, view_proj: Mat4, camera_pos: Vec3) void { - if (self.lod) |lod| { - const detail_render_radius = @min(self.streamer.getActiveRenderDistance(), lod.manager.config.getChunkRenderRadius()); - lod.manager.prepareFrame(self.renderer.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkTerrainReadyForHandoff, @ptrCast(&self.storage), lod.manager.getHorizonRenderRadius(), detail_render_radius); - } + pub fn render(self: *World, view_proj: Mat4, camera_pos: Vec3) void { + WorldOrchestration.render(self.renderer, self.streamer, view_proj, camera_pos, .all); } /// Renders opaque terrain and world geometry for the current camera. /// Fluid and transparent passes are intentionally excluded. - pub fn renderOpaque(self: *World, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - const lod_mgr: ?*LODManager = if (self.lod) |lod| lod.manager else null; - WorldOrchestration.render(self.renderer, self.streamer, lod_mgr, self.lod_enabled, view_proj, camera_pos, render_lod, .terrain); + pub fn renderOpaque(self: *World, view_proj: Mat4, camera_pos: Vec3) void { + WorldOrchestration.render(self.renderer, self.streamer, view_proj, camera_pos, .terrain); } /// Renders fluid surfaces for the current camera. /// Opaque depth and reflection resources should already be prepared by the renderer. - pub fn renderFluid(self: *World, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - const lod_mgr: ?*LODManager = if (self.lod) |lod| lod.manager else null; - WorldOrchestration.render(self.renderer, self.streamer, lod_mgr, self.lod_enabled, view_proj, camera_pos, render_lod, .fluid); + pub fn renderFluid(self: *World, view_proj: Mat4, camera_pos: Vec3) void { + WorldOrchestration.render(self.renderer, self.streamer, view_proj, camera_pos, .fluid); } /// Renders world geometry into the active shadow pass. @@ -1267,8 +1034,6 @@ pub const World = struct { .deinit = ideinit, .getRenderStats = igetRenderStats, .getStats = igetStats, - .getLODStats = igetLODStats, - .isLODEnabled = iisLODEnabled, .shadowScene = ishadowScene, .enableSaveManager = ienableSaveManager, .takeSaveFailureWarningCount = itakeSaveFailureWarningCount, @@ -1284,10 +1049,6 @@ pub const World = struct { .getGeneratorName = igetGeneratorName, .getRenderDistance = igetRenderDistance, .setRenderDistance = isetRenderDistance, - .getHorizonDistance = igetHorizonDistance, - .setHorizonDistance = isetHorizonDistance, - .isLODRenderingEnabled = iisLODRenderingEnabled, - .toggleLODRendering = itoggleLODRendering, .getChunkStateCounts = igetChunkStateCounts, .isStartupBusy = iisStartupBusy, .getWorldStateData = igetWorldStateData, @@ -1298,35 +1059,29 @@ pub const World = struct { }; const WORLD_RENDER_VIEW_VTABLE = GraphicsWorldRenderView.VTable{ - .prepareLODCulling = iprepareLODCulling, .render = irender, .renderOpaque = irenderOpaque, .renderFluid = irenderFluid, }; - fn iprepareLODCulling(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void { - const self: *World = @ptrCast(@alignCast(ptr)); - self.prepareLODCulling(view_proj, camera_pos); - } - fn iupdate(ptr: *anyopaque, player_pos: Vec3, dt: f32) anyerror!void { const self: *World = @ptrCast(@alignCast(ptr)); return self.update(player_pos, dt); } - fn irender(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { + fn irender(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void { const self: *World = @ptrCast(@alignCast(ptr)); - self.render(view_proj, camera_pos, render_lod); + self.render(view_proj, camera_pos); } - fn irenderOpaque(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { + fn irenderOpaque(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void { const self: *World = @ptrCast(@alignCast(ptr)); - self.renderOpaque(view_proj, camera_pos, render_lod); + self.renderOpaque(view_proj, camera_pos); } - fn irenderFluid(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { + fn irenderFluid(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3) void { const self: *World = @ptrCast(@alignCast(ptr)); - self.renderFluid(view_proj, camera_pos, render_lod); + self.renderFluid(view_proj, camera_pos); } fn ideinit(ptr: *anyopaque) void { @@ -1344,16 +1099,6 @@ pub const World = struct { return self.getStats(); } - fn igetLODStats(ptr: *anyopaque) ?@import("world-lod").LODStats { - const self: *World = @ptrCast(@alignCast(ptr)); - return self.getLODStats(); - } - - fn iisLODEnabled(ptr: *anyopaque) bool { - const self: *World = @ptrCast(@alignCast(ptr)); - return self.isLODEnabled(); - } - fn ishadowScene(ptr: *anyopaque) IShadowScene { const self: *World = @ptrCast(@alignCast(ptr)); return self.shadowScene(); @@ -1429,27 +1174,6 @@ pub const World = struct { self.setRenderDistance(distance); } - fn igetHorizonDistance(ptr: *anyopaque) i32 { - const self: *World = @ptrCast(@alignCast(ptr)); - return self.horizon_distance; - } - - fn isetHorizonDistance(ptr: *anyopaque, distance: i32) void { - const self: *World = @ptrCast(@alignCast(ptr)); - self.setHorizonDistance(distance); - } - - fn iisLODRenderingEnabled(ptr: *anyopaque) bool { - const self: *World = @ptrCast(@alignCast(ptr)); - return self.lod_enabled; - } - - fn itoggleLODRendering(ptr: *anyopaque) bool { - const self: *World = @ptrCast(@alignCast(ptr)); - self.lod_enabled = !self.lod_enabled; - return self.lod_enabled; - } - fn igetChunkStateCounts(ptr: *anyopaque) ChunkStateCounts { const self: *World = @ptrCast(@alignCast(ptr)); return self.getChunkStateCounts(); @@ -1497,17 +1221,4 @@ pub const World = struct { const block = self.getBlock(x, y, z); return block_registry.getBlockDefinition(block).is_solid; } - - /// Get LOD system statistics (returns null if LOD not enabled) - pub fn getLODStats(self: *World) ?@import("world-lod").LODStats { - if (self.lod) |lod| { - return lod.getStats(); - } - return null; - } - - /// Check if LOD system is enabled - pub fn isLODEnabled(self: *const World) bool { - return self.lod != null; - } }; diff --git a/modules/world-runtime/src/world_facade_tests.zig b/modules/world-runtime/src/world_facade_tests.zig index 3454d66e..3b43ea8c 100644 --- a/modules/world-runtime/src/world_facade_tests.zig +++ b/modules/world-runtime/src/world_facade_tests.zig @@ -1,638 +1,35 @@ const std = @import("std"); -const testing = std.testing; - -const world_mod = @import("world.zig"); -const world_core = @import("world-core"); -const world_meshing = @import("world-meshing"); -const worldgen = @import("world-worldgen"); const math = @import("engine-math"); -const LpvGridBuilder = @import("lpv_grid_builder.zig").LpvGridBuilder; -const world_renderer = @import("world_renderer.zig"); -const RenderLayer = world_renderer.RenderLayer; -const WorldMutationCoordinator = @import("world_mutation.zig").WorldMutationCoordinator; -const SaveManager = @import("world-persistence").SaveManager; -const World = world_mod.World; - -test "full-detail radius follows active preset cap" { - try testing.expectEqual(@as(i32, 12), World.effectiveChunkRenderRadius(16, 12, true)); - try testing.expectEqual(@as(i32, 16), World.effectiveChunkRenderRadius(16, 16, true)); - try testing.expectEqual(@as(i32, 22), World.effectiveChunkRenderRadius(22, 10, false)); - try testing.expectEqual(@as(i32, 2), World.effectiveChunkRenderRadius(0, 12, true)); - try testing.expectEqual(@as(i32, 2), World.effectiveChunkRenderRadius(-8, 12, false)); -} - -test "live LOD radii follow the active full-detail preset cap" { - const expected = @import("world-lod").LODConfig.radiiForDistances(10, 1024); - try testing.expectEqual(expected, World.effectiveLODRadii(18, 10, 1024)); -} +const world = @import("world.zig"); +const renderer_mod = @import("world_renderer.zig"); test "full-detail render candidates use the streaming disk" { - try testing.expect(world_renderer.isWithinChunkRenderRadius(10, 0, 0, 0, 10)); - try testing.expect(world_renderer.isWithinChunkRenderRadius(-10, 0, 0, 0, 10)); - try testing.expect(!world_renderer.isWithinChunkRenderRadius(10, 10, 0, 0, 10)); - try testing.expect(!world_renderer.isWithinChunkRenderRadius(-11, 0, 0, 0, 10)); -} - -test "full-detail MDI overflow falls back before truncating visibility" { - try testing.expect(world_renderer.hasMdiCapacity(16_383, 49_149, 3)); - try testing.expect(!world_renderer.hasMdiCapacity(16_384, 0, 1)); - try testing.expect(!world_renderer.hasMdiCapacity(1, 49_151, 2)); -} - -const MockWorld = struct { - update_count: u32 = 0, - render_count: u32 = 0, - render_opaque_count: u32 = 0, - render_fluid_count: u32 = 0, - deinit_count: u32 = 0, - save_count: u32 = 0, - pause_count: u32 = 0, - set_block_count: u32 = 0, - render_distance: i32 = 12, - horizon_distance: i32 = 512, - paused: bool = false, - lod_rendering_enabled: bool = true, - last_block: world_core.BlockType = .air, - - fn iface(self: *@This()) world_mod.IWorld { - return .{ .ptr = self, .vtable = &vtable }; - } - - const vtable = world_mod.IWorld.VTable{ - .update = update, - .render = render, - .renderOpaque = renderOpaque, - .renderFluid = renderFluid, - .deinit = deinit, - .getRenderStats = getRenderStats, - .getStats = getStats, - .getLODStats = getLODStats, - .isLODEnabled = isLODEnabled, - .shadowScene = shadowScene, - .enableSaveManager = enableSaveManager, - .takeSaveFailureWarningCount = takeSaveFailureWarningCount, - .pauseGeneration = pauseGeneration, - .isPaused = isPaused, - .collisionWorld = collisionWorld, - .getBlock = getBlock, - .setBlock = setBlock, - .getColumnInfo = getColumnInfo, - .getDebugLightInfo = getDebugLightInfo, - .getRegionInfo = getRegionInfo, - .getGenerator = getGenerator, - .getGeneratorName = getGeneratorName, - .getRenderDistance = getRenderDistance, - .setRenderDistance = setRenderDistance, - .getHorizonDistance = getHorizonDistance, - .setHorizonDistance = setHorizonDistance, - .isLODRenderingEnabled = isLODRenderingEnabled, - .toggleLODRendering = toggleLODRendering, - .getChunkStateCounts = getChunkStateCounts, - .isStartupBusy = isStartupBusy, - .getWorldStateData = getWorldStateData, - .lpvWorld = lpvWorld, - .graphicsRenderView = graphicsRenderView, - .getGpuMeshDispatch = getGpuMeshDispatch, - .isGpuCullingEnabled = isGpuCullingEnabled, - }; - - fn cast(ptr: *anyopaque) *@This() { - return @ptrCast(@alignCast(ptr)); - } - - fn update(ptr: *anyopaque, player_pos: math.Vec3, dt: f32) anyerror!void { - _ = player_pos; - _ = dt; - cast(ptr).update_count += 1; - } - - fn render(ptr: *anyopaque, view_proj: math.Mat4, camera_pos: math.Vec3, render_lod: bool) void { - _ = view_proj; - _ = camera_pos; - _ = render_lod; - cast(ptr).render_count += 1; - } - - fn prepareLODCulling(_: *anyopaque, _: math.Mat4, _: math.Vec3) void {} - - fn renderOpaque(ptr: *anyopaque, view_proj: math.Mat4, camera_pos: math.Vec3, render_lod: bool) void { - _ = view_proj; - _ = camera_pos; - _ = render_lod; - cast(ptr).render_opaque_count += 1; - } - - fn renderFluid(ptr: *anyopaque, view_proj: math.Mat4, camera_pos: math.Vec3, render_lod: bool) void { - _ = view_proj; - _ = camera_pos; - _ = render_lod; - cast(ptr).render_fluid_count += 1; - } - - fn deinit(ptr: *anyopaque) void { - cast(ptr).deinit_count += 1; - } - - fn getRenderStats(ptr: *anyopaque) world_mod.RenderStats { - _ = ptr; - return .{ .chunks_total = 3, .chunks_rendered = 2, .chunks_culled = 1, .vertices_rendered = 144 }; - } - - fn getStats(ptr: *anyopaque) world_mod.WorldStatsData { - _ = ptr; - return .{ .chunks_loaded = 4, .total_vertices = 200, .gen_queue = 1, .mesh_queue = 2, .upload_queue = 3 }; - } - - fn getLODStats(ptr: *anyopaque) ?@import("world-lod").LODStats { - _ = ptr; - return null; - } - - fn isLODEnabled(ptr: *anyopaque) bool { - _ = ptr; - return true; - } - - fn shadowScene(ptr: *anyopaque) @import("engine-rhi").IShadowScene { - _ = ptr; - return undefined; - } - - fn enableSaveManager(ptr: *anyopaque, save_dir_path: []const u8, world_name: []const u8) anyerror!void { - _ = save_dir_path; - _ = world_name; - cast(ptr).save_count += 1; - } - - fn takeSaveFailureWarningCount(ptr: *anyopaque) usize { - _ = ptr; - return 2; - } - - fn pauseGeneration(ptr: *anyopaque) void { - const self = cast(ptr); - self.paused = true; - self.pause_count += 1; - } - - fn isPaused(ptr: *anyopaque) bool { - return cast(ptr).paused; - } - - fn collisionWorld(ptr: *anyopaque) @import("engine-physics").VoxelCollisionWorld { - _ = ptr; - return undefined; - } - - fn getBlock(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32) world_core.BlockType { - _ = world_x; - _ = world_y; - _ = world_z; - return cast(ptr).last_block; - } - - fn setBlock(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32, block: world_core.BlockType) anyerror!void { - _ = world_x; - _ = world_y; - _ = world_z; - const self = cast(ptr); - self.last_block = block; - self.set_block_count += 1; - } - - fn getColumnInfo(ptr: *anyopaque, world_x: i32, world_z: i32) worldgen.ColumnInfo { - _ = ptr; - _ = world_x; - _ = world_z; - return undefined; - } - - fn getDebugLightInfo(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32) ?world_mod.DebugLightInfo { - _ = ptr; - _ = world_x; - _ = world_y; - _ = world_z; - return .{ .sky = 15, .block = 3 }; - } - - fn getRegionInfo(ptr: *anyopaque, world_x: i32, world_z: i32) worldgen.RegionInfo { - _ = ptr; - _ = world_x; - _ = world_z; - return undefined; - } - - fn getGenerator(ptr: *anyopaque) worldgen.Generator { - _ = ptr; - return undefined; - } - - fn getGeneratorName(ptr: *anyopaque) []const u8 { - _ = ptr; - return "mock-generator"; - } - - fn getRenderDistance(ptr: *anyopaque) i32 { - return cast(ptr).render_distance; - } - - fn setRenderDistance(ptr: *anyopaque, distance: i32) void { - cast(ptr).render_distance = distance; - } - - fn getHorizonDistance(ptr: *anyopaque) i32 { - return cast(ptr).horizon_distance; - } - - fn setHorizonDistance(ptr: *anyopaque, distance: i32) void { - cast(ptr).horizon_distance = distance; - } - - fn isLODRenderingEnabled(ptr: *anyopaque) bool { - return cast(ptr).lod_rendering_enabled; - } - - fn toggleLODRendering(ptr: *anyopaque) bool { - const self = cast(ptr); - self.lod_rendering_enabled = !self.lod_rendering_enabled; - return self.lod_rendering_enabled; - } - - fn getChunkStateCounts(ptr: *anyopaque) world_core.ChunkStateCounts { - _ = ptr; - return .{ .total = 5, .renderable = 4, .dirty = 1 }; - } - - fn isStartupBusy(ptr: *anyopaque) bool { - _ = ptr; - return false; - } - - fn getWorldStateData(ptr: *anyopaque) world_core.WorldStateData { - _ = ptr; - return .{ .generator_name = "mock-generator", .seed = 99, .gen_queue = 1, .mesh_queue = 2, .upload_queue = 3 }; - } - - fn lpvWorld(ptr: *anyopaque) @import("engine-rhi").ILPVWorld { - _ = ptr; - return undefined; - } - - fn graphicsRenderView(ptr: *anyopaque) @import("engine-rhi").IWorldRenderView { - _ = ptr; - return undefined; - } - - fn getGpuMeshDispatch(ptr: *anyopaque) world_mod.GpuMeshDispatch { - _ = ptr; - return .{ .dispatch_fn = null, .dispatch_ctx = null }; - } - - fn isGpuCullingEnabled(ptr: *anyopaque) bool { - _ = ptr; - return false; - } -}; - -fn makeStorageOnlyWorld(allocator: std.mem.Allocator) world_mod.World { - // Storage-only fixture for real World methods that do not touch renderer, - // streamer, RHI, generator, save manager, or LOD. Tests must tear it down - // with deinitStorageOnlyWorld(), not World.deinit(). - var world = world_mod.World{ - .storage = world_meshing.ChunkStorage.init(allocator), - .streamer = undefined, - .renderer = undefined, - .allocator = allocator, - .generator = undefined, - .render_distance = 8, - .lod_chunk_render_radius_limit = 8, - .horizon_distance = 512, - .rhi = undefined, - .paused = false, - .safe_mode = false, - .safe_render_distance = 8, - .lod = null, - .lod_enabled = false, - .save_manager = null, - .gpu_block_buffer = null, - .mutation = undefined, - .lpv_grid_builder = undefined, - }; - world.mutation = WorldMutationCoordinator.init(&world.storage, allocator, null, false); - world.lpv_grid_builder = LpvGridBuilder.init(&world.storage); - return world; + try std.testing.expect(renderer_mod.isWithinChunkRenderRadius(10, 0, 0, 0, 10)); + try std.testing.expect(!renderer_mod.isWithinChunkRenderRadius(10, 10, 0, 0, 10)); } -fn deinitStorageOnlyWorld(world: *world_mod.World) void { - if (world.save_manager) |sm| { - sm.deinit(); - world.save_manager = null; - } - // Mutation and LPV grid builder currently borrow storage/allocator only. - // If either starts owning resources, this fixture teardown should grow with it. - world.storage.deinitWithoutRHI(); +test "full-detail MDI capacity rejects overflow before visibility truncation" { + try std.testing.expect(renderer_mod.hasMdiCapacity(16_383, 49_149, 3)); + try std.testing.expect(!renderer_mod.hasMdiCapacity(16_384, 0, 1)); } -const OrchestrationRecorder = struct { - next_order: u32 = 0, - begin_order: u32 = 999, - update_order: u32 = 999, - autosave_order: u32 = 999, -}; - -const MockRendererSubsystem = struct { - recorder: ?*OrchestrationRecorder = null, - begin_count: u32 = 0, - render_count: u32 = 0, - last_render_distance: i32 = 0, - last_render_lod: bool = false, - last_layer: RenderLayer = .all, - - pub fn beginFrame(self: *@This()) void { - self.begin_count += 1; - if (self.recorder) |recorder| { - recorder.begin_order = recorder.next_order; - recorder.next_order += 1; +test "world orchestration delegates the active chunk distance to the renderer" { + const Renderer = struct { + calls: u32 = 0, + distance: i32 = 0, + fn render(self: *@This(), _: math.Mat4, _: math.Vec3, distance: i32, _: renderer_mod.RenderLayer) void { + self.calls += 1; + self.distance = distance; } - } - - pub fn render(self: *@This(), view_proj: math.Mat4, camera_pos: math.Vec3, render_distance: i32, lod_manager: ?*@import("world-lod").LODManager, render_lod: bool, layer: RenderLayer) void { - _ = view_proj; - _ = camera_pos; - _ = lod_manager; - self.render_count += 1; - self.last_render_distance = render_distance; - self.last_render_lod = render_lod; - self.last_layer = layer; - } -}; - -const MockStreamerSubsystem = struct { - recorder: ?*OrchestrationRecorder = null, - update_count: u32 = 0, - active_render_distance: i32 = 18, - last_dt: f32 = 0.0, - last_player_pos: math.Vec3 = math.Vec3.zero, - - pub fn updateFrame(self: *@This(), player_pos: math.Vec3, dt: f32) !void { - self.update_count += 1; - self.last_player_pos = player_pos; - self.last_dt = dt; - if (self.recorder) |recorder| { - recorder.update_order = recorder.next_order; - recorder.next_order += 1; + }; + const Streamer = struct { + fn getActiveRenderDistance(_: *@This()) i32 { + return 24; } - } - - pub fn getActiveRenderDistance(self: *@This()) i32 { - return self.active_render_distance; - } -}; - -const MockAutoSaveWorld = struct { - recorder: *OrchestrationRecorder, - autosave_count: u32 = 0, - - pub fn checkAutoSave(self: *@This()) void { - self.autosave_count += 1; - self.recorder.autosave_order = self.recorder.next_order; - self.recorder.next_order += 1; - } -}; - -test "IWorld forwards simulation lifecycle calls" { - var mock = MockWorld{}; - const world = mock.iface(); - - try world.update(math.Vec3.zero, 0.016); - world.deinit(); - - try testing.expectEqual(@as(u32, 1), mock.update_count); - try testing.expectEqual(@as(u32, 1), mock.deinit_count); -} - -test "IWorld forwards render passes" { - var mock = MockWorld{}; - const world = mock.iface(); - const mat = math.Mat4.identity(); - - world.render(mat, math.Vec3.zero, true); - world.renderOpaque(mat, math.Vec3.zero, true); - world.renderFluid(mat, math.Vec3.zero, false); - - try testing.expectEqual(@as(u32, 1), mock.render_count); - try testing.expectEqual(@as(u32, 1), mock.render_opaque_count); - try testing.expectEqual(@as(u32, 1), mock.render_fluid_count); -} - -test "IWorld forwards block reads and writes" { - var mock = MockWorld{}; - const world = mock.iface(); - - try world.setBlock(1, 2, 3, .stone); - - try testing.expectEqual(@as(u32, 1), mock.set_block_count); - try testing.expectEqual(world_core.BlockType.stone, world.getBlock(1, 2, 3)); -} - -test "IWorld forwards save and pause state" { - var mock = MockWorld{}; - const world = mock.iface(); - - try world.enableSaveManager("/tmp/save", "world"); - world.pauseGeneration(); - - try testing.expectEqual(@as(u32, 1), mock.save_count); - try testing.expectEqual(@as(u32, 1), mock.pause_count); - try testing.expect(world.isPaused()); - try testing.expectEqual(@as(usize, 2), world.takeSaveFailureWarningCount()); -} - -test "IWorld telemetry forwards stats and distances" { - var mock = MockWorld{}; - const world = mock.iface(); - - world.setRenderDistance(24); - world.setHorizonDistance(1024); - const render_stats = world.getRenderStats(); - const stats = world.getStats(); - - try testing.expectEqual(@as(i32, 24), world.getRenderDistance()); - try testing.expectEqual(@as(i32, 1024), world.getHorizonDistance()); - try testing.expectEqual(@as(u32, 2), render_stats.chunks_rendered); - try testing.expectEqual(@as(u64, 200), stats.total_vertices); -} - -test "IWorld forwards LOD rendering toggle" { - var mock = MockWorld{}; - const world = mock.iface(); - - try testing.expect(world.isLODRenderingEnabled()); - try testing.expect(!world.toggleLODRendering()); - try testing.expect(!world.isLODRenderingEnabled()); -} - -test "IWorld exposes role interface views" { - var mock = MockWorld{}; - const world = mock.iface(); - - try world.simulation().setBlock(0, 0, 0, .dirt); - world.telemetry().setRenderDistance(18); - world.renderView().render(math.Mat4.identity(), math.Vec3.zero, false); - - try testing.expectEqual(world_core.BlockType.dirt, world.simulation().getBlock(0, 0, 0)); - try testing.expectEqual(@as(i32, 18), world.telemetry().getRenderDistance()); - try testing.expectEqual(@as(u32, 1), mock.render_count); -} - -test "IWorld telemetry view forwards debug and state data" { - var mock = MockWorld{}; - const telemetry = mock.iface().telemetry(); - - const chunk_counts = telemetry.getChunkStateCounts(); - const state = telemetry.getWorldStateData(); - const light = telemetry.getDebugLightInfo(1, 2, 3).?; - - try testing.expectEqualStrings("mock-generator", telemetry.getGeneratorName()); - try testing.expectEqual(@as(u32, 5), chunk_counts.total); - try testing.expectEqual(@as(u32, 4), chunk_counts.renderable); - try testing.expectEqual(@as(u64, 99), state.seed); - try testing.expectEqual(@as(u4, 15), light.sky); -} - -test "WorldOrchestration update orders renderer streamer and autosave" { - var recorder = OrchestrationRecorder{}; - var renderer = MockRendererSubsystem{ .recorder = &recorder }; - var streamer = MockStreamerSubsystem{ .recorder = &recorder }; - var save_owner = MockAutoSaveWorld{ .recorder = &recorder }; - const player_pos = math.Vec3.init(1.0, 2.0, 3.0); - - try world_mod.WorldOrchestration.update(&renderer, &streamer, &save_owner, player_pos, 0.25); - - try testing.expectEqual(@as(u32, 1), renderer.begin_count); - try testing.expectEqual(@as(u32, 1), streamer.update_count); - try testing.expectEqual(@as(u32, 1), save_owner.autosave_count); - try testing.expectEqual(@as(u32, 0), recorder.begin_order); - try testing.expectEqual(@as(u32, 1), recorder.update_order); - try testing.expectEqual(@as(u32, 2), recorder.autosave_order); - try testing.expectEqual(@as(f32, 0.25), streamer.last_dt); - try testing.expectEqual(player_pos.x, streamer.last_player_pos.x); -} - -test "WorldOrchestration render delegates active distance layer and LOD flag" { - var renderer = MockRendererSubsystem{}; - var streamer = MockStreamerSubsystem{ .active_render_distance = 24 }; - - world_mod.WorldOrchestration.render(&renderer, &streamer, null, true, math.Mat4.identity(), math.Vec3.zero, true, .terrain); - - try testing.expectEqual(@as(u32, 1), renderer.render_count); - try testing.expectEqual(@as(i32, 24), renderer.last_render_distance); - try testing.expect(renderer.last_render_lod); - try testing.expectEqual(RenderLayer.terrain, renderer.last_layer); - - world_mod.WorldOrchestration.render(&renderer, &streamer, null, true, math.Mat4.identity(), math.Vec3.zero, false, .fluid); - - try testing.expectEqual(@as(u32, 2), renderer.render_count); - try testing.expect(!renderer.last_render_lod); - try testing.expectEqual(RenderLayer.fluid, renderer.last_layer); -} - -test "World storage facade returns air for unloaded and out-of-bounds blocks" { - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - - try testing.expectEqual(world_core.BlockType.air, world.getBlock(0, 64, 0)); - try testing.expectEqual(world_core.BlockType.air, world.getBlock(0, -1, 0)); - try testing.expectEqual(world_core.BlockType.air, world.getBlock(0, 256, 0)); -} - -test "World setBlock ignores unloaded chunks" { - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - - try world.setBlock(17, 42, -1, .stone); - - try testing.expectEqual(world_core.BlockType.air, world.getBlock(17, 42, -1)); - try testing.expectEqual(@as(usize, 0), world.storage.count()); -} - -test "World setBlock ignores out-of-bounds y without creating chunks" { - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - - try world.setBlock(0, -1, 0, .dirt); - try world.setBlock(0, 256, 0, .dirt); - - try testing.expectEqual(@as(usize, 0), world.storage.count()); -} - -test "World getChunkStateCounts reports storage telemetry" { - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - - const missing = try world.storage.getOrCreate(0, 0); - missing.chunk.state = .missing; - missing.chunk.dirty = false; - const renderable = try world.storage.getOrCreate(1, 0); - renderable.chunk.state = .renderable; - renderable.chunk.dirty = true; - - const counts = world.getChunkStateCounts(); - - try testing.expectEqual(@as(u32, 2), counts.total); - try testing.expectEqual(@as(u32, 1), counts.missing); - try testing.expectEqual(@as(u32, 1), counts.renderable); - try testing.expectEqual(@as(u32, 1), counts.dirty); -} - -test "World saveAllModifiedChunks and loadChunkFromSave round-trip chunk data" { - var tmp_dir = testing.tmpDir(.{}); - defer tmp_dir.cleanup(); - const base_path = try tmp_dir.dir.realpathAlloc(testing.allocator, "."); - defer testing.allocator.free(base_path); - const save_path = try std.fmt.allocPrint(testing.allocator, "{s}/world_save_roundtrip", .{base_path}); - defer testing.allocator.free(save_path); - - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - world.save_manager = try SaveManager.init(testing.allocator, save_path, "world_save_roundtrip", 1234, "test-generator"); - - const data = try world.storage.getOrCreate(2, -1); - data.chunk.generated = true; - data.chunk.setBlock(4, 64, 5, .stone); - data.chunk.setBiome(4, 5, .forest); - - world.saveAllModifiedChunks(); - - try testing.expect(!data.chunk.modified); - var loaded = world_core.Chunk.init(2, -1); - try testing.expectEqual(@import("world-persistence").LoadResult.success, world.loadChunkFromSave(2, -1, &loaded)); - try testing.expectEqual(world_core.BlockType.stone, loaded.getBlock(4, 64, 5)); - try testing.expectEqual(world_core.BiomeId.forest, loaded.getBiome(4, 5)); -} - -test "World loaded map capture reflects foliage placement and removal" { - var world = makeStorageOnlyWorld(testing.allocator); - defer deinitStorageOnlyWorld(&world); - const data = try world.storage.getOrCreate(0, 0); - data.chunk.generated = true; - data.chunk.setBlock(3, 64, 5, .grass); - data.chunk.setBlock(3, 72, 5, .leaves); - - var map = try worldgen.WorldMap.init(testing.allocator, 16, 16); - defer map.deinit(); - const first = try map.createLoadedSurfaceOverlay(); - defer first.deinit(); - try world.captureLoadedMapSurface(first, 8, 8, 1, 16, 16); - const canopy = first.sample(3, 5).?; - try testing.expectEqual(world_core.BlockType.leaves, canopy.block); - try testing.expectEqual(@as(i16, 72), canopy.height); - - data.chunk.setBlock(3, 72, 5, .air); - const second = try map.createLoadedSurfaceOverlay(); - defer second.deinit(); - try world.captureLoadedMapSurface(second, 8, 8, 1, 16, 16); - const exposed = second.sample(3, 5).?; - try testing.expectEqual(world_core.BlockType.grass, exposed.block); - try testing.expectEqual(@as(i16, 64), exposed.height); + }; + var renderer = Renderer{}; + var streamer = Streamer{}; + world.WorldOrchestration.render(&renderer, &streamer, math.Mat4.identity(), math.Vec3.zero, .terrain); + try std.testing.expectEqual(@as(u32, 1), renderer.calls); + try std.testing.expectEqual(@as(i32, 24), renderer.distance); } diff --git a/modules/world-runtime/src/world_renderer.zig b/modules/world-runtime/src/world_renderer.zig index 9f39332b..f3d41a8d 100644 --- a/modules/world-runtime/src/world_renderer.zig +++ b/modules/world-runtime/src/world_renderer.zig @@ -14,8 +14,6 @@ const ResourceManager = rhi_mod.ResourceManager; const RenderContext = rhi_mod.RenderContext; const IDeviceQuery = rhi_mod.IDeviceQuery; const IDeviceTiming = rhi_mod.IDeviceTiming; -const LODManager = @import("world-lod").LODManager; -const LODRenderLayer = @import("world-lod").LODRenderLayer; const math = @import("engine-math"); const Vec3 = math.Vec3; const Mat4 = math.Mat4; @@ -333,30 +331,11 @@ pub const WorldRenderer = struct { self.allocator.destroy(self); } - pub fn render(self: *WorldRenderer, view_proj: Mat4, camera_pos: Vec3, render_distance: i32, lod_manager: ?*LODManager, render_lod: bool, layer: RenderLayer) void { + pub fn render(self: *WorldRenderer, view_proj: Mat4, camera_pos: Vec3, render_distance: i32, layer: RenderLayer) void { if (layer != .fluid) { self.last_render_stats = .{ .gpu_culling = self.use_gpu_culling }; } - const detail_render_radius = if (lod_manager) |mgr| @min(render_distance, mgr.config.getChunkRenderRadius()) else render_distance; - - if (render_lod) { - if (lod_manager) |lod_mgr| { - const lod_render_limit = lod_mgr.getHorizonRenderRadius(); - if (layer != .fluid) { - self.timing.beginPassTiming("LODTerrainPass"); - lod_mgr.renderFrame(self.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkTerrainReadyForHandoff, @ptrCast(self.storage), true, lod_render_limit, detail_render_radius, LODRenderLayer.terrain); - self.timing.endPassTiming("LODTerrainPass"); - } - if (layer != .terrain and parseEnabledEnv(getenv("ZIGCRAFT_LOD_WATER"), true)) { - self.timing.beginPassTiming("LODWaterPass"); - lod_mgr.renderFrame(self.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkTerrainReadyForHandoff, @ptrCast(self.storage), true, lod_render_limit, detail_render_radius, LODRenderLayer.fluid); - self.timing.endPassTiming("LODWaterPass"); - } - } - } - - // LOD rendering uses a separate descriptor set path; switch back before drawing full chunks. - self.render_ctx.setInstanceBuffer(0); + const detail_render_radius = render_distance; self.storage.chunks_mutex.lockShared(); defer self.storage.chunks_mutex.unlockShared(); @@ -389,9 +368,6 @@ pub const WorldRenderer = struct { var total_vertices: u64 = 0; for (self.visible_chunks.items) |data| { - // LOD projection is not proof that GPU culling emitted replacement - // geometry. Keep detail as the fail-open fallback; the LOD shader - // mask owns overlap with the contiguous detailed area. if (layer != .fluid) { self.last_render_stats.chunks_rendered += 1; } @@ -429,9 +405,6 @@ pub const WorldRenderer = struct { self.instance_data.appendAssumeCapacity(.{ .model = model, - .mask_radius = 0, - .lod_fade = 1.0, - .padding = .{ 0, 0 }, }); if (layer != .fluid) { @@ -503,7 +476,7 @@ pub const WorldRenderer = struct { } fn drawChunkDirect(self: *WorldRenderer, data: *ChunkData, model: Mat4, layer: RenderLayer, count_vertices: bool) u64 { - self.render_ctx.setModelMatrix(model, Vec3.one, 0); + self.render_ctx.setModelMatrix(model, Vec3.one); var total_vertices: u64 = 0; if (layer != .fluid) { @@ -759,11 +732,11 @@ pub const WorldRenderer = struct { if (self.instance_data.items.len >= MAX_MDI_CHUNKS or self.draw_commands.items.len + command_count > MAX_MDI_CHUNKS * 3) { log.log.warn("Shadow MDI: batch capacity reached, drawing overflow chunk directly", .{}); if (data.render.mesh.solid_allocation) |alloc| { - self.render_ctx.setModelMatrix(model, Vec3.one, 0); + self.render_ctx.setModelMatrix(model, Vec3.one); self.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } if (data.render.mesh.cutout_allocation) |alloc| { - self.render_ctx.setModelMatrix(model, Vec3.one, 0); + self.render_ctx.setModelMatrix(model, Vec3.one); self.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } continue; @@ -772,9 +745,6 @@ pub const WorldRenderer = struct { const instance_idx: u32 = @intCast(self.instance_data.items.len); self.instance_data.append(self.allocator, .{ .model = model, - .mask_radius = 0, - .lod_fade = 1.0, - .padding = .{ 0, 0 }, }) catch continue; if (data.render.mesh.solid_allocation) |alloc| { @@ -797,11 +767,11 @@ pub const WorldRenderer = struct { } if (data.render.mesh.solid_allocation) |alloc| { - self.render_ctx.setModelMatrix(model, Vec3.one, 0); + self.render_ctx.setModelMatrix(model, Vec3.one); self.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } if (data.render.mesh.cutout_allocation) |alloc| { - self.render_ctx.setModelMatrix(model, Vec3.one, 0); + self.render_ctx.setModelMatrix(model, Vec3.one); self.render_ctx.drawOffset(self.vertex_allocator.buffer, alloc.count, .triangles, alloc.offset); } } diff --git a/modules/world-runtime/src/world_streamer.zig b/modules/world-runtime/src/world_streamer.zig index 07bf87f3..af67dd5f 100644 --- a/modules/world-runtime/src/world_streamer.zig +++ b/modules/world-runtime/src/world_streamer.zig @@ -59,8 +59,6 @@ const JobQueue = engine_core.JobQueue; const WorkerPool = engine_core.WorkerPool; const Generator = @import("world-worldgen").Generator; const GlobalVertexAllocator = world_meshing.GlobalVertexAllocator; -const LODManager = @import("world-lod").LODManager; -const ChunkResolver = @import("world-lod").ChunkResolver; const TextureAtlas = @import("engine-assets").TextureAtlas; const log = @import("engine-core").log; const SaveManager = @import("world-persistence").SaveManager; @@ -69,15 +67,53 @@ const GpuMesher = @import("gpu_mesher.zig").GpuMesher; const WorldMutationCoordinator = @import("world_mutation.zig").WorldMutationCoordinator; const GpuAccelerationCoordinator = @import("gpu_acceleration_coordinator.zig").GpuAccelerationCoordinator; const ChunkQueueCoordinator = @import("chunk_queue_coordinator.zig").ChunkQueueCoordinator; -const LODStreamingCoordinator = @import("world-lod").LODStreamingCoordinator; -const QueueStats = @import("world-lod").lod_streaming_coordinator.QueueStats; const build_options = @import("world_runtime_options"); /// Buffer distance beyond render_distance for chunk unloading. /// Prevents thrashing when player moves near chunk boundaries. // const CHUNK_UNLOAD_BUFFER: i32 = 1; -pub const PlayerMovement = @import("world-lod").lod_streaming_coordinator.PlayerMovement; +pub const PlayerMovement = struct { + dir_x: f32 = 0, + dir_z: f32 = 0, + speed: f32 = 0, + last_pos: Vec3 = Vec3.init(0, 0, 0), + has_velocity: bool = false, + + pub fn update(self: *PlayerMovement, pos: Vec3, dt: f32) bool { + if (dt <= 0.001) return false; + const dx = pos.x - self.last_pos.x; + const dz = pos.z - self.last_pos.z; + self.last_pos = pos; + const distance = @sqrt(dx * dx + dz * dz); + self.speed = distance / dt; + if (self.speed < 2.0) { + self.has_velocity = false; + return false; + } + const old_x = self.dir_x; + const old_z = self.dir_z; + self.dir_x = dx / distance; + self.dir_z = dz / distance; + self.has_velocity = true; + return old_x * self.dir_x + old_z * self.dir_z < 0.707; + } + + pub fn priorityWeight(self: PlayerMovement, chunk_dx: i32, chunk_dz: i32) f32 { + if (!self.has_velocity) return 1.0; + const dx: f32 = @floatFromInt(chunk_dx); + const dz: f32 = @floatFromInt(chunk_dz); + const distance = @sqrt(dx * dx + dz * dz); + if (distance < 0.001) return 0.5; + return 1.0 - (dx * self.dir_x + dz * self.dir_z) / distance * 0.5; + } +}; + +pub const QueueStats = struct { + gen_queue: usize, + mesh_queue: usize, + upload_queue: usize, +}; pub const WorldStreamer = struct { allocator: std.mem.Allocator, @@ -90,7 +126,12 @@ pub const WorldStreamer = struct { gen_pool: *WorkerPool, mesh_pool: *WorkerPool, queue_coordinator: ChunkQueueCoordinator, - lod_coordinator: LODStreamingCoordinator, + render_distance: i32, + active_render_distance: i32, + startup_stream_radius: i32, + player_movement: PlayerMovement = .{}, + last_pc: struct { x: i32, z: i32 } = .{ .x = 0, .z = 0 }, + startup_mesh_finalized: bool = false, gpu_acceleration: GpuAccelerationCoordinator, vertex_allocator: *GlobalVertexAllocator, @@ -115,26 +156,16 @@ pub const WorldStreamer = struct { const MAX_GEN_WORKERS = 10; const MIN_MESH_WORKERS = 2; const MAX_MESH_WORKERS = 8; - const MIN_LOD_WORKERS = 1; - const MAX_LOD_WORKERS = 8; - - pub fn init(allocator: std.mem.Allocator, storage: *ChunkStorage, generator: Generator, atlas: *const TextureAtlas, render_distance: i32, lod_enabled: bool, vertex_allocator: *GlobalVertexAllocator, max_uploads_per_frame: usize, gpu_block_buffer: ?*GpuBlockBuffer, gpu_mesher: ?*GpuMesher) !*WorldStreamer { + const STARTUP_RADIUS_INITIAL = 3; + const STARTUP_RADIUS_STEP = 2; + const STARTUP_PREFETCH_RINGS = 2; + pub fn init(allocator: std.mem.Allocator, storage: *ChunkStorage, generator: Generator, atlas: *const TextureAtlas, render_distance: i32, vertex_allocator: *GlobalVertexAllocator, max_uploads_per_frame: usize, gpu_block_buffer: ?*GpuBlockBuffer, gpu_mesher: ?*GpuMesher) !*WorldStreamer { const streamer = try allocator.create(WorldStreamer); errdefer allocator.destroy(streamer); - // Reserve LOD capacity from the same CPU budget as full-detail work - // while preserving the foreground pool caps. This leaves capacity for - // the main thread and graphics driver. - // This keeps horizon generation responsive without creating two pools - // that each try to saturate every core. const cpu_count = std.Thread.getCpuCount() catch MIN_GEN_WORKERS + MIN_MESH_WORKERS; const total_budget = @max(@as(usize, 5), cpu_count -| 1); - const requested_lod_workers = if (lod_enabled) - std.math.clamp(cpu_count / 2, MIN_LOD_WORKERS, MAX_LOD_WORKERS) - else - 0; - const lod_worker_reserve = @min(requested_lod_workers, total_budget -| (MIN_GEN_WORKERS + MIN_MESH_WORKERS)); - const foreground_budget = total_budget - lod_worker_reserve; + const foreground_budget = total_budget; // Generation is the dominant startup stage after coalescing boundary // remeshes, so allocate roughly two thirds of the foreground workers // to generation and the remainder to meshing. @@ -168,7 +199,9 @@ pub const WorldStreamer = struct { .gen_pool = undefined, .mesh_pool = undefined, .queue_coordinator = undefined, - .lod_coordinator = LODStreamingCoordinator.init(render_distance), + .render_distance = @max(render_distance, 2), + .active_render_distance = @min(@max(render_distance, 2), STARTUP_RADIUS_INITIAL), + .startup_stream_radius = @min(@max(render_distance, 2), STARTUP_RADIUS_INITIAL), .gpu_acceleration = GpuAccelerationCoordinator.init(gpu_block_buffer, gpu_mesher), .vertex_allocator = vertex_allocator, .last_diag_generated = 0, @@ -179,7 +212,7 @@ pub const WorldStreamer = struct { streamer.queue_coordinator = try ChunkQueueCoordinator.init(allocator, storage, generator, atlas, gen_queue, mesh_queue, vertex_allocator, max_uploads_per_frame, &streamer.gpu_acceleration); errdefer streamer.queue_coordinator.deinit(); - log.log.info("WorldStreamer workers: gen={} mesh={} lod_reserve={} (cpu={})", .{ gen_worker_count, mesh_worker_count, lod_worker_reserve, cpu_count }); + log.log.info("WorldStreamer workers: gen={} mesh={} (cpu={})", .{ gen_worker_count, mesh_worker_count, cpu_count }); streamer.gen_pool = try WorkerPool.init(allocator, gen_worker_count, gen_queue, &streamer.queue_coordinator, ChunkQueueCoordinator.processGenJob); errdefer streamer.gen_pool.deinit(); @@ -223,26 +256,31 @@ pub const WorldStreamer = struct { if (paused) { self.queue_coordinator.resetPausedChunks(); } else { - self.lod_coordinator.forceRescan(); + self.has_scanned_missing_chunks = false; self.has_processed_unloads = false; } } pub fn setRenderDistance(self: *WorldStreamer, distance: i32) void { - _ = self.lod_coordinator.setRenderDistance(distance); + const previous_active = self.active_render_distance; + self.render_distance = @max(distance, 2); + self.startup_stream_radius = @min(self.render_distance, @max(previous_active, STARTUP_RADIUS_INITIAL)); + self.active_render_distance = self.startup_stream_radius; + self.startup_mesh_finalized = false; + self.has_scanned_missing_chunks = false; } pub fn getActiveRenderDistance(self: *const WorldStreamer) i32 { - return self.lod_coordinator.getActiveRenderDistance(); + return self.active_render_distance; } pub fn isStartupBusy(self: *WorldStreamer, target_render_dist: i32) bool { - if (self.lod_coordinator.isStartupBusy(self.getStats(), target_render_dist)) return true; + if (self.active_render_distance < target_render_dist) return true; if (self.queue_coordinator.hasInFlightWork()) return true; if (!self.has_scanned_missing_chunks) return true; - const pc_x = self.lod_coordinator.last_pc.x; - const pc_z = self.lod_coordinator.last_pc.z; + const pc_x = self.last_pc.x; + const pc_z = self.last_pc.z; self.storage.chunks_mutex.lockShared(); defer self.storage.chunks_mutex.unlockShared(); @@ -326,20 +364,6 @@ pub const WorldStreamer = struct { } } - pub fn setLODManager(self: *WorldStreamer, lod_manager: ?*LODManager) void { - self.lod_coordinator.setLODManager(lod_manager); - self.queue_coordinator.setLODManager(lod_manager); - if (lod_manager) |mgr| { - // Resolver lets deferred ingestions fetch resident chunks. The - // returned pointer is consumed synchronously within the manager's - // main-thread update, so it does not need pinning. - mgr.setChunkResolver(.{ - .ptr = self.storage, - .resolve_fn = resolveChunkFromStorage, - }); - } - } - pub fn setSaveManager(self: *WorldStreamer, sm: ?*SaveManager) void { self.save_manager = sm; self.queue_coordinator.setSaveManager(sm); @@ -389,7 +413,7 @@ pub const WorldStreamer = struct { }; self.queue_coordinator.processUploads(); const pc = worldToChunkFromFloat(player_pos.x, player_pos.z); - const unload_render_dist = self.lod_coordinator.targetRenderDistance(); + const unload_render_dist = self.render_distance; const needs_unload_scan = !self.has_processed_unloads or self.last_unload_pc_x != pc.chunk_x or self.last_unload_pc_z != pc.chunk_z or @@ -462,44 +486,26 @@ pub const WorldStreamer = struct { if (renderable_no_alloc > 0) { log.log.warn(" {} chunks renderable with no allocations (max 3 recovery attempts)", .{renderable_no_alloc}); } - - if (self.lod_coordinator.lod_manager) |lod_mgr| { - const lod0_r = lod_mgr.config.getChunkRenderRadius(); - const pc_x = self.lod_coordinator.last_pc.x; - const pc_z = self.lod_coordinator.last_pc.z; - const check_dirs = [_][2]i32{ .{ lod0_r, 0 }, .{ -lod0_r, 0 }, .{ 0, lod0_r }, .{ 0, -lod0_r } }; - var renderable_at_boundary: u32 = 0; - var missing_at_boundary: u32 = 0; - for (check_dirs) |dir| { - const cx = pc_x + dir[0]; - const cz = pc_z + dir[1]; - if (self.storage.chunks.get(.{ .x = cx, .z = cz })) |data| { - if (data.chunk.state == .renderable or data.render.mesh.solid_allocation != null) { - renderable_at_boundary += 1; - } else { - if (missing_at_boundary == 0) { - log.log.warn(" BOUNDARY_CHUNK: ({},{}) state={} (expected renderable)", .{ cx, cz, data.chunk.state }); - } - missing_at_boundary += 1; - } - } else { - if (missing_at_boundary == 0) { - log.log.warn(" BOUNDARY_CHUNK: ({},{}) NOT IN STORAGE", .{ cx, cz }); - } - missing_at_boundary += 1; - } - } - if (missing_at_boundary > 0) { - log.log.warn(" BOUNDARY: {}/4 chunks at LOD0 boundary (r={}) are NOT renderable", .{ missing_at_boundary, lod0_r }); - } - } } } fn updateStreaming(self: *WorldStreamer, player_pos: Vec3, dt: f32) !void { self.gpu_acceleration.refreshForceCpuMeshing(self.frame_counter, self.storage); - const frame = self.lod_coordinator.beginFrame(self.storage, self.gen_queue, self.mesh_queue, player_pos, dt, self.frame_counter); + _ = self.player_movement.update(player_pos, dt); + const pc = worldToChunkFromFloat(player_pos.x, player_pos.z); + const moved = pc.chunk_x != self.last_pc.x or pc.chunk_z != self.last_pc.z; + self.last_pc = .{ .x = pc.chunk_x, .z = pc.chunk_z }; + if (moved) { + self.gen_queue.updatePlayerPos(pc.chunk_x, pc.chunk_z) catch {}; + self.mesh_queue.updatePlayerPos(pc.chunk_x, pc.chunk_z) catch {}; + } + if (self.startup_stream_radius < self.render_distance and self.frame_counter % 10 == 0 and !self.queue_coordinator.hasInFlightWork()) { + self.startup_stream_radius = @min(self.render_distance, self.startup_stream_radius + STARTUP_RADIUS_STEP); + } + self.active_render_distance = self.startup_stream_radius; + const stream_dist = @min(self.render_distance, self.active_render_distance + STARTUP_RADIUS_STEP * STARTUP_PREFETCH_RINGS); + const frame = .{ .pc_x = pc.chunk_x, .pc_z = pc.chunk_z, .stream_dist = stream_dist, .movement = self.player_movement }; self.queue_coordinator.setView(frame.pc_x, frame.pc_z, frame.stream_dist); if (self.frame_counter % 600 == 0) { @@ -527,11 +533,9 @@ pub const WorldStreamer = struct { self.last_missing_scan_render_dist = frame.stream_dist; } self.queue_coordinator.processChunkStates(frame.pc_x, frame.pc_z, frame.stream_dist, self.frame_counter); - self.lod_coordinator.updateLOD(player_pos, self.storage); - - if (!self.lod_coordinator.startup_mesh_finalized and !self.isStartupBusy(self.lod_coordinator.render_distance)) { + if (!self.startup_mesh_finalized and !self.isStartupBusy(self.render_distance)) { self.finalizeStartupArea(frame.pc_x, frame.pc_z, 1); - self.lod_coordinator.startup_mesh_finalized = true; + self.startup_mesh_finalized = true; } } @@ -617,7 +621,7 @@ pub const WorldStreamer = struct { fn processUnloads(self: *WorldStreamer, player_pos: Vec3) !void { const pc = worldToChunkFromFloat(player_pos.x, player_pos.z); - const render_dist_unload = self.lod_coordinator.targetRenderDistance(); + const render_dist_unload = self.render_distance; const unload_distance = @as(i128, render_dist_unload) + CHUNK_UNLOAD_BUFFER; const unload_dist_sq = unload_distance * unload_distance; @@ -663,33 +667,6 @@ pub const WorldStreamer = struct { }; const chunk = unload_candidate.chunk; - // Do not acquire the LOD manager while holding chunks_mutex: LOD - // visibility takes the locks in the opposite order. The pin keeps - // this authoritative snapshot alive through edit ingestion/save. - var defer_unload = false; - if (chunk.generated) { - if (self.lod_coordinator.lod_manager) |manager| { - const retain_pending = manager.isInRange(key.x, key.z); - const pending_mask = manager.flushEditedChunkForUnload(key.x, key.z, chunk, retain_pending); - defer_unload = retain_pending and pending_mask != 0; - } - } - - // Keep an edited full-detail source resident while visible LOD - // levels are still in flight. Once the player leaves the LOD - // horizon, the persisted chunk becomes the durable repair source. - if (defer_unload) { - self.storage.chunks_mutex.lock(); - if (self.storage.chunks.get(key)) |data| { - if (&data.chunk == chunk and data.chunk.state == .unloading) { - data.chunk.state = unload_candidate.previous_state; - } - } - chunk.unpin(); - self.storage.chunks_mutex.unlock(); - continue; - } - const save_enqueued = chunk.modified and chunk.generated and self.save_manager != null; if (save_enqueued) self.save_manager.?.enqueueSave(chunk); @@ -712,8 +689,8 @@ pub const WorldStreamer = struct { } fn logMissingChunkDiagnostic(self: *WorldStreamer, pc_x: i32, pc_z: i32) void { - const target_render_dist = self.lod_coordinator.targetRenderDistance(); - const render_dist = @min(self.getActiveRenderDistance(), target_render_dist); + const target_render_dist = self.render_distance; + const render_dist = self.render_distance; var counts = [_]u32{0} ** 10; var missing_keys = std.ArrayListUnmanaged(ChunkKey).empty; @@ -832,15 +809,3 @@ fn cleanupMutationLighting(raw_context: *anyopaque) void { const allocator = context.allocator; allocator.destroy(context); } - -/// ChunkResolver callback: look up a resident, generated chunk by coordinate. -/// The returned pointer is only valid for synchronous use on the main thread -/// (it is not pinned); the LOD manager consumes it immediately within update(). -fn resolveChunkFromStorage(ptr: *anyopaque, cx: i32, cz: i32) ?*const world_core.Chunk { - const storage: *ChunkStorage = @ptrCast(@alignCast(ptr)); - storage.chunks_mutex.lockShared(); - defer storage.chunks_mutex.unlockShared(); - const entry = storage.chunks.get(ChunkKey{ .x = cx, .z = cz }) orelse return null; - if (!entry.chunk.generated) return null; - return &entry.chunk; -} diff --git a/modules/world-worldgen/src/root.zig b/modules/world-worldgen/src/root.zig index 402d6bf9..c2fa8f07 100644 --- a/modules/world-worldgen/src/root.zig +++ b/modules/world-worldgen/src/root.zig @@ -65,7 +65,6 @@ pub const findGeneratorIndex = registry.findGeneratorIndex; pub const createGenerator = registry.createGenerator; pub const createGeneratorById = registry.createGeneratorById; pub const FeatureFocus = region.FeatureFocus; -pub const GenerationOptions = worldgen_api.GenerationOptions; pub const Generator = worldgen_api.Generator; pub const GeneratorInfo = worldgen_api.GeneratorInfo; pub const HeightParams = height_sampler.HeightParams; diff --git a/modules/worldgen-api/src/root.zig b/modules/worldgen-api/src/root.zig index 72c8a100..94b322f4 100644 --- a/modules/worldgen-api/src/root.zig +++ b/modules/worldgen-api/src/root.zig @@ -2,8 +2,6 @@ const std = @import("std"); const world_core = @import("world-core"); const Chunk = world_core.Chunk; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; const BiomeId = world_core.BiomeId; pub const RegionMood = enum { @@ -74,31 +72,6 @@ pub const MapSample = struct { ridge_mask: f32, }; -pub const GenerationOptions = struct { - lod_level: LODLevel = .lod0, - enable_caves: bool = true, - enable_worm_caves: bool = true, - enable_decorations: bool = true, - enable_ores: bool = true, - enable_lighting: bool = true, - octave_reduction: u8 = 0, - skip_biome_blending: bool = false, - - pub fn fromLOD(lod: LODLevel) GenerationOptions { - const level = @intFromEnum(lod); - return .{ - .lod_level = lod, - .enable_caves = level <= 1, - .enable_worm_caves = level == 0, - .enable_decorations = level <= 1, - .enable_ores = level == 0, - .enable_lighting = level == 0, - .octave_reduction = @intCast(level), - .skip_biome_blending = level > 0, - }; - } -}; - pub const GeneratorInfo = struct { name: []const u8, description: []const u8, @@ -112,8 +85,6 @@ pub const Generator = struct { pub const VTable = struct { generate: *const fn (ptr: *anyopaque, chunk: *Chunk, stop_flag: ?*const bool) WorldgenError!void, - generateHeightmapOnly: *const fn (ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void, - maybeRecenterCache: *const fn (ptr: *anyopaque, player_x: i32, player_z: i32) bool, getSeed: *const fn (ptr: *anyopaque) u64, getRegionInfo: *const fn (ptr: *anyopaque, world_x: i32, world_z: i32) RegionInfo, getColumnInfo: *const fn (ptr: *anyopaque, wx: f32, wz: f32) ColumnInfo, @@ -129,14 +100,6 @@ pub const Generator = struct { try self.vtable.generate(self.ptr, chunk, stop_flag); } - pub fn generateHeightmapOnly(self: Generator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - self.vtable.generateHeightmapOnly(self.ptr, data, region_x, region_z, lod_level, stop_flag); - } - - pub fn maybeRecenterCache(self: Generator, player_x: i32, player_z: i32) bool { - return self.vtable.maybeRecenterCache(self.ptr, player_x, player_z); - } - pub fn getSeed(self: Generator) u64 { return self.vtable.getSeed(self.ptr); } @@ -149,7 +112,7 @@ pub const Generator = struct { return self.vtable.getColumnInfo(self.ptr, wx, wz); } - /// Returns map/LOD-oriented column data when the generator supports it. + /// Returns map-oriented column data when the generator supports it. /// Generators without a reduced sampler retain their existing behavior. pub fn getColumnInfoReduced(self: Generator, wx: f32, wz: f32, reduction: u8) ColumnInfo { const reduced = self.vtable.getColumnInfoReduced orelse return self.getColumnInfo(wx, wz); diff --git a/modules/worldgen-flat/src/root.zig b/modules/worldgen-flat/src/root.zig index da94c9c3..5aa2741a 100644 --- a/modules/worldgen-flat/src/root.zig +++ b/modules/worldgen-flat/src/root.zig @@ -10,8 +10,6 @@ const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; const BlockType = world_core.BlockType; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; const LightingComputer = @import("worldgen-common").LightingComputer; pub const FlatWorldGenerator = struct { @@ -19,7 +17,6 @@ pub const FlatWorldGenerator = struct { allocator: std.mem.Allocator, const FLAT_HEIGHT: i32 = 64; - const GRASS_COLOR: u32 = 0xFF40A040; pub const INFO = GeneratorInfo{ .name = "Flat World", @@ -65,40 +62,6 @@ pub const FlatWorldGenerator = struct { chunk.dirty = true; } - pub fn generateHeightmapOnly(self: *const FlatWorldGenerator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - _ = self; - _ = region_x; - _ = region_z; - _ = lod_level; - _ = stop_flag; - - @memset(data.heightmap, @floatFromInt(FLAT_HEIGHT)); - @memset(data.biomes, .plains); - @memset(data.top_blocks, .grass); - @memset(data.colors, GRASS_COLOR); - @memset(data.material_layers, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }); - @memset(data.water, world_core.LODWaterState.empty); - @memset(data.lighting, world_core.LODLightingHint.daylight); - @memset(data.vegetation, world_core.LODVegetationHint.empty); - - if (data.hasVerticalSpans()) { - var gz: u32 = 0; - while (gz < data.width) : (gz += 1) { - var gx: u32 = 0; - while (gx < data.width) : (gx += 1) { - data.setGeneratedColumn(gx, gz, @floatFromInt(FLAT_HEIGHT), .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, GRASS_COLOR, world_core.LODWaterState.empty, world_core.LODLightingHint.daylight, world_core.LODVegetationHint.empty); - } - } - } - } - - pub fn maybeRecenterCache(self: *FlatWorldGenerator, player_x: i32, player_z: i32) bool { - _ = self; - _ = player_x; - _ = player_z; - return false; - } - pub fn getSeed(self: *const FlatWorldGenerator) u64 { return self.seed; } @@ -121,8 +84,6 @@ pub const FlatWorldGenerator = struct { const VTABLE = Generator.VTable{ .generate = generateWrapper, - .generateHeightmapOnly = generateHeightmapOnlyWrapper, - .maybeRecenterCache = maybeRecenterCacheWrapper, .getSeed = getSeedWrapper, .getRegionInfo = getRegionInfoWrapper, .getColumnInfo = getColumnInfoWrapper, @@ -135,16 +96,6 @@ pub const FlatWorldGenerator = struct { try self.generate(chunk, stop_flag); } - fn generateHeightmapOnlyWrapper(ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - const self: *FlatWorldGenerator = @ptrCast(@alignCast(ptr)); - self.generateHeightmapOnly(data, region_x, region_z, lod_level, stop_flag); - } - - fn maybeRecenterCacheWrapper(ptr: *anyopaque, player_x: i32, player_z: i32) bool { - const self: *FlatWorldGenerator = @ptrCast(@alignCast(ptr)); - return self.maybeRecenterCache(player_x, player_z); - } - fn getSeedWrapper(ptr: *anyopaque) u64 { const self: *FlatWorldGenerator = @ptrCast(@alignCast(ptr)); return self.getSeed(); diff --git a/modules/worldgen-overworld-v2/src/lod_sampling.zig b/modules/worldgen-overworld-v2/src/lod_sampling.zig deleted file mode 100644 index 4f911b00..00000000 --- a/modules/worldgen-overworld-v2/src/lod_sampling.zig +++ /dev/null @@ -1,266 +0,0 @@ -const std = @import("std"); -const world_core = @import("world-core"); -const biomes = @import("biomes.zig"); -const block_colors = @import("block_colors.zig"); -const climate = @import("climate.zig"); -const terrain_shape = @import("terrain_shape.zig"); -const trees = @import("trees.zig"); -const util = @import("util.zig"); - -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; - -const ColumnSample = struct { - terrain_height: i32, - base_height: i32, - biome: BiomeId, - is_river: bool, - is_ocean: bool, - temperature: f32, - humidity: f32, - continentalness: f32, -}; - -const ClassifiedLODSample = struct { - world_x: i32, - world_z: i32, - terrain_height: f32, - terrain_height_i: i32, - biome: BiomeId, - surface_block: BlockType, - render_water_surface: bool, -}; - -const RepresentativeLODColumn = struct { - height: f32, - biome: BiomeId, - layers: world_core.LODMaterialLayers, - color: u32, - water: world_core.LODWaterState, - lighting: world_core.LODLightingHint, - vegetation: world_core.LODVegetationHint, -}; - -pub fn generateHeightmapOnly(self: anytype, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - if (data.width < 2) return; - const region_size_i: i32 = @intCast(world_core.regionSizeBlocks(lod_level)); - const region_size_f: f32 = @floatFromInt(region_size_i); - const grid_max: f32 = @floatFromInt(data.width - 1); - const cell_span = region_size_f / grid_max; - const world_x = util.chunkWorldOffset(region_x, 0, region_size_i); - const world_z = util.chunkWorldOffset(region_z, 0, region_size_i); - - var gz: u32 = 0; - while (gz < data.width) : (gz += 1) { - if (stop_flag) |sf| if (sf.load(.acquire)) return; - var gx: u32 = 0; - while (gx < data.width) : (gx += 1) { - const wx_f = @as(f32, @floatFromInt(world_x)) + (@as(f32, @floatFromInt(gx)) / grid_max) * region_size_f; - const wz_f = @as(f32, @floatFromInt(world_z)) + (@as(f32, @floatFromInt(gz)) / grid_max) * region_size_f; - const sample = sampleRepresentativeLODColumn(self, wx_f, wz_f, cell_span); - data.setGeneratedColumn(gx, gz, sample.height, sample.biome, sample.layers, sample.color, sample.water, sample.lighting, sample.vegetation); - } - } -} - -fn sampleRepresentativeLODColumn(self: anytype, wx: f32, wz: f32, cell_span: f32) RepresentativeLODColumn { - // Sample the full cell footprint so shorelines retain fractional coverage - // rather than snapping every coarse cell fully to land or water. - const sample_offsets = [_]f32{ -1.0, 0.0, 1.0 }; - const sample_radius = @min(cell_span * 0.5, 48.0); - var classified_samples: [sample_offsets.len * sample_offsets.len]ClassifiedLODSample = undefined; - var classified_count: usize = 0; - - var block_counts = [_]u32{0} ** world_core.MAX_BLOCK_TYPES; - var biome_counts = [_]u32{0} ** 256; - var color_r: u32 = 0; - var color_g: u32 = 0; - var color_b: u32 = 0; - var terrain_height_sum: f32 = 0.0; - var terrain_min: f32 = std.math.floatMax(f32); - var terrain_max: f32 = -std.math.floatMax(f32); - var water_depth_sum: f32 = 0.0; - var water_samples: u32 = 0; - var total_samples: u32 = 0; - - for (sample_offsets) |oz| { - for (sample_offsets) |ox| { - const sample = classifyLODSample(self, wx + ox * sample_radius, wz + oz * sample_radius); - classified_samples[classified_count] = sample; - classified_count += 1; - const block_index = @intFromEnum(sample.surface_block); - if (block_index < block_counts.len) block_counts[block_index] += 1; - biome_counts[@intFromEnum(sample.biome)] += 1; - - const color = block_colors.colorForBiome(sample.biome, sample.surface_block); - color_r += (color >> 16) & 0xFF; - color_g += (color >> 8) & 0xFF; - color_b += color & 0xFF; - terrain_height_sum += sample.terrain_height; - terrain_min = @min(terrain_min, sample.terrain_height); - terrain_max = @max(terrain_max, sample.terrain_height); - total_samples += 1; - - if (sample.render_water_surface) { - water_samples += 1; - water_depth_sum += @floatFromInt(@max(self.params.sea_level - sample.terrain_height_i, 0)); - } - } - } - - const sample_count = @max(total_samples, 1); - const water_coverage = @as(f32, @floatFromInt(water_samples)) / @as(f32, @floatFromInt(sample_count)); - const render_water_surface = water_coverage >= 0.45; - const dominant_biome = dominantBiome(biome_counts); - const dominant_block = dominantBlock(block_counts); - const center_sample = classified_samples[classified_samples.len / 2]; - const surface_block: BlockType = if (render_water_surface) .water else if (center_sample.render_water_surface) dominant_block else center_sample.surface_block; - const avg_height = terrain_height_sum / @as(f32, @floatFromInt(sample_count)); - const terrain_range = @max(terrain_max - terrain_min, 0.0); - const center_height = center_sample.terrain_height; - const height_blend: f32 = if (terrain_range > 24.0 and center_height > avg_height) 0.82 else 0.68; - const land_height = center_height * height_blend + avg_height * (1.0 - height_blend); - const vegetation = if (render_water_surface) - world_core.LODVegetationHint.empty - else - lodVegetationHintFromSamples(self, classified_samples[0..classified_count], wx, wz); - const avg_color = packAverageColor(color_r, color_g, color_b, sample_count); - - return .{ - .height = if (render_water_surface) @floatFromInt(self.params.sea_level) else land_height, - .biome = dominant_biome, - .layers = .{ - .surface = surface_block, - .subsurface = block_colors.fillerBlock(dominant_biome, render_water_surface), - .foundation = .stone, - }, - .color = avg_color, - .water = if (render_water_surface) .{ - .is_surface = true, - .surface_height = @floatFromInt(self.params.sea_level), - .depth = if (water_samples == 0) 0.0 else water_depth_sum / @as(f32, @floatFromInt(water_samples)), - .coverage = water_coverage, - } else world_core.LODWaterState.empty, - .lighting = .{ - .sky_light = 15, - .block_light = 0, - .ambient_occlusion = if (render_water_surface) 0.92 else 1.0, - }, - .vegetation = vegetation, - }; -} - -fn classifyLODSample(self: anytype, wx: f32, wz: f32) ClassifiedLODSample { - const wx_i = util.floorToI32(wx); - const wz_i = util.floorToI32(wz); - const sample = sampleLODColumn(self, wx_i, wz_i); - const render_water_surface = sample.terrain_height < self.params.sea_level; - const surface_block: BlockType = if (render_water_surface) .water else block_colors.surfaceBlock(sample.biome, sample.terrain_height, self.params.sea_level, false); - - return .{ - .world_x = wx_i, - .world_z = wz_i, - .terrain_height = @floatFromInt(sample.terrain_height), - .terrain_height_i = sample.terrain_height, - .biome = sample.biome, - .surface_block = surface_block, - .render_water_surface = render_water_surface, - }; -} - -fn sampleLODColumn(self: anytype, wx: i32, wz: i32) ColumnSample { - const base_height = util.floorToI32(terrain_shape.baseTerrainLevelAtPoint(self, wx, wz)); - const terrain_height = sampleTerrainHeightForLOD(self, wx, wz); - const climate_sample = climate.sampleClimate(self, wx, wz); - const river = terrain_shape.isRiverColumn(self, wx, wz) and terrain_height >= self.params.sea_level - 18 and terrain_height <= self.params.sea_level + 1; - const biome = biomes.selectBiome(self, wx, wz, terrain_height, river, climate_sample.temperature, climate_sample.humidity); - const continentalness = std.math.clamp((@as(f32, @floatFromInt(terrain_height - self.params.sea_level)) + 56.0) / 150.0, 0.0, 1.0); - return .{ - .terrain_height = terrain_height, - .base_height = base_height, - .biome = biome, - .is_river = river, - .is_ocean = terrain_height < self.params.sea_level - 2, - .temperature = climate_sample.temperature, - .humidity = climate_sample.humidity, - .continentalness = continentalness, - }; -} - -/// Uses the same highest-solid terrain estimate as full chunk generation. -/// The grounded-only estimate stops at the first air gap and can miss elevated -/// mountain terrain above an underwater base, turning distant islands into sea. -pub fn sampleTerrainHeightForLOD(self: anytype, wx: i32, wz: i32) i32 { - const base_height = util.floorToI32(terrain_shape.baseTerrainLevelAtPoint(self, wx, wz)); - return terrain_shape.estimateTerrainHeight(self, wx, wz, base_height); -} - -fn lodVegetationHintFromSamples(self: anytype, samples: []const ClassifiedLODSample, center_wx: f32, center_wz: f32) world_core.LODVegetationHint { - var tree_count: u32 = 0; - var total_columns: u32 = 0; - var height_sum: f32 = 0.0; - var offset_x_sum: f32 = 0.0; - var offset_z_sum: f32 = 0.0; - var best_shape: ?trees.TreeShape = null; - - for (samples) |sample| { - total_columns += 1; - if (sample.terrain_height_i <= self.params.sea_level) continue; - if (sample.surface_block != .grass and sample.surface_block != .dirt and sample.surface_block != .snow_block and sample.surface_block != .sand) continue; - - const shape = trees.treeForColumn(self, sample.biome, sample.world_x, sample.world_z) orelse continue; - tree_count += 1; - height_sum += trees.treeHeightForShape(shape); - offset_x_sum += @as(f32, @floatFromInt(sample.world_x)) - center_wx; - offset_z_sum += @as(f32, @floatFromInt(sample.world_z)) - center_wz; - if (best_shape == null) best_shape = shape; - } - - if (tree_count == 0) return world_core.LODVegetationHint.empty; - - const area = @as(f32, @floatFromInt(@max(total_columns, 1))); - const sampled_coverage = std.math.clamp(@as(f32, @floatFromInt(tree_count)) / area, 0.0, 1.0); - const blocks = trees.treeBlocksForShape(best_shape orelse .oak); - - return .{ - .tree_coverage = sampled_coverage, - .avg_tree_height = height_sum / @as(f32, @floatFromInt(tree_count)), - .offset_x = offset_x_sum / @as(f32, @floatFromInt(tree_count)), - .offset_z = offset_z_sum / @as(f32, @floatFromInt(tree_count)), - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; -} - -fn dominantBlock(counts: [world_core.MAX_BLOCK_TYPES]u32) BlockType { - var best_index: usize = @intFromEnum(BlockType.grass); - var best_count: u32 = 0; - for (counts, 0..) |count, i| { - if (count > best_count) { - best_index = i; - best_count = count; - } - } - return @enumFromInt(best_index); -} - -fn dominantBiome(counts: [256]u32) BiomeId { - var best_index: usize = @intFromEnum(BiomeId.plains); - var best_count: u32 = 0; - for (counts, 0..) |count, i| { - if (count > best_count) { - best_index = i; - best_count = count; - } - } - return @enumFromInt(best_index); -} - -fn packAverageColor(r_sum: u32, g_sum: u32, b_sum: u32, count: u32) u32 { - const r = r_sum / count; - const g = g_sum / count; - const b = b_sum / count; - return (r << 16) | (g << 8) | b; -} diff --git a/modules/worldgen-overworld-v2/src/root.zig b/modules/worldgen-overworld-v2/src/root.zig index 4359fc10..a3ca305c 100644 --- a/modules/worldgen-overworld-v2/src/root.zig +++ b/modules/worldgen-overworld-v2/src/root.zig @@ -13,7 +13,6 @@ const biomes = @import("biomes.zig"); const block_colors = @import("block_colors.zig"); const caves = @import("caves.zig"); const climate = @import("climate.zig"); -const lod_sampling = @import("lod_sampling.zig"); const noise = @import("noise.zig"); const terrain_shape = @import("terrain_shape.zig"); const trees = @import("trees.zig"); @@ -26,8 +25,6 @@ const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; const BlockType = world_core.BlockType; const BiomeId = world_core.BiomeId; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; const Generator = worldgen_api.Generator; const GeneratorInfo = worldgen_api.GeneratorInfo; const ColumnInfo = worldgen_api.ColumnInfo; @@ -61,8 +58,6 @@ pub const OverworldV2Generator = struct { pub const INFO = GeneratorInfo{ .name = "Overworld V2", .description = "Luanti v7-style terrain with ridges, mountains, rivers, and cave noise.", - // Version 3 invalidates LOD source caches generated with the grounded - // height estimator, which could classify elevated islands as ocean. .version = 3, }; @@ -292,17 +287,6 @@ pub const OverworldV2Generator = struct { return biomes.isBeachColumn(self, wx, wz, height); } - pub fn generateHeightmapOnly(self: *const OverworldV2Generator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - lod_sampling.generateHeightmapOnly(self, data, region_x, region_z, lod_level, stop_flag); - } - - pub fn maybeRecenterCache(self: *OverworldV2Generator, player_x: i32, player_z: i32) bool { - _ = self; - _ = player_x; - _ = player_z; - return false; - } - pub fn getSeed(self: *const OverworldV2Generator) u64 { return self.seed; } @@ -352,8 +336,6 @@ pub const OverworldV2Generator = struct { const VTABLE = Generator.VTable{ .generate = generateWrapper, - .generateHeightmapOnly = generateHeightmapOnlyWrapper, - .maybeRecenterCache = maybeRecenterCacheWrapper, .getSeed = getSeedWrapper, .getRegionInfo = getRegionInfoWrapper, .getColumnInfo = getColumnInfoWrapper, @@ -366,16 +348,6 @@ pub const OverworldV2Generator = struct { try self.generate(chunk, stop_flag); } - fn generateHeightmapOnlyWrapper(ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - const self: *const OverworldV2Generator = @ptrCast(@alignCast(ptr)); - self.generateHeightmapOnly(data, region_x, region_z, lod_level, stop_flag); - } - - fn maybeRecenterCacheWrapper(ptr: *anyopaque, player_x: i32, player_z: i32) bool { - const self: *OverworldV2Generator = @ptrCast(@alignCast(ptr)); - return self.maybeRecenterCache(player_x, player_z); - } - fn getSeedWrapper(ptr: *anyopaque) u64 { const self: *OverworldV2Generator = @ptrCast(@alignCast(ptr)); return self.getSeed(); @@ -484,60 +456,6 @@ test "overworld-v2 places ground vegetation" { try std.testing.expect(vegetation_blocks > 0); } -test "overworld-v2 generates representative LOD data" { - var gen = OverworldV2Generator.init(12345, std.testing.allocator); - var data = try LODSimplifiedData.init(std.testing.allocator, .lod3); - defer data.deinit(); - - gen.generateHeightmapOnly(&data, 0, 0, .lod3, null); - - var filled_columns: u32 = 0; - var material_columns: u32 = 0; - for (data.heightmap, 0..) |height, i| { - if (height > 0.0) filled_columns += 1; - if (data.top_blocks[i] != .air and data.colors[i] != 0) material_columns += 1; - try std.testing.expect(height <= @as(f32, @floatFromInt(CHUNK_SIZE_Y - 1))); - } - - try std.testing.expect(filled_columns > 0); - try std.testing.expect(material_columns > 0); -} - -test "overworld-v2 LOD height sampling retains elevated terrain above underwater bases" { - var gen = OverworldV2Generator.init(12345, std.testing.allocator); - const sea_level = gen.params.sea_level; - var found_elevated_island = false; - - var wz: i32 = -2048; - scan: while (wz <= 2048) : (wz += 32) { - var wx: i32 = -2048; - while (wx <= 2048) : (wx += 32) { - const base_height = util.floorToI32(terrain_shape.baseTerrainLevelAtPoint(&gen, wx, wz)); - const grounded_height = terrain_shape.estimateGroundedTerrainHeight(&gen, wx, wz, base_height); - const full_height = terrain_shape.estimateTerrainHeight(&gen, wx, wz, base_height); - if (grounded_height < sea_level and full_height >= sea_level) { - try std.testing.expectEqual(full_height, lod_sampling.sampleTerrainHeightForLOD(&gen, wx, wz)); - found_elevated_island = true; - break :scan; - } - } - } - - try std.testing.expect(found_elevated_island); -} - -test "overworld-v2 LOD tree density covers forest variants" { - try std.testing.expect(trees.treeDensityForBiome(.forest) > 0.5); - try std.testing.expect(trees.treeDensityForBiome(.birch_forest) > 0.5); - try std.testing.expect(trees.treeDensityForBiome(.dark_forest) > trees.treeDensityForBiome(.forest)); - try std.testing.expect(trees.treeDensityForBiome(.old_growth_taiga) > trees.treeDensityForBiome(.taiga)); - try std.testing.expect(trees.treeDensityForBiome(.bamboo_jungle) > 0.5); - try std.testing.expect(trees.treeDensityForBiome(.plains) >= 0.1); - try std.testing.expectEqual(TreeShape.birch, trees.defaultTreeShapeForBiome(.birch_forest)); - try std.testing.expectEqual(TreeShape.spruce, trees.defaultTreeShapeForBiome(.old_growth_taiga)); - try std.testing.expectEqual(TreeShape.jungle, trees.defaultTreeShapeForBiome(.bamboo_jungle)); -} - test "overworld-v2 propagates lighting allocation failure" { var failing = std.testing.FailingAllocator.init(std.testing.allocator, .{ .fail_index = 0 }); var gen = OverworldV2Generator.init(0, failing.allocator()); diff --git a/modules/worldgen-overworld/src/biome.zig b/modules/worldgen-overworld/src/biome.zig index c749b138..e023ab2c 100644 --- a/modules/worldgen-overworld/src/biome.zig +++ b/modules/worldgen-overworld/src/biome.zig @@ -6,7 +6,7 @@ //! - biome_registry.zig — Data definitions, types, BIOME_REGISTRY //! - biome_selector.zig — Selection algorithms (Voronoi, score-based, blended) //! - biome_edge_detector.zig — Edge detection, transition rules -//! - biome_color_provider.zig — Color lookup for LOD/minimap +//! - biome_color_provider.zig — Color lookup for maps //! - biome_source.zig — BiomeSource unified interface // ============================================================================ @@ -90,7 +90,6 @@ pub const selectBiomeBlended = biome_selector.selectBiomeBlended; pub const selectBiomeWithRiverBlended = biome_selector.selectBiomeWithRiverBlended; pub const selectBiomeWithConstraints = biome_selector.selectBiomeWithConstraints; pub const selectBiomeWithConstraintsAndRiver = biome_selector.selectBiomeWithConstraintsAndRiver; -pub const selectBiomeSimple = biome_selector.selectBiomeSimple; // ============================================================================ // Functions from biome_color_provider.zig diff --git a/modules/worldgen-overworld/src/biome_color_provider.zig b/modules/worldgen-overworld/src/biome_color_provider.zig index d92521f5..28a38895 100644 --- a/modules/worldgen-overworld/src/biome_color_provider.zig +++ b/modules/worldgen-overworld/src/biome_color_provider.zig @@ -1,8 +1,8 @@ -//! Biome color lookup for LOD rendering and minimap. +//! Biome color lookup for maps. const BiomeId = @import("biome_registry.zig").BiomeId; -/// Get biome color for LOD rendering (packed RGB) +/// Get a packed RGB biome color. /// Colors adjusted to match textured output (grass/surface colors) pub fn getBiomeColor(biome_id: BiomeId) u32 { return switch (biome_id) { diff --git a/modules/worldgen-overworld/src/biome_selector.zig b/modules/worldgen-overworld/src/biome_selector.zig index 49f91dc9..46e2c8c1 100644 --- a/modules/worldgen-overworld/src/biome_selector.zig +++ b/modules/worldgen-overworld/src/biome_selector.zig @@ -1,4 +1,4 @@ -//! Biome selection algorithms: Voronoi, score-based, blended, and LOD-simplified. +//! Biome selection algorithms: Voronoi, score-based, and blended. //! All selection functions are pure — they read from the registry but have no side effects. const std = @import("std"); @@ -305,17 +305,3 @@ pub fn selectBiomeWithConstraintsAndRiver(climate: ClimateParams, structural: St } return selectBiomeWithConstraints(climate, structural); } - -// ============================================================================ -// LOD-optimized Biome Functions (Issue #114) -// ============================================================================ - -/// Simplified biome selection for LOD2+ paths that only have climate params. -/// -/// Keep this aligned with the full registry scoring path so distant biome color -/// and material choices use the same climate, elevation, continentalness, -/// ruggedness, and ridge signals as LOD0 where structural height/slope filters -/// are not available. -pub fn selectBiomeSimple(climate: ClimateParams) BiomeId { - return selectBiome(climate); -} diff --git a/modules/worldgen-overworld/src/biome_selector_tests.zig b/modules/worldgen-overworld/src/biome_selector_tests.zig index e1a4570a..d0c4f30f 100644 --- a/modules/worldgen-overworld/src/biome_selector_tests.zig +++ b/modules/worldgen-overworld/src/biome_selector_tests.zig @@ -25,7 +25,6 @@ const selectBiomeBlended = selector.selectBiomeBlended; const selectBiomeWithRiverBlended = selector.selectBiomeWithRiverBlended; const selectBiomeWithConstraints = selector.selectBiomeWithConstraints; const selectBiomeWithConstraintsAndRiver = selector.selectBiomeWithConstraintsAndRiver; -const selectBiomeSimple = selector.selectBiomeSimple; const computeClimateParams = selector.computeClimateParams; const BiomeSelection = selector.BiomeSelection; const BIOME_POINTS = registry.BIOME_POINTS; @@ -646,159 +645,6 @@ test "selectBiomeWithRiverBlended returns normal selection when river not active try testing.expect(selection.primary != .river); } -// ============================================================================ -// selectBiomeSimple (LOD) Tests -// ============================================================================ - -test "selectBiomeSimple returns ocean for low continentalness" { - const params = ClimateParams{ - .temperature = 0.5, - .humidity = 0.5, - .elevation = 0.2, - .continentalness = 0.15, - .ruggedness = 0.3, - }; - const biome = selectBiomeSimple(params); - try testing.expect(biome == .deep_ocean or biome == .ocean); -} - -test "selectBiomeSimple does not return ocean above sea level" { - const params = ClimateParams{ - .temperature = 0.5, - .humidity = 0.5, - .elevation = 0.31, - .continentalness = 0.15, - .ruggedness = 0.3, - }; - const biome = selectBiomeSimple(params); - try testing.expect(biome != .deep_ocean); - try testing.expect(biome != .ocean); - try testing.expect(biome != .frozen_ocean); - try testing.expect(biome != .cold_ocean); - try testing.expect(biome != .warm_ocean); -} - -test "selectBiomeSimple returns deep_ocean for very low continentalness" { - const params = ClimateParams{ - .temperature = 0.5, - .humidity = 0.5, - .elevation = 0.1, - .continentalness = 0.1, - .ruggedness = 0.3, - }; - try testing.expectEqual(BiomeId.deep_ocean, selectBiomeSimple(params)); -} - -test "selectBiomeSimple returns cold ocean variants for cold low continentalness" { - const frozen = ClimateParams{ - .temperature = 0.1, - .humidity = 0.5, - .elevation = 0.1, - .continentalness = 0.25, - .ruggedness = 0.3, - }; - try testing.expectEqual(BiomeId.frozen_ocean, selectBiomeSimple(frozen)); - - const cold = ClimateParams{ - .temperature = 0.25, - .humidity = 0.5, - .elevation = 0.1, - .continentalness = 0.25, - .ruggedness = 0.3, - }; - try testing.expectEqual(BiomeId.cold_ocean, selectBiomeSimple(cold)); -} - -test "selectBiomeSimple returns different biomes for hot vs cold" { - const hot = ClimateParams{ - .temperature = 0.9, - .humidity = 0.5, - .elevation = 0.4, - .continentalness = 0.6, - .ruggedness = 0.3, - }; - const cold = ClimateParams{ - .temperature = 0.1, - .humidity = 0.5, - .elevation = 0.4, - .continentalness = 0.6, - .ruggedness = 0.3, - }; - try testing.expect(selectBiomeSimple(hot) != selectBiomeSimple(cold)); -} - -test "selectBiomeSimple returns desert for hot dry conditions" { - const params = ClimateParams{ - .temperature = 0.9, - .humidity = 0.1, - .elevation = 0.5, - .continentalness = 0.6, - .ruggedness = 0.2, - }; - try testing.expectEqual(BiomeId.desert, selectBiomeSimple(params)); -} - -test "selectBiomeSimple hot humid returns non-desert" { - const params = ClimateParams{ - .temperature = 0.9, - .humidity = 0.8, - .elevation = 0.5, - .continentalness = 0.6, - .ruggedness = 0.3, - }; - const biome = selectBiomeSimple(params); - try testing.expect(biome != .desert); - try testing.expect(biome != .snow_tundra); - try testing.expect(biome != .deep_ocean); - try testing.expect(biome != .ocean); -} - -test "selectBiomeSimple matches constrained selector for LOD-compatible samples" { - const Case = struct { - name: []const u8, - climate: ClimateParams, - structural: StructuralParams, - }; - - const cases = [_]Case{ - .{ - .name = "deep ocean", - .climate = .{ .temperature = 0.5, .humidity = 0.5, .elevation = 0.2, .continentalness = 0.10, .ruggedness = 0.1 }, - .structural = .{ .height = 48, .slope = 1, .continentalness = 0.10, .ridge_mask = 0.0 }, - }, - .{ - .name = "plains", - .climate = .{ .temperature = 0.5, .humidity = 0.45, .elevation = 0.35, .continentalness = 0.60, .ruggedness = 0.1 }, - .structural = .{ .height = 70, .slope = 2, .continentalness = 0.60, .ridge_mask = 0.1 }, - }, - .{ - .name = "jagged peaks", - .climate = .{ .temperature = 0.32, .humidity = 0.45, .elevation = 0.85, .continentalness = 0.85, .ruggedness = 0.85, .ridge_mask = 0.7 }, - .structural = .{ .height = 150, .slope = 18, .continentalness = 0.85, .ridge_mask = 0.7 }, - }, - }; - - for (cases) |case| { - errdefer std.debug.print("failed LOD selector consistency case: {s}\n", .{case.name}); - try testing.expectEqual(selectBiomeWithConstraints(case.climate, case.structural), selectBiomeSimple(case.climate)); - } -} - -test "selectBiomeSimple uses terrain signals beyond heat and humidity" { - const warm_dry = ClimateParams{ - .temperature = 0.82, - .humidity = 0.25, - .elevation = 0.40, - .continentalness = 0.80, - .ruggedness = 0.10, - }; - var rugged = warm_dry; - rugged.ruggedness = 0.75; - - try testing.expectEqual(BiomeId.savanna, selectBiomeSimple(warm_dry)); - try testing.expectEqual(BiomeId.badlands, selectBiomeSimple(rugged)); -} - // ============================================================================ // BiomeSelection Structure Tests // ============================================================================ diff --git a/modules/worldgen-overworld/src/biome_source.zig b/modules/worldgen-overworld/src/biome_source.zig index 5bb65ea9..96ce40c4 100644 --- a/modules/worldgen-overworld/src/biome_source.zig +++ b/modules/worldgen-overworld/src/biome_source.zig @@ -113,12 +113,6 @@ pub const BiomeSource = struct { }; } - /// Simplified biome selection for LOD levels - pub fn selectBiomeSimplified(self: *const BiomeSource, climate: ClimateParams) BiomeId { - _ = self; - return selector.selectBiomeSimple(climate); - } - /// Check if a position is ocean based on continentalness pub fn isOcean(self: *const BiomeSource, continentalness: f32) bool { return continentalness < self.params.ocean_threshold; diff --git a/modules/worldgen-overworld/src/gen_region.zig b/modules/worldgen-overworld/src/gen_region.zig index d05b08c7..66b4ef4d 100644 --- a/modules/worldgen-overworld/src/gen_region.zig +++ b/modules/worldgen-overworld/src/gen_region.zig @@ -286,215 +286,3 @@ pub const GenRegionCache = struct { return .{ .x = rx, .z = rz }; } }; - -// ============================================================================ -// Climate Cache for LOD Generation (Issue #114) -// ============================================================================ - -/// Coarse-resolution climate data for fast LOD terrain generation. -/// Caches temperature, humidity, and continentalness at 64-block resolution. -pub const ClimateCache = struct { - /// Cell size in blocks (coarse sampling) - pub const CELL_SIZE: u32 = 64; - - /// Cache grid dimensions - pub const GRID_SIZE: u32 = 32; // 32x32 grid = 2048x2048 blocks coverage - - /// Cached climate values - const CacheCell = struct { - temperature: f32, - humidity: f32, - continentalness: f32, - valid: bool, - }; - - cells: [GRID_SIZE * GRID_SIZE]CacheCell, - origin_x: i32, // World X of grid origin - origin_z: i32, // World Z of grid origin - - pub fn init() ClimateCache { - return .{ - .cells = [_]CacheCell{.{ - .temperature = 0, - .humidity = 0, - .continentalness = 0, - .valid = false, - }} ** (GRID_SIZE * GRID_SIZE), - .origin_x = 0, - .origin_z = 0, - }; - } - - /// Recenter the cache grid around a new origin. - /// Invalidates all cells. - pub fn recenter(self: *ClimateCache, center_x: i32, center_z: i32) void { - const half_size: i32 = @intCast((GRID_SIZE * CELL_SIZE) / 2); - const aligned_center_x = @divFloor(center_x, @as(i32, @intCast(CELL_SIZE))) * @as(i32, @intCast(CELL_SIZE)); - const aligned_center_z = @divFloor(center_z, @as(i32, @intCast(CELL_SIZE))) * @as(i32, @intCast(CELL_SIZE)); - self.origin_x = aligned_center_x - half_size; - self.origin_z = aligned_center_z - half_size; - - // Invalidate all cells - for (&self.cells) |*cell| { - cell.valid = false; - } - } - - /// Check if a world position is within the cache grid - pub fn contains(self: *const ClimateCache, world_x: i32, world_z: i32) bool { - const grid_extent: i32 = @intCast(GRID_SIZE * CELL_SIZE); - return world_x >= self.origin_x and - world_x < self.origin_x + grid_extent and - world_z >= self.origin_z and - world_z < self.origin_z + grid_extent; - } - - /// Get grid cell index for world position, or null if out of bounds - fn getCellIndex(self: *const ClimateCache, world_x: i32, world_z: i32) ?usize { - if (!self.contains(world_x, world_z)) return null; - - const local_x: u32 = @intCast(world_x - self.origin_x); - const local_z: u32 = @intCast(world_z - self.origin_z); - const cell_x = local_x / CELL_SIZE; - const cell_z = local_z / CELL_SIZE; - - return cell_x + cell_z * GRID_SIZE; - } - - /// Try to get cached climate values at a world position - pub fn get(self: *const ClimateCache, world_x: i32, world_z: i32) ?struct { temp: f32, humid: f32, cont: f32 } { - const idx = self.getCellIndex(world_x, world_z) orelse return null; - const cell = &self.cells[idx]; - if (!cell.valid) return null; - return .{ .temp = cell.temperature, .humid = cell.humidity, .cont = cell.continentalness }; - } - - /// Store climate values at a world position - pub fn put(self: *ClimateCache, world_x: i32, world_z: i32, temperature: f32, humidity: f32, continentalness: f32) void { - const idx = self.getCellIndex(world_x, world_z) orelse return; - self.cells[idx] = .{ - .temperature = temperature, - .humidity = humidity, - .continentalness = continentalness, - .valid = true, - }; - } - - /// Get or compute climate values using provided noise functions - pub fn getOrCompute( - self: *ClimateCache, - world_x: i32, - world_z: i32, - computeFn: *const fn (x: f32, z: f32) struct { temp: f32, humid: f32, cont: f32 }, - ) struct { temp: f32, humid: f32, cont: f32 } { - if (self.get(world_x, world_z)) |cached| { - return cached; - } - - // Compute and cache - const result = computeFn(@floatFromInt(world_x), @floatFromInt(world_z)); - self.put(world_x, world_z, result.temp, result.humid, result.cont); - return result; - } -}; - -// ============================================================================ -// Classification Cache for LOD Generation (Issue #119 Phase 2) -// ============================================================================ - -const world_class = @import("world_class.zig"); -pub const ClassCell = world_class.ClassCell; -pub const ContinentalZone = world_class.ContinentalZone; -pub const SurfaceType = world_class.SurfaceType; - -/// Classification cache for LOD terrain generation. -/// Stores authoritative biome/surface/water decisions at coarse resolution. -/// LOD levels sample from this cache instead of recomputing. -pub const ClassificationCache = struct { - /// Cell size in blocks (must match world_class.CELL_SIZE) - pub const CELL_SIZE: u32 = world_class.CELL_SIZE; // 8 blocks - - /// Cache grid dimensions (covers 2048x2048 blocks = 128x128 chunks) - pub const GRID_SIZE: u32 = 256; - - /// Optional cell - null means not yet computed - const OptionalCell = ?ClassCell; - - cells: [GRID_SIZE * GRID_SIZE]OptionalCell, - origin_x: i32, // World X of grid origin (in blocks) - origin_z: i32, // World Z of grid origin (in blocks) - - pub fn init() ClassificationCache { - return .{ - .cells = [_]OptionalCell{null} ** (GRID_SIZE * GRID_SIZE), - .origin_x = 0, - .origin_z = 0, - }; - } - - /// Recenter the cache grid around a new origin. - /// Invalidates all cells. - pub fn recenter(self: *ClassificationCache, center_x: i32, center_z: i32) void { - const half_size: i64 = @intCast((GRID_SIZE * CELL_SIZE) / 2); - const cell_size: i64 = @intCast(CELL_SIZE); - const aligned_center_x = @divFloor(@as(i64, center_x), cell_size) * cell_size; - const aligned_center_z = @divFloor(@as(i64, center_z), cell_size) * cell_size; - self.origin_x = clampCacheOrigin(aligned_center_x - half_size); - self.origin_z = clampCacheOrigin(aligned_center_z - half_size); - - // Invalidate all cells - for (&self.cells) |*cell| { - cell.* = null; - } - } - - /// Check if a world position is within the cache grid - pub fn contains(self: *const ClassificationCache, world_x: i32, world_z: i32) bool { - const grid_extent: i64 = @intCast(GRID_SIZE * CELL_SIZE); - const wx = @as(i64, world_x); - const wz = @as(i64, world_z); - const ox = @as(i64, self.origin_x); - const oz = @as(i64, self.origin_z); - return wx >= ox and - wx < ox + grid_extent and - wz >= oz and - wz < oz + grid_extent; - } - - /// Get grid cell index for world position, or null if out of bounds - fn getCellIndex(self: *const ClassificationCache, world_x: i32, world_z: i32) ?usize { - if (!self.contains(world_x, world_z)) return null; - - const local_x: u32 = @intCast(@as(i64, world_x) - @as(i64, self.origin_x)); - const local_z: u32 = @intCast(@as(i64, world_z) - @as(i64, self.origin_z)); - const cell_x = local_x / CELL_SIZE; - const cell_z = local_z / CELL_SIZE; - - return cell_x + cell_z * GRID_SIZE; - } - - /// Try to get cached classification at a world position - pub fn get(self: *const ClassificationCache, world_x: i32, world_z: i32) ?ClassCell { - const idx = self.getCellIndex(world_x, world_z) orelse return null; - return self.cells[idx]; - } - - /// Store classification at a world position - pub fn put(self: *ClassificationCache, world_x: i32, world_z: i32, cell: ClassCell) void { - const idx = self.getCellIndex(world_x, world_z) orelse return; - self.cells[idx] = cell; - } - - /// Check if position has a cached value - pub fn has(self: *const ClassificationCache, world_x: i32, world_z: i32) bool { - const idx = self.getCellIndex(world_x, world_z) orelse return false; - return self.cells[idx] != null; - } - - fn clampCacheOrigin(origin: i64) i32 { - const grid_extent: i64 = @intCast(GRID_SIZE * CELL_SIZE); - const min_origin = @as(i64, std.math.minInt(i32)); - const max_origin = @as(i64, std.math.maxInt(i32)) - grid_extent; - return @intCast(std.math.clamp(origin, min_origin, max_origin)); - } -}; diff --git a/modules/worldgen-overworld/src/height_sampler.zig b/modules/worldgen-overworld/src/height_sampler.zig index 5314b6a9..2c237de4 100644 --- a/modules/worldgen-overworld/src/height_sampler.zig +++ b/modules/worldgen-overworld/src/height_sampler.zig @@ -216,7 +216,7 @@ pub const HeightSampler = struct { /// This is the main entry point for height computation. /// /// Parameters: - /// - reduction: LOD reduction level (0-4). Higher values simplify noise sampling. + /// - reduction: Detail-reduction level (0-4). Higher values simplify noise sampling. pub fn computeHeight( self: *const HeightSampler, noise_sampler: *const NoiseSampler, @@ -237,7 +237,7 @@ pub const HeightSampler = struct { reduction: u8, terrain_modifier: ?TerrainModifier, ) f32 { - // Validate reduction is in expected range (0-4 for LOD0-LOD4) + // Validate the supported detail-reduction range. std.debug.assert(reduction <= 4); const p = self.params; diff --git a/modules/worldgen-overworld/src/lod_coloring.zig b/modules/worldgen-overworld/src/lod_coloring.zig deleted file mode 100644 index cd5701ca..00000000 --- a/modules/worldgen-overworld/src/lod_coloring.zig +++ /dev/null @@ -1,94 +0,0 @@ -//! LOD surface color selection for overworld biome samples. - -const std = @import("std"); -const world_core = @import("world-core"); -const BlockType = world_core.BlockType; -const BiomeId = world_core.BiomeId; -const block_registry = world_core.block_registry; - -pub fn colorForSample(biome_id: BiomeId, surface_block: BlockType) u32 { - return switch (surface_block) { - .air => 0, - .grass => packFloatColor(grassTint(biome_id)), - .water => packFloatColor(waterTint(biome_id)), - .leaves, .mangrove_leaves, .jungle_leaves, .acacia_leaves, .birch_leaves, .spruce_leaves => packFloatColor(foliageTint(biome_id)), - else => packBlockColor(surface_block), - }; -} - -pub fn grassTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .forest => .{ 0.18, 0.64, 0.16 }, - .birch_forest => .{ 0.24, 0.68, 0.18 }, - .dark_forest => .{ 0.12, 0.46, 0.12 }, - .flower_forest => .{ 0.30, 0.72, 0.18 }, - .taiga => .{ 0.24, 0.56, 0.24 }, - .snowy_taiga => .{ 0.62, 0.74, 0.70 }, - .old_growth_taiga => .{ 0.20, 0.48, 0.28 }, - .desert => .{ 0.75, 0.70, 0.35 }, - .snow_tundra, .snowy_beach, .frozen_ocean, .frozen_river => .{ 0.7, 0.75, 0.8 }, - .snowy_mountains => .{ 0.85, 0.90, 0.95 }, - .meadow => .{ 0.32, 0.74, 0.24 }, - .grove => .{ 0.20, 0.48, 0.24 }, - .snowy_slopes => .{ 0.82, 0.88, 0.94 }, - .jagged_peaks => .{ 0.56, 0.56, 0.54 }, - .frozen_peaks => .{ 0.76, 0.88, 0.96 }, - .stony_peaks => .{ 0.62, 0.58, 0.48 }, - .swamp => .{ 0.26, 0.58, 0.18 }, - .jungle => .{ 0.10, 0.76, 0.08 }, - .savanna => .{ 0.55, 0.55, 0.30 }, - .badlands => .{ 0.5, 0.4, 0.3 }, - .mushroom_fields => .{ 0.4, 0.8, 0.4 }, - .foothills => .{ 0.24, 0.62, 0.22 }, - .dry_plains => .{ 0.55, 0.50, 0.28 }, - .coastal_plains => .{ 0.24, 0.66, 0.24 }, - .stony_shore => .{ 0.48, 0.52, 0.50 }, - else => .{ 0.22, 0.72, 0.16 }, - }; -} - -pub fn foliageTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .forest => .{ 0.12, 0.52, 0.12 }, - .birch_forest => .{ 0.18, 0.58, 0.14 }, - .dark_forest => .{ 0.08, 0.36, 0.08 }, - .flower_forest => .{ 0.20, 0.58, 0.12 }, - .taiga => .{ 0.18, 0.46, 0.18 }, - .meadow => .{ 0.20, 0.58, 0.18 }, - .grove => .{ 0.16, 0.38, 0.18 }, - .snowy_slopes => .{ 0.64, 0.72, 0.70 }, - .jagged_peaks => .{ 0.44, 0.46, 0.42 }, - .frozen_peaks => .{ 0.68, 0.78, 0.82 }, - .stony_peaks => .{ 0.46, 0.44, 0.34 }, - .snowy_taiga => .{ 0.16, 0.40, 0.24 }, - .old_growth_taiga => .{ 0.12, 0.34, 0.20 }, - .swamp => .{ 0.22, 0.52, 0.16 }, - .jungle => .{ 0.08, 0.62, 0.08 }, - .savanna => .{ 0.50, 0.50, 0.28 }, - .foothills => .{ 0.18, 0.50, 0.16 }, - .coastal_plains => .{ 0.18, 0.52, 0.16 }, - else => .{ 0.14, 0.58, 0.12 }, - }; -} - -pub fn waterTint(biome_id: BiomeId) [3]f32 { - return switch (biome_id) { - .deep_ocean => .{ 0.1, 0.2, 0.5 }, - .frozen_ocean, .frozen_river => .{ 0.32, 0.54, 0.74 }, - .cold_ocean, .stony_shore, .snowy_beach => .{ 0.10, 0.34, 0.62 }, - .swamp => .{ 0.16, 0.38, 0.30 }, - else => .{ 0.12, 0.38, 0.78 }, - }; -} - -pub fn packFloatColor(color: [3]f32) u32 { - const r: u32 = @intFromFloat(@round(std.math.clamp(color[0], 0.0, 1.0) * 255.0)); - const g: u32 = @intFromFloat(@round(std.math.clamp(color[1], 0.0, 1.0) * 255.0)); - const b: u32 = @intFromFloat(@round(std.math.clamp(color[2], 0.0, 1.0) * 255.0)); - return (r << 16) | (g << 8) | b; -} - -fn packBlockColor(block_type: BlockType) u32 { - const color = block_registry.getBlockDefinition(block_type).default_color; - return packFloatColor(color); -} diff --git a/modules/worldgen-overworld/src/noise_sampler.zig b/modules/worldgen-overworld/src/noise_sampler.zig index 8d043c0f..5064cd71 100644 --- a/modules/worldgen-overworld/src/noise_sampler.zig +++ b/modules/worldgen-overworld/src/noise_sampler.zig @@ -303,15 +303,14 @@ pub const NoiseSampler = struct { /// Get detail noise for fine terrain variation. /// - /// NOTE: The LOD multiplier intentionally reduces detail contribution at higher - /// reduction levels. At reduction=4, lod_mult becomes 0.0, completely eliminating - /// detail noise. This is by design: distant terrain (high LOD) should appear - /// smooth without fine-grained variation that causes visual noise/aliasing. + /// Higher reduction levels intentionally reduce detail contribution. At + /// reduction=4, the multiplier becomes zero, eliminating detail noise for + /// coarse map sampling. pub fn getDetail(self: *const NoiseSampler, x: f32, z: f32, reduction: u8) f32 { std.debug.assert(reduction <= 4); // Valid reduction range is 0-4 const octaves: u32 = if (3 > reduction) 3 - reduction else 1; - const lod_mult = (1.0 - 0.25 * @as(f32, @floatFromInt(reduction))); - return self.detail_noise.get2DOctaves(x, z, @intCast(octaves)) * lod_mult; + const detail_multiplier = (1.0 - 0.25 * @as(f32, @floatFromInt(reduction))); + return self.detail_noise.get2DOctaves(x, z, @intCast(octaves)) * detail_multiplier; } /// Get mountain lift noise diff --git a/modules/worldgen-overworld/src/overworld_generator.zig b/modules/worldgen-overworld/src/overworld_generator.zig index 4e66d113..16570579 100644 --- a/modules/worldgen-overworld/src/overworld_generator.zig +++ b/modules/worldgen-overworld/src/overworld_generator.zig @@ -3,6 +3,7 @@ const std = @import("std"); const sync = @import("sync"); +const log = @import("engine-core").log; const biome_mod = @import("biome.zig"); const BiomeId = biome_mod.BiomeId; const region_pkg = @import("region.zig"); @@ -17,19 +18,14 @@ const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; const BlockType = world_core.BlockType; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; -const regionSizeBlocks = world_core.regionSizeBlocks; const DecorationProvider = @import("decoration_provider.zig").DecorationProvider; const gen_region = @import("gen_region.zig"); -const ClassificationCache = gen_region.ClassificationCache; const gen_interface = @import("worldgen-api"); const Generator = gen_interface.Generator; const GeneratorInfo = gen_interface.GeneratorInfo; const WorldgenError = gen_interface.WorldgenError; const ColumnInfo = gen_interface.ColumnInfo; const MapSample = gen_interface.MapSample; -const log = @import("engine-core").log; const terrain_shape_mod = @import("terrain_shape_generator.zig"); const TerrainShapeGenerator = terrain_shape_mod.TerrainShapeGenerator; @@ -39,8 +35,6 @@ const SurfaceBuilder = terrain_shape_mod.SurfaceBuilder; const CoastalSurfaceType = terrain_shape_mod.CoastalSurfaceType; const BiomeSource = @import("biome.zig").BiomeSource; const BiomeDecorator = @import("biome_decorator.zig").BiomeDecorator; -const lod_coloring = @import("lod_coloring.zig"); -const tree_hints = @import("tree_hints.zig"); const LightingComputer = @import("worldgen-common").LightingComputer; const Mutex = sync.Mutex; @@ -48,13 +42,10 @@ pub const OverworldGenerator = struct { pub const INFO = GeneratorInfo{ .name = "Overworld", .description = "Standard terrain with diverse biomes and caves.", - // Version 3 invalidates LOD source caches whose blended controls did - // not match the chunk-local controls used by full-detail generation. .version = 3, }; allocator: std.mem.Allocator, - classification_cache: ClassificationCache, cache_center_x: i32, cache_center_z: i32, cache_mutex: Mutex, @@ -77,7 +68,6 @@ pub const OverworldGenerator = struct { pub fn initWithParams(seed: u64, allocator: std.mem.Allocator, decoration_provider: DecorationProvider, params: InitParams) OverworldGenerator { return .{ .allocator = allocator, - .classification_cache = ClassificationCache.init(), .cache_center_x = 0, .cache_center_z = 0, .cache_mutex = .{}, @@ -90,8 +80,7 @@ pub const OverworldGenerator = struct { /// Release owned resources before the struct is destroyed. /// /// All fields of `OverworldGenerator` are inline value types with no heap - /// allocations (`classification_cache` is a fixed 256x256 inline grid, - /// `cache_mutex` is an inline primitive, and `terrain_shape` / + /// allocations (`cache_mutex` is an inline primitive, and `terrain_shape` / /// `biome_decorator` own no heap memory), so there is nothing to free here /// today. This hook is still invoked by `deinitWrapper` so that any future /// heap-owning field is cleaned up through the erased `Generator` interface @@ -189,21 +178,6 @@ pub const OverworldGenerator = struct { return if (column.is_ocean) .ocean else .inland; } - pub fn maybeRecenterCache(self: *OverworldGenerator, player_x: i32, player_z: i32) bool { - self.cache_mutex.lock(); - defer self.cache_mutex.unlock(); - - const dx = player_x - self.cache_center_x; - const dz = player_z - self.cache_center_z; - if (dx * dx + dz * dz > CACHE_RECENTER_THRESHOLD * CACHE_RECENTER_THRESHOLD) { - self.classification_cache.recenter(player_x, player_z); - self.cache_center_x = player_x; - self.cache_center_z = player_z; - return true; - } - return false; - } - pub fn generate(self: *OverworldGenerator, chunk: *Chunk, stop_flag: ?*const bool) WorldgenError!void { chunk.generated = false; const world_x = chunk.getWorldX(); @@ -212,8 +186,10 @@ pub const OverworldGenerator = struct { self.cache_mutex.lock(); defer self.cache_mutex.unlock(); - if (!self.classification_cache.contains(world_x, world_z)) { - self.classification_cache.recenter(world_x, world_z); + const dx = @as(i128, world_x) - @as(i128, self.cache_center_x); + const dz = @as(i128, world_z) - @as(i128, self.cache_center_z); + const threshold: i128 = CACHE_RECENTER_THRESHOLD; + if (dx * dx + dz * dz > threshold * threshold) { self.cache_center_x = world_x; self.cache_center_z = world_z; } @@ -232,18 +208,6 @@ pub const OverworldGenerator = struct { stop_flag, )) return; - self.cache_mutex.lock(); - self.populateClassificationCache( - world_x, - world_z, - &phase_data.surface_heights, - &phase_data.biome_ids, - &phase_data.continentalness_values, - &phase_data.is_ocean_water_flags, - &phase_data.coastal_types, - ); - self.cache_mutex.unlock(); - var worm_map_opt = if (self.terrain_shape.params.disable_caves) null else @@ -296,396 +260,10 @@ pub const OverworldGenerator = struct { return self.terrain_shape.getContinentalZone(c); } - /// Generate heightmap data only (for LODSimplifiedData) - /// Uses classification cache when available to ensure LOD matches LOD0. - pub fn generateHeightmapOnly(self: *const OverworldGenerator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - if (data.width < 2) return; - - const region_size_i: i32 = @intCast(regionSizeBlocks(lod_level)); - const region_size_f: f32 = @floatFromInt(region_size_i); - const grid_max: f32 = @floatFromInt(data.width - 1); - const world_x = region_x * region_size_i; - const world_z = region_z * region_size_i; - const sea_level = self.terrain_shape.getSeaLevel(); - // Kept allocated (cheap: empty HashMap, no heap use until first put) so - // tree hints can be re-enabled per-level in sampleRepresentativeLODColumn - // without a signature change. Currently unused since compute_tree_hints - // is false for all LOD levels. - var tree_hint_cache = TreeHintCache.init(self.allocator); - defer tree_hint_cache.deinit(); - - var gz: u32 = 0; - while (gz < data.width) : (gz += 1) { - if (stop_flag) |sf| if (sf.load(.acquire)) return; - var gx: u32 = 0; - while (gx < data.width) : (gx += 1) { - const wx = @as(f32, @floatFromInt(world_x)) + (@as(f32, @floatFromInt(gx)) / grid_max) * region_size_f; - const wz = @as(f32, @floatFromInt(world_z)) + (@as(f32, @floatFromInt(gz)) / grid_max) * region_size_f; - const sample = self.sampleRepresentativeLODColumn(wx, wz, region_size_f / grid_max, sea_level, &tree_hint_cache, lod_level); - data.setGeneratedColumn(gx, gz, sample.height, sample.biome, sample.layers, sample.color, sample.water, sample.lighting, sample.vegetation); - } - } - } - - const RepresentativeLODColumn = struct { - height: f32, - biome: BiomeId, - layers: world_core.LODMaterialLayers, - color: u32, - water: world_core.LODWaterState, - lighting: world_core.LODLightingHint, - vegetation: world_core.LODVegetationHint, - }; - - const ClassifiedLODSample = struct { - wx_i: i32, - wz_i: i32, - terrain_height: f32, - terrain_height_i: i32, - biome: BiomeId, - surface_block: BlockType, - render_water_surface: bool, - }; - - const TreeHintChunk = tree_hints.TreeHintChunk; - const TreeHintCache = std.AutoHashMap(u64, TreeHintChunk); - - fn sampleRepresentativeLODColumn(self: *const OverworldGenerator, wx: f32, wz: f32, cell_span: f32, sea_level: i32, tree_hint_cache: *TreeHintCache, lod_level: LODLevel) RepresentativeLODColumn { - // Single center sample. The previous 3x3 (9-sample) grid sampled a - // sub-block neighborhood (sample_radius ~= cell_span/2 ~= 0.5-1.3 - // blocks), so 8 of 9 samples were nearly co-located and returned - // near-identical results — ~9x cost for negligible anti-aliasing. - // One sample cuts LOD heightmap generation ~9x (the dominant LOD - // loading bottleneck; was producing <1 region/sec/worker). - const sample_offsets = [_]f32{0.0}; - const sample_radius = @min(cell_span * 0.5, 48.0); - // Exact tree-coverage hints are computed via computeChunkTreeHints, which - // evaluates a full 256-column chunk of biome/decoration/tree noise per - // cache miss. LOD columns span many chunks with little reuse, so tree - // hints dominated gen cost (~30x the heightmap on lod3: 5.3s -> 0.04s - // per region). Keep that path disabled by default and use a cheap - // biome/tree-registry estimate so distant forests still have canopy - // geometry without regressing generation latency. - const compute_exact_tree_hints: bool = switch (lod_level) { - .lod0, .lod1, .lod2, .lod3, .lod4 => false, - }; - - var block_counts = [_]u32{0} ** world_core.MAX_BLOCK_TYPES; - var biome_counts = [_]u32{0} ** 256; - var color_r: u32 = 0; - var color_g: u32 = 0; - var color_b: u32 = 0; - var terrain_height_sum: f32 = 0.0; - var terrain_min: f32 = std.math.floatMax(f32); - var terrain_max: f32 = -std.math.floatMax(f32); - var water_depth_sum: f32 = 0.0; - var water_samples: u32 = 0; - var total_samples: u32 = 0; - - for (sample_offsets) |oz| { - for (sample_offsets) |ox| { - const sample = self.classifyLODSample(wx + ox * sample_radius, wz + oz * sample_radius, sea_level); - const block_index = @intFromEnum(sample.surface_block); - if (block_index < block_counts.len) block_counts[block_index] += 1; - biome_counts[@intFromEnum(sample.biome)] += 1; - - const color = lod_coloring.colorForSample(sample.biome, sample.surface_block); - color_r += (color >> 16) & 0xFF; - color_g += (color >> 8) & 0xFF; - color_b += color & 0xFF; - terrain_height_sum += sample.terrain_height; - terrain_min = @min(terrain_min, sample.terrain_height); - terrain_max = @max(terrain_max, sample.terrain_height); - total_samples += 1; - - if (sample.render_water_surface) { - water_samples += 1; - water_depth_sum += @floatFromInt(@max(sea_level - sample.terrain_height_i, 0)); - } - } - } - - const water_coverage = if (total_samples == 0) 0.0 else @as(f32, @floatFromInt(water_samples)) / @as(f32, @floatFromInt(total_samples)); - const dominant_block = dominantBlock(block_counts); - const dominant_biome = dominantBiome(biome_counts); - const render_water_surface = water_coverage >= 0.25; - const surface_block: BlockType = if (render_water_surface) .water else dominant_block; - const avg_height = terrain_height_sum / @as(f32, @floatFromInt(@max(total_samples, 1))); - const terrain_range = @max(terrain_max - terrain_min, 0.0); - const land_height = if (terrain_range > 12.0) - avg_height + terrain_range * 0.35 - else - avg_height; - const height = if (render_water_surface) - @as(f32, @floatFromInt(sea_level)) - else - land_height; - const avg_color = packAverageColor(color_r, color_g, color_b, @max(total_samples, 1)); - const vegetation_hint = if (render_water_surface) - world_core.LODVegetationHint.empty - else if (compute_exact_tree_hints) - self.actualTreeHintInArea(wx, wz, sample_radius, dominant_biome, tree_hint_cache) - else - tree_hints.estimatedTreeHintForBiome(dominant_biome, wx, wz, self.terrain_shape.getSeed()); - return .{ - .height = height, - .biome = dominant_biome, - .layers = makeMaterialLayers(surface_block, dominant_biome, render_water_surface), - .color = avg_color, - .water = .{ - .is_surface = render_water_surface, - .surface_height = if (render_water_surface) @floatFromInt(sea_level) else 0.0, - .depth = if (water_samples == 0) 0.0 else water_depth_sum / @as(f32, @floatFromInt(water_samples)), - .coverage = water_coverage, - }, - .lighting = makeLightingHint(render_water_surface), - .vegetation = vegetation_hint, - }; - } - - fn actualTreeHintAtColumn(self: *const OverworldGenerator, target_wx: i32, target_wz: i32, cache: *TreeHintCache) world_core.LODVegetationHint { - const chunk_x = @divFloor(target_wx, @as(i32, @intCast(CHUNK_SIZE_X))); - const chunk_z = @divFloor(target_wz, @as(i32, @intCast(CHUNK_SIZE_Z))); - const local_x: u32 = @intCast(@mod(target_wx, @as(i32, @intCast(CHUNK_SIZE_X)))); - const local_z: u32 = @intCast(@mod(target_wz, @as(i32, @intCast(CHUNK_SIZE_Z)))); - const idx = local_x + local_z * CHUNK_SIZE_X; - const key = tree_hints.cacheKey(chunk_x, chunk_z); - - if (cache.get(key)) |hints| return hints[idx]; - - const hints = self.computeChunkTreeHints(chunk_x, chunk_z); - const result = hints[idx]; - cache.put(key, hints) catch |err| { - log.log.warn("TreeHintCache: failed to cache tree hints for chunk ({}, {}): {}", .{ chunk_x, chunk_z, err }); - }; - return result; - } - - fn actualTreeHintInArea(self: *const OverworldGenerator, center_wx: f32, center_wz: f32, radius: f32, dominant_biome: BiomeId, cache: *TreeHintCache) world_core.LODVegetationHint { - const min_x: i32 = @intFromFloat(@floor(center_wx - radius)); - const max_x: i32 = @intFromFloat(@ceil(center_wx + radius)); - const min_z: i32 = @intFromFloat(@floor(center_wz - radius)); - const max_z: i32 = @intFromFloat(@ceil(center_wz + radius)); - const min_chunk_x = @divFloor(min_x, @as(i32, @intCast(CHUNK_SIZE_X))); - const max_chunk_x = @divFloor(max_x, @as(i32, @intCast(CHUNK_SIZE_X))); - const min_chunk_z = @divFloor(min_z, @as(i32, @intCast(CHUNK_SIZE_Z))); - const max_chunk_z = @divFloor(max_z, @as(i32, @intCast(CHUNK_SIZE_Z))); - - var tree_count: u32 = 0; - var height_sum: f32 = 0.0; - var offset_x_sum: f32 = 0.0; - var offset_z_sum: f32 = 0.0; - var best = world_core.LODVegetationHint.empty; - - var chunk_z = min_chunk_z; - while (chunk_z <= max_chunk_z) : (chunk_z += 1) { - var chunk_x = min_chunk_x; - while (chunk_x <= max_chunk_x) : (chunk_x += 1) { - const key = tree_hints.cacheKey(chunk_x, chunk_z); - const hints = blk: { - if (cache.get(key)) |cached| break :blk cached; - const computed = self.computeChunkTreeHints(chunk_x, chunk_z); - cache.put(key, computed) catch |err| { - log.log.warn("TreeHintCache: failed to cache tree hints for chunk ({}, {}): {}", .{ chunk_x, chunk_z, err }); - }; - break :blk computed; - }; - const chunk_world_x = chunk_x * @as(i32, @intCast(CHUNK_SIZE_X)); - const chunk_world_z = chunk_z * @as(i32, @intCast(CHUNK_SIZE_Z)); - - var lz: u32 = 0; - while (lz < CHUNK_SIZE_Z) : (lz += 1) { - const wz = chunk_world_z + @as(i32, @intCast(lz)); - if (wz < min_z or wz > max_z) continue; - var lx: u32 = 0; - while (lx < CHUNK_SIZE_X) : (lx += 1) { - const wx = chunk_world_x + @as(i32, @intCast(lx)); - if (wx < min_x or wx > max_x) continue; - const hint = hints[lx + lz * CHUNK_SIZE_X]; - if (hint.tree_coverage <= 0.0) continue; - tree_count += 1; - height_sum += hint.avg_tree_height; - offset_x_sum += @as(f32, @floatFromInt(wx)) - center_wx; - offset_z_sum += @as(f32, @floatFromInt(wz)) - center_wz; - if (best.tree_coverage <= 0.0) best = hint; - } - } - } - } - - if (tree_count == 0) return world_core.LODVegetationHint.empty; - - const blocks = if (best.leaves == .air) tree_hints.blocksForBiome(dominant_biome) else tree_hints.TreeBlocks{ .trunk = best.trunk, .leaves = best.leaves }; - const area = @max(1.0, (radius * 2.0 + 1.0) * (radius * 2.0 + 1.0)); - return .{ - .tree_coverage = std.math.clamp(@as(f32, @floatFromInt(tree_count)) / area * 24.0, 0.0, 1.0), - .avg_tree_height = height_sum / @as(f32, @floatFromInt(tree_count)), - .offset_x = offset_x_sum / @as(f32, @floatFromInt(tree_count)), - .offset_z = offset_z_sum / @as(f32, @floatFromInt(tree_count)), - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; - } - - fn computeChunkTreeHints(self: *const OverworldGenerator, chunk_x: i32, chunk_z: i32) TreeHintChunk { - const world_x = chunk_x * @as(i32, @intCast(CHUNK_SIZE_X)); - const world_z = chunk_z * @as(i32, @intCast(CHUNK_SIZE_Z)); - return tree_hints.computeChunkTreeHints(.{ - .chunk_x = chunk_x, - .chunk_z = chunk_z, - .world_x = world_x, - .world_z = world_z, - .sea_level = self.terrain_shape.getSeaLevel(), - .terrain_region_seed = self.terrain_shape.getRegionSeed(), - .decorator_region_seed = self.biome_decorator.region_seed, - .noise_sampler = self.terrain_shape.getNoiseSampler(), - .classify_context = self, - .classify_fn = classifyTreeHintSample, - }); - } - - fn classifyTreeHintSample(context: *const anyopaque, wx: f32, wz: f32, sea_level: i32, controls: region_pkg.RegionControlCorners) tree_hints.ClassifiedSample { - const self: *const OverworldGenerator = @ptrCast(@alignCast(context)); - _ = controls; - const sample = self.classifyLODSample(wx, wz, sea_level); - return .{ - .biome = sample.biome, - .surface_block = sample.surface_block, - .terrain_height_i = sample.terrain_height_i, - }; - } - - fn classifyLODSample(self: *const OverworldGenerator, wx: f32, wz: f32, sea_level: i32) ClassifiedLODSample { - const wx_i: i32 = @intFromFloat(@floor(wx)); - const wz_i: i32 = @intFromFloat(@floor(wz)); - const column = self.sampleFullDetailColumnData(wx, wz, wx_i, wz_i); - const render_water_surface = column.terrain_height_i < sea_level; - - if (self.getCachedClassification(wx_i, wz_i)) |cached| { - return .{ - .wx_i = wx_i, - .wz_i = wz_i, - .terrain_height = column.terrain_height, - .terrain_height_i = column.terrain_height_i, - .biome = cached.biome_id, - .surface_block = if (render_water_surface) .water else self.surfaceTypeToBlock(cached.surface_type), - .render_water_surface = render_water_surface, - }; - } - - const climate = biome_mod.computeClimateParams( - column.temperature, - column.humidity, - column.terrain_height_i, - column.continentalness, - column.erosion, - sea_level, - CHUNK_SIZE_Y, - ); - - const structural = biome_mod.StructuralParams{ - .height = column.terrain_height_i, - .slope = 0, - .continentalness = column.continentalness, - .ridge_mask = column.ridge_mask, - }; - - const biome_id = biome_mod.selectBiomeWithConstraintsAndRiver(climate, structural, column.river_mask); - return .{ - .wx_i = wx_i, - .wz_i = wz_i, - .terrain_height = column.terrain_height, - .terrain_height_i = column.terrain_height_i, - .biome = biome_id, - .surface_block = self.getSurfaceBlock(biome_id, column.terrain_height_i, sea_level, render_water_surface), - .render_water_surface = render_water_surface, - }; - } - - /// Samples terrain with the same chunk-local region controls as - /// `prepareChunkPhaseData`. Canonical blended controls can select a very - /// different terrain height and place an LOD surface above the real chunk. - fn sampleFullDetailColumnData(self: *const OverworldGenerator, wx: f32, wz: f32, wx_i: i32, wz_i: i32) terrain_shape_mod.ColumnData { - const chunk_x = @divFloor(wx_i, CHUNK_SIZE_X) * CHUNK_SIZE_X; - const chunk_z = @divFloor(wz_i, CHUNK_SIZE_Z) * CHUNK_SIZE_Z; - const controls = region_pkg.RegionControlCorners.init( - self.terrain_shape.getRegionSeed(), - chunk_x, - chunk_z, - chunk_x + CHUNK_SIZE_X - 1, - chunk_z + CHUNK_SIZE_Z - 1, - ); - return self.terrain_shape.sampleColumnDataWithControls(wx, wz, 0, controls.sample(wx_i, wz_i)); - } - - fn dominantBlock(counts: [world_core.MAX_BLOCK_TYPES]u32) BlockType { - var best_index: usize = @intFromEnum(BlockType.grass); - var best_count: u32 = 0; - for (counts, 0..) |count, i| { - if (count > best_count) { - best_index = i; - best_count = count; - } - } - return @enumFromInt(best_index); - } - - fn dominantBiome(counts: [256]u32) BiomeId { - var best_index: usize = @intFromEnum(BiomeId.plains); - var best_count: u32 = 0; - for (counts, 0..) |count, i| { - if (count > best_count) { - best_index = i; - best_count = count; - } - } - return @enumFromInt(best_index); - } - - fn packAverageColor(r_sum: u32, g_sum: u32, b_sum: u32, count: u32) u32 { - const r = r_sum / count; - const g = g_sum / count; - const b = b_sum / count; - return (r << 16) | (g << 8) | b; - } - - fn makeMaterialLayers(surface_block: BlockType, biome_id: BiomeId, render_water_surface: bool) world_core.LODMaterialLayers { - if (render_water_surface and (surface_block == .ice or surface_block == .packed_ice)) { - return .{ - .surface = surface_block, - .subsurface = .water, - .foundation = .stone, - }; - } - - if (render_water_surface) { - const floor_block: BlockType = switch (biome_id) { - .deep_ocean, .frozen_ocean, .cold_ocean, .stony_shore, .frozen_river => .gravel, - .ocean, .river, .beach, .snowy_beach => .sand, - else => .dirt, - }; - return .{ - .surface = .water, - .subsurface = floor_block, - .foundation = .stone, - }; - } - - return .{ - .surface = surface_block, - .subsurface = registryFillerBlock(biome_id), - .foundation = .stone, - }; - } - fn registrySurfaceBlock(biome_id: BiomeId) BlockType { return biome_mod.getBiomeDefinition(biome_id).surface.top; } - fn registryFillerBlock(biome_id: BiomeId) BlockType { - return biome_mod.getBiomeDefinition(biome_id).surface.filler; - } - fn surfaceTypeForBlock(block: BlockType) SurfaceType { return switch (block) { .sand, .red_sand => .sand, @@ -698,92 +276,6 @@ pub const OverworldGenerator = struct { }; } - fn makeLightingHint(render_water_surface: bool) world_core.LODLightingHint { - return .{ - .sky_light = 15, - .block_light = 0, - .ambient_occlusion = if (render_water_surface) 0.92 else 1.0, - }; - } - - fn surfaceTypeToBlock(_: *const OverworldGenerator, surface_type: SurfaceType) BlockType { - return switch (surface_type) { - .grass => .grass, - .sand => .sand, - .rock => .gravel, - .snow => .snow_block, - .water_deep, .water_shallow => .water, - .dirt => .dirt, - .stone => .stone, - }; - } - - fn getSurfaceBlock(_: *const OverworldGenerator, biome_id: BiomeId, height: i32, sea_level: i32, render_water_surface: bool) BlockType { - if (render_water_surface and biome_id == .frozen_ocean) return .ice; - if (render_water_surface and biome_id == .frozen_river) return .ice; - if (render_water_surface or height < sea_level) return .water; - return registrySurfaceBlock(biome_id); - } - - fn populateClassificationCache( - self: *OverworldGenerator, - world_x: i32, - world_z: i32, - surface_heights: *const [CHUNK_SIZE_X * CHUNK_SIZE_Z]i32, - biome_ids: *const [CHUNK_SIZE_X * CHUNK_SIZE_Z]BiomeId, - continentalness_values: *const [CHUNK_SIZE_X * CHUNK_SIZE_Z]f32, - is_ocean_water_flags: *const [CHUNK_SIZE_X * CHUNK_SIZE_Z]bool, - coastal_types: *const [CHUNK_SIZE_X * CHUNK_SIZE_Z]CoastalSurfaceType, - ) void { - const sea_level = self.terrain_shape.getSeaLevel(); - const region_seed = self.terrain_shape.getRegionSeed(); - - var local_z: u32 = 0; - while (local_z < CHUNK_SIZE_Z) : (local_z += 1) { - var local_x: u32 = 0; - while (local_x < CHUNK_SIZE_X) : (local_x += 1) { - const idx = local_x + local_z * CHUNK_SIZE_X; - const wx = world_x + @as(i32, @intCast(local_x)); - const wz = world_z + @as(i32, @intCast(local_z)); - if (self.classification_cache.has(wx, wz)) continue; - - const biome_id = biome_ids[idx]; - const height = surface_heights[idx]; - const continentalness = continentalness_values[idx]; - const is_ocean = is_ocean_water_flags[idx]; - const coastal_type = coastal_types[idx]; - - const surface_type = self.deriveSurfaceTypeInternal( - biome_id, - height, - sea_level, - is_ocean, - coastal_type, - ); - - const continental_zone = self.terrain_shape.getContinentalZone(continentalness); - const region_info = region_pkg.getRegion(region_seed, wx, wz); - const path_info = region_pkg.getPathInfo(region_seed, wx, wz, region_info); - - self.classification_cache.put(wx, wz, .{ - .biome_id = biome_id, - .surface_type = surface_type, - .is_water = height < sea_level, - .continental_zone = continental_zone, - .region_role = region_info.role, - .path_type = path_info.path_type, - }); - } - } - } - - fn getCachedClassification(self: *const OverworldGenerator, world_x: i32, world_z: i32) ?gen_region.ClassCell { - const mutable_self: *OverworldGenerator = @constCast(self); - mutable_self.cache_mutex.lock(); - defer mutable_self.cache_mutex.unlock(); - return mutable_self.classification_cache.get(world_x, world_z); - } - fn deriveSurfaceTypeInternal( _: *const OverworldGenerator, biome_id: BiomeId, @@ -817,8 +309,6 @@ pub const OverworldGenerator = struct { const VTABLE = Generator.VTable{ .generate = generateWrapper, - .generateHeightmapOnly = generateHeightmapOnlyWrapper, - .maybeRecenterCache = maybeRecenterCacheWrapper, .getSeed = getSeedWrapper, .getRegionInfo = getRegionInfoWrapper, .getColumnInfo = getColumnInfoWrapper, @@ -833,16 +323,6 @@ pub const OverworldGenerator = struct { try self.generate(chunk, stop_flag); } - fn generateHeightmapOnlyWrapper(ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - const self: *const OverworldGenerator = @ptrCast(@alignCast(ptr)); - self.generateHeightmapOnly(data, region_x, region_z, lod_level, stop_flag); - } - - fn maybeRecenterCacheWrapper(ptr: *anyopaque, player_x: i32, player_z: i32) bool { - const self: *OverworldGenerator = @ptrCast(@alignCast(ptr)); - return self.maybeRecenterCache(player_x, player_z); - } - fn getSeedWrapper(ptr: *anyopaque) u64 { const self: *OverworldGenerator = @ptrCast(@alignCast(ptr)); return self.getSeed(); @@ -875,70 +355,6 @@ pub const OverworldGenerator = struct { } }; -test "LOD cached water surfaces resolve to seabed block" { - try std.testing.expectEqual(BlockType.water, OverworldGenerator.surfaceTypeToBlock(undefined, .water_shallow)); - try std.testing.expectEqual(BlockType.water, OverworldGenerator.surfaceTypeToBlock(undefined, .water_deep)); -} - -test "LOD classification matches full-detail chunk controls and sea-level water" { - var gen = OverworldGenerator.initWithParams(12345, std.testing.allocator, testDecorationProvider(), .{ - .terrain_shape = .{ .disable_caves = true }, - .basic_chunks_only = true, - }); - defer gen.deinit(); - - const sea_level = gen.terrain_shape.getSeaLevel(); - const positions = [_][2]i32{ - .{ -1025, -1025 }, - .{ -513, 511 }, - .{ -1, 0 }, - .{ 0, 0 }, - .{ 511, 513 }, - .{ 1025, -1025 }, - }; - for (positions) |position| { - const wx: f32 = @floatFromInt(position[0]); - const wz: f32 = @floatFromInt(position[1]); - const chunk_x = @divFloor(position[0], CHUNK_SIZE_X) * CHUNK_SIZE_X; - const chunk_z = @divFloor(position[1], CHUNK_SIZE_Z) * CHUNK_SIZE_Z; - const controls = region_pkg.RegionControlCorners.init( - gen.terrain_shape.getRegionSeed(), - chunk_x, - chunk_z, - chunk_x + CHUNK_SIZE_X - 1, - chunk_z + CHUNK_SIZE_Z - 1, - ); - const column = gen.terrain_shape.sampleColumnDataWithControls(wx, wz, 0, controls.sample(position[0], position[1])); - const lod_sample = gen.classifyLODSample(wx, wz, sea_level); - try std.testing.expectEqual(column.terrain_height_i, lod_sample.terrain_height_i); - try std.testing.expectEqual(column.terrain_height_i < sea_level, lod_sample.render_water_surface); - } -} - -test "LOD surface height matches generated full-detail terrain at chunk origin" { - var gen = OverworldGenerator.initWithParams(12345, std.testing.allocator, testDecorationProvider(), .{ - .terrain_shape = .{ .disable_caves = true }, - .basic_chunks_only = true, - }); - defer gen.deinit(); - - var chunk = Chunk.init(0, 0); - try gen.generate(&chunk, null); - - var top_solid_y: i32 = 0; - var y: i32 = CHUNK_SIZE_Y - 1; - while (y >= 0) : (y -= 1) { - const block = chunk.getBlock(0, @intCast(y), 0); - if (block != .air and block != .water) { - top_solid_y = y; - break; - } - } - - const lod_sample = gen.classifyLODSample(0.0, 0.0, gen.terrain_shape.getSeaLevel()); - try std.testing.expectApproxEqAbs(@as(f32, @floatFromInt(top_solid_y)), lod_sample.terrain_height, 1.0); -} - fn testDecorationProvider() DecorationProvider { const NoopProvider = struct { fn decorate(_: ?*anyopaque, _: DecorationProvider.DecorationContext) void {} diff --git a/modules/worldgen-overworld/src/root.zig b/modules/worldgen-overworld/src/root.zig index aceb1f79..c5d7fda5 100644 --- a/modules/worldgen-overworld/src/root.zig +++ b/modules/worldgen-overworld/src/root.zig @@ -24,7 +24,6 @@ pub const height_sampler_tests = @import("height_sampler_tests.zig"); pub const noise_sampler = @import("noise_sampler.zig"); pub const lighting_computer = @import("worldgen-common").lighting_computer; pub const lighting_interface = @import("worldgen-common").lighting_interface; -pub const lod_coloring = @import("lod_coloring.zig"); pub const mood = @import("mood.zig"); pub const noise = @import("noise.zig"); pub const overworld_generator = @import("overworld_generator.zig"); @@ -35,7 +34,6 @@ pub const terrain_shape_generator = @import("terrain_shape_generator.zig"); pub const terrain_shape_generator_tests = @import("terrain_shape_generator_tests.zig"); pub const terrain_modifier_tests = @import("terrain_modifier_tests.zig"); pub const terrain_report = @import("terrain_report.zig"); -pub const tree_hints = @import("tree_hints.zig"); pub const tree_registry = @import("tree_registry.zig"); pub const world_class = @import("world_class.zig"); pub const world_map = @import("world_map.zig"); diff --git a/modules/worldgen-overworld/src/terrain_shape_generator.zig b/modules/worldgen-overworld/src/terrain_shape_generator.zig index 5dd9bbe5..cea09e47 100644 --- a/modules/worldgen-overworld/src/terrain_shape_generator.zig +++ b/modules/worldgen-overworld/src/terrain_shape_generator.zig @@ -273,10 +273,10 @@ pub const TerrainShapeGenerator = struct { center_controls: region_pkg.RegionControls, ) biome_mod.TerrainModifier { _ = center_controls; - // Coarse LOD columns already represent multiple blocks, so the 9-point + // Coarse map samples already represent multiple blocks, so the 9-point // biome influence blend is visually indistinguishable but multiplies - // the dominant terrain-modifier sampling cost. Keep full blending for - // near/detail generation and use the center sample from reduction 2+. + // the dominant terrain-modifier sampling cost. Use the center sample + // from reduction 2+. if (reduction >= 2) return self.sampleTerrainModifierAt(wx, wz, reduction); const samples = [_]biome_mod.TerrainModifier{ diff --git a/modules/worldgen-overworld/src/tree_hints.zig b/modules/worldgen-overworld/src/tree_hints.zig deleted file mode 100644 index cf460c93..00000000 --- a/modules/worldgen-overworld/src/tree_hints.zig +++ /dev/null @@ -1,287 +0,0 @@ -//! LOD vegetation hint computation for overworld tree placement. - -const std = @import("std"); -const biome_mod = @import("biome.zig"); -const BiomeId = biome_mod.BiomeId; -const region_pkg = @import("region.zig"); -const decoration_registry = @import("decoration_registry.zig"); -const NoiseSampler = @import("noise_sampler.zig").NoiseSampler; -const schematics = @import("schematics.zig"); -const tree_registry = @import("tree_registry.zig"); -const world_core = @import("world-core"); -const BlockType = world_core.BlockType; -const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; -const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; -const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; - -pub const TreeHintChunk = [CHUNK_SIZE_X * CHUNK_SIZE_Z]world_core.LODVegetationHint; - -pub const TreeBlocks = struct { - trunk: BlockType, - leaves: BlockType, -}; - -pub const ClassifiedSample = struct { - biome: BiomeId, - surface_block: BlockType, - terrain_height_i: i32, -}; - -pub const ClassifyFn = *const fn (*const anyopaque, f32, f32, i32, region_pkg.RegionControlCorners) ClassifiedSample; - -pub const ComputeParams = struct { - chunk_x: i32, - chunk_z: i32, - world_x: i32, - world_z: i32, - sea_level: i32, - terrain_region_seed: u64, - decorator_region_seed: u64, - noise_sampler: *const NoiseSampler, - classify_context: *const anyopaque, - classify_fn: ClassifyFn, -}; - -pub fn computeChunkTreeHints(params: ComputeParams) TreeHintChunk { - const controls = region_pkg.RegionControlCorners.init( - params.terrain_region_seed, - params.world_x, - params.world_z, - params.world_x + @as(i32, @intCast(CHUNK_SIZE_X)) - 1, - params.world_z + @as(i32, @intCast(CHUNK_SIZE_Z)) - 1, - ); - var prng = std.Random.DefaultPrng.init(params.decorator_region_seed ^ @as(u64, @bitCast(@as(i64, params.chunk_x))) ^ (@as(u64, @bitCast(@as(i64, params.chunk_z))) << 32)); - const random = prng.random(); - var tree_occupancy = [_]bool{false} ** world_core.CHUNK_VOLUME; - var hints = [_]world_core.LODVegetationHint{world_core.LODVegetationHint.empty} ** (CHUNK_SIZE_X * CHUNK_SIZE_Z); - - var lz: u32 = 0; - while (lz < CHUNK_SIZE_Z) : (lz += 1) { - var lx: u32 = 0; - while (lx < CHUNK_SIZE_X) : (lx += 1) { - const wx_i = params.world_x + @as(i32, @intCast(lx)); - const wz_i = params.world_z + @as(i32, @intCast(lz)); - const sample = params.classify_fn(params.classify_context, @floatFromInt(wx_i), @floatFromInt(wz_i), params.sea_level, controls); - const column_controls = controls.sample(wx_i, wz_i); - const variant = params.noise_sampler.variant_noise.get2D(@floatFromInt(wx_i), @floatFromInt(wz_i)); - _ = decoration_registry.chooseStaticSimpleDecoration(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, 0, random); - - const placed_tree = choosePlacedTree(sample.biome, sample.surface_block, variant, column_controls.subbiome_mask > 0.5, column_controls.vegetation_mult, lx, lz, sample.terrain_height_i, &tree_occupancy, random); - if (placed_tree) |tree| { - placeTreeOccupancy(tree.schematic, lx, @intCast(sample.terrain_height_i + 1), lz, &tree_occupancy, random); - hints[lx + lz * CHUNK_SIZE_X] = treeHintForType(tree.tree_type); - } - } - } - return hints; -} - -pub fn estimatedTreeHintForBiome(biome_id: BiomeId, wx: f32, wz: f32, seed: u64) world_core.LODVegetationHint { - const tree_types = biome_mod.getBiomeDefinition(biome_id).vegetation.tree_types; - if (tree_types.len == 0) return world_core.LODVegetationHint.empty; - - var best_type = tree_types[0]; - var best_probability: f32 = 0.0; - var probability_sum: f32 = 0.0; - for (tree_types) |tree_type| { - const def = tree_registry.getTreeDefinition(tree_type) orelse continue; - probability_sum += def.probability; - if (def.probability > best_probability) { - best_probability = def.probability; - best_type = tree_type; - } - } - - const base_coverage = std.math.clamp(probability_sum * 7.5, 0.0, 1.0); - if (base_coverage < 0.035) return world_core.LODVegetationHint.empty; - - const wx_i: i32 = @intFromFloat(@floor(wx)); - const wz_i: i32 = @intFromFloat(@floor(wz)); - const jitter = hashUnit2D(wx_i, wz_i, seed); - const placement_roll = hashUnit2D(wx_i, wz_i, seed ^ 0x6D2B79F5); - const placement_probability = std.math.clamp(base_coverage * 0.32, 0.025, 0.55); - if (placement_roll > placement_probability) return world_core.LODVegetationHint.empty; - - const blocks = blocksForType(best_type); - return .{ - .tree_coverage = std.math.clamp(0.12 + base_coverage * (0.18 + jitter * 0.08), 0.0, 0.45), - .avg_tree_height = heightForType(best_type), - .offset_x = (hashUnit2D(wx_i, wz_i, seed ^ 0xA53A9D13) - 0.5) * 2.0, - .offset_z = (hashUnit2D(wx_i, wz_i, seed ^ 0xC2B2AE35) - 0.5) * 2.0, - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; -} - -pub fn cacheKey(chunk_x: i32, chunk_z: i32) u64 { - const x: u64 = @bitCast(@as(i64, chunk_x)); - const z: u64 = @bitCast(@as(i64, chunk_z)); - return (x *% 0x9E3779B97F4A7C15) ^ (z *% 0xC2B2AE3D27D4EB4F); -} - -pub fn blocksForBiome(biome_id: BiomeId) TreeBlocks { - return switch (biome_id) { - .taiga, .snowy_taiga, .old_growth_taiga, .snow_tundra, .snowy_mountains, .grove, .snowy_slopes => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .birch_forest => .{ .trunk = .birch_log, .leaves = .birch_leaves }, - .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, - .savanna => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, - .swamp, .mangrove_swamp, .marsh => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, - else => .{ .trunk = .wood, .leaves = .leaves }, - }; -} - -fn hashUnit2D(x: i32, z: i32, seed: u64) f32 { - var h = seed; - h ^= @as(u64, @bitCast(@as(i64, x))) *% 0x9E3779B97F4A7C15; - h ^= @as(u64, @bitCast(@as(i64, z))) *% 0xC2B2AE3D27D4EB4F; - h ^= h >> 33; - h *%= 0xFF51AFD7ED558CCD; - h ^= h >> 33; - const bucket: u32 = @intCast(h & 0xFFFF); - return @as(f32, @floatFromInt(bucket)) / 65535.0; -} - -const PlacedTree = struct { - tree_type: tree_registry.TreeType, - schematic: schematics.Schematic, -}; - -fn choosePlacedTree( - biome_id: BiomeId, - surface_block: BlockType, - variant: f32, - allow_subbiomes: bool, - veg_mult: f32, - local_x: u32, - local_z: u32, - surface_y: i32, - tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, - random: std.Random, -) ?PlacedTree { - const vegetation = biome_mod.getBiomeDefinition(biome_id).vegetation; - if (vegetation.tree_types.len == 0) return null; - - for (vegetation.tree_types) |tree_type| { - const tree_def = tree_registry.getTreeDefinition(tree_type) orelse continue; - if (!treeSurfaceAllowed(tree_def.place_on, surface_block)) continue; - if (!variantAllowed(variant, allow_subbiomes, tree_def.variant_min, tree_def.variant_max)) continue; - const prob = @min(1.0, tree_def.probability * veg_mult); - if (random.float(f32) >= prob) continue; - if (tree_def.spacing_radius > 0 and !isTreeAreaClear(tree_occupancy, @intCast(local_x), surface_y, @intCast(local_z), tree_def.spacing_radius)) continue; - return .{ - .tree_type = tree_type, - .schematic = tree_def.schematic, - }; - } - return null; -} - -fn variantAllowed(variant: f32, allow_subbiomes: bool, min: f32, max: f32) bool { - if (allow_subbiomes) return variant >= min and variant <= max; - return min == -1.0 and max == 1.0; -} - -fn treeSurfaceAllowed(place_on: []const BlockType, surface_block: BlockType) bool { - for (place_on) |valid| { - if (surface_block == valid) return true; - } - return false; -} - -fn isTreeAreaClear(tree_occupancy: *const [world_core.CHUNK_VOLUME]bool, x: i32, y: i32, z: i32, radius: i32) bool { - var dz: i32 = -radius; - while (dz <= radius) : (dz += 1) { - var dx: i32 = -radius; - while (dx <= radius) : (dx += 1) { - if (dx == 0 and dz == 0) continue; - const check_x = x + dx; - const check_z = z + dz; - if (check_x >= 0 and check_x < CHUNK_SIZE_X and check_z >= 0 and check_z < CHUNK_SIZE_Z) { - var dy: i32 = 1; - while (dy <= 3) : (dy += 1) { - const check_y = y + dy; - if (check_y >= 0 and check_y < CHUNK_SIZE_Y) { - const idx: usize = @intCast(@as(u32, @intCast(check_x)) + @as(u32, @intCast(check_z)) * CHUNK_SIZE_X + @as(u32, @intCast(check_y)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); - if (tree_occupancy[idx]) return false; - } - } - } - } - } - return true; -} - -fn placeTreeOccupancy(schematic: schematics.Schematic, x: u32, y: u32, z: u32, tree_occupancy: *[world_core.CHUNK_VOLUME]bool, random: std.Random) void { - const center_x = @as(i32, @intCast(x)); - const center_y = @as(i32, @intCast(y)); - const center_z = @as(i32, @intCast(z)); - for (schematic.blocks) |block| { - if (block.probability < 1.0) { - if (random.float(f32) >= block.probability) continue; - } - const bx = center_x + block.offset[0] - schematic.center_x; - const by = center_y + block.offset[1]; - const bz = center_z + block.offset[2] - schematic.center_z; - if (bx >= 0 and bx < CHUNK_SIZE_X and bz >= 0 and bz < CHUNK_SIZE_Z and by >= 0 and by < CHUNK_SIZE_Y and isTreeBlock(block.block)) { - const idx: usize = @intCast(@as(u32, @intCast(bx)) + @as(u32, @intCast(bz)) * CHUNK_SIZE_X + @as(u32, @intCast(by)) * CHUNK_SIZE_X * CHUNK_SIZE_Z); - tree_occupancy[idx] = true; - } - } -} - -fn isTreeBlock(block: BlockType) bool { - return switch (block) { - .wood, - .leaves, - .birch_log, - .birch_leaves, - .spruce_log, - .spruce_leaves, - .jungle_log, - .jungle_leaves, - .acacia_log, - .acacia_leaves, - .mangrove_log, - .mangrove_leaves, - => true, - else => false, - }; -} - -fn treeHintForType(tree_type: tree_registry.TreeType) world_core.LODVegetationHint { - const blocks = blocksForType(tree_type); - return .{ - .tree_coverage = 1.0, - .avg_tree_height = heightForType(tree_type), - .offset_x = 0.0, - .offset_z = 0.0, - .trunk = blocks.trunk, - .leaves = blocks.leaves, - }; -} - -fn blocksForType(tree_type: tree_registry.TreeType) TreeBlocks { - return switch (tree_type) { - .spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .dense_spruce => .{ .trunk = .spruce_log, .leaves = .spruce_leaves }, - .birch, .dense_birch => .{ .trunk = .birch_log, .leaves = .birch_leaves }, - .jungle => .{ .trunk = .jungle_log, .leaves = .jungle_leaves }, - .acacia => .{ .trunk = .acacia_log, .leaves = .acacia_leaves }, - .mangrove => .{ .trunk = .mangrove_log, .leaves = .mangrove_leaves }, - .huge_red_mushroom => .{ .trunk = .mushroom_stem, .leaves = .red_mushroom_block }, - .huge_brown_mushroom => .{ .trunk = .mushroom_stem, .leaves = .brown_mushroom_block }, - else => .{ .trunk = .wood, .leaves = .leaves }, - }; -} - -fn heightForType(tree_type: tree_registry.TreeType) f32 { - return switch (tree_type) { - .jungle => 13.0, - .spruce => 10.0, - .dense_spruce => 12.0, - .acacia => 8.0, - .swamp_oak, .mangrove => 7.0, - .huge_red_mushroom, .huge_brown_mushroom => 6.0, - else => 6.0, - }; -} diff --git a/modules/worldgen-overworld/src/world_class.zig b/modules/worldgen-overworld/src/world_class.zig index 9d3e6881..de11f82f 100644 --- a/modules/worldgen-overworld/src/world_class.zig +++ b/modules/worldgen-overworld/src/world_class.zig @@ -1,9 +1,9 @@ -//! World Classification Map - authoritative world layout for LOD +//! World Classification Map - authoritative world layout for generation. //! //! Computed ONCE per region, deterministically -//! All LOD levels sample from this SAME map (no re-computation) +//! Full-detail generation samples this shared map without recomputation. //! -//! LOD-INVARIANT: These values MUST be identical for all LODs: +//! Generation invariant: these values must remain stable for every sample: //! - Biome ID //! - Region Role / Mood //! - Land vs water decision diff --git a/modules/worldgen-overworld/src/world_map.zig b/modules/worldgen-overworld/src/world_map.zig index 7b9c3145..dfe2c41a 100644 --- a/modules/worldgen-overworld/src/world_map.zig +++ b/modules/worldgen-overworld/src/world_map.zig @@ -634,10 +634,6 @@ const FakeMapGenerator = struct { release_sample: std.atomic.Value(bool) = std.atomic.Value(bool).init(false), fn generate(_: *anyopaque, _: *world_core.Chunk, _: ?*const bool) gen_interface.WorldgenError!void {} - fn generateHeightmapOnly(_: *anyopaque, _: *world_core.LODSimplifiedData, _: i32, _: i32, _: world_core.LODLevel, _: ?*const std.atomic.Value(bool)) void {} - fn maybeRecenterCache(_: *anyopaque, _: i32, _: i32) bool { - return false; - } fn getSeed(_: *anyopaque) u64 { return 1; } @@ -661,8 +657,6 @@ const FakeMapGenerator = struct { const VTABLE = Generator.VTable{ .generate = generate, - .generateHeightmapOnly = generateHeightmapOnly, - .maybeRecenterCache = maybeRecenterCache, .getSeed = getSeed, .getRegionInfo = getRegionInfo, .getColumnInfo = getColumnInfo, diff --git a/modules/worldgen-test/src/root.zig b/modules/worldgen-test/src/root.zig index 041a617f..9540f83b 100644 --- a/modules/worldgen-test/src/root.zig +++ b/modules/worldgen-test/src/root.zig @@ -10,8 +10,6 @@ const CHUNK_SIZE_X = world_core.CHUNK_SIZE_X; const CHUNK_SIZE_Y = world_core.CHUNK_SIZE_Y; const CHUNK_SIZE_Z = world_core.CHUNK_SIZE_Z; const BlockType = world_core.BlockType; -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; const build_options = @import("worldgen_test_options"); const LightingComputer = @import("worldgen-common").LightingComputer; @@ -26,7 +24,6 @@ pub const ShadowTestWorldGenerator = struct { const CAVE_MAX_Z: i32 = 14; const CAVE_MIN_Y: i32 = GROUND_Y + 1; const CAVE_MAX_Y: i32 = GROUND_Y + 11; - const GRASS_COLOR: u32 = 0xFF40A040; pub const INFO = GeneratorInfo{ .name = "Shadow Test Scene", @@ -187,39 +184,6 @@ pub const ShadowTestWorldGenerator = struct { } } - pub fn generateHeightmapOnly(self: *const ShadowTestWorldGenerator, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - _ = self; - _ = region_x; - _ = region_z; - _ = lod_level; - _ = stop_flag; - @memset(data.heightmap, @floatFromInt(GROUND_Y)); - @memset(data.biomes, .plains); - @memset(data.top_blocks, .grass); - @memset(data.colors, GRASS_COLOR); - @memset(data.material_layers, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }); - @memset(data.water, world_core.LODWaterState.empty); - @memset(data.lighting, world_core.LODLightingHint.daylight); - @memset(data.vegetation, world_core.LODVegetationHint.empty); - - if (data.hasVerticalSpans()) { - var gz: u32 = 0; - while (gz < data.width) : (gz += 1) { - var gx: u32 = 0; - while (gx < data.width) : (gx += 1) { - data.setGeneratedColumn(gx, gz, @floatFromInt(GROUND_Y), .plains, .{ .surface = .grass, .subsurface = .dirt, .foundation = .stone }, GRASS_COLOR, world_core.LODWaterState.empty, world_core.LODLightingHint.daylight, world_core.LODVegetationHint.empty); - } - } - } - } - - pub fn maybeRecenterCache(self: *ShadowTestWorldGenerator, player_x: i32, player_z: i32) bool { - _ = self; - _ = player_x; - _ = player_z; - return false; - } - pub fn getSeed(self: *const ShadowTestWorldGenerator) u64 { return self.seed; } @@ -242,8 +206,6 @@ pub const ShadowTestWorldGenerator = struct { const VTABLE = Generator.VTable{ .generate = generateWrapper, - .generateHeightmapOnly = generateHeightmapOnlyWrapper, - .maybeRecenterCache = maybeRecenterCacheWrapper, .getSeed = getSeedWrapper, .getRegionInfo = getRegionInfoWrapper, .getColumnInfo = getColumnInfoWrapper, @@ -256,16 +218,6 @@ pub const ShadowTestWorldGenerator = struct { try self.generate(chunk, stop_flag); } - fn generateHeightmapOnlyWrapper(ptr: *anyopaque, data: *LODSimplifiedData, region_x: i32, region_z: i32, lod_level: LODLevel, stop_flag: ?*const std.atomic.Value(bool)) void { - const self: *ShadowTestWorldGenerator = @ptrCast(@alignCast(ptr)); - self.generateHeightmapOnly(data, region_x, region_z, lod_level, stop_flag); - } - - fn maybeRecenterCacheWrapper(ptr: *anyopaque, player_x: i32, player_z: i32) bool { - const self: *ShadowTestWorldGenerator = @ptrCast(@alignCast(ptr)); - return self.maybeRecenterCache(player_x, player_z); - } - fn getSeedWrapper(ptr: *anyopaque) u64 { const self: *ShadowTestWorldGenerator = @ptrCast(@alignCast(ptr)); return self.getSeed(); diff --git a/phase5_benchmark_gate_tests.zig b/phase5_benchmark_gate_tests.zig deleted file mode 100644 index 0ba2bb5e..00000000 --- a/phase5_benchmark_gate_tests.zig +++ /dev/null @@ -1,179 +0,0 @@ -//! Machine-checkable Phase 5 benchmark policy contract. -//! -//! Runtime thresholds live in `game-core/benchmark.zig`; the benchmark and LOD -//! quality-control documents are checked against that source of truth here. - -const std = @import("std"); -const benchmark = @import("game-core").benchmark; -const session = @import("game-core").session; -const rhi = @import("engine-rhi"); - -const BENCHMARK_DOC = @embedFile("docs/benchmarks/README.md"); -const LOD_QUALITY_DOC = @embedFile("docs/lod-quality-controls.md"); - -const ScenarioContract = struct { - name: []const u8, - scenario: benchmark.Scenario, -}; - -const SCENARIOS = [_]ScenarioContract{ - .{ .name = "stationary", .scenario = .stationary }, - .{ .name = "traversal", .scenario = .traversal }, - .{ .name = "rapid-turn", .scenario = .rapid_turn }, - .{ .name = "teleport-eviction", .scenario = .teleport_eviction }, -}; - -test "Phase 5 recognizes exactly the four documented bounded scenarios" { - try std.testing.expectEqual(@as(usize, SCENARIOS.len), @typeInfo(benchmark.Scenario).@"enum".fields.len); - try std.testing.expect(std.mem.indexOf(u8, BENCHMARK_DOC, "deterministic, bounded camera path") != null); - - for (SCENARIOS) |contract| { - const parsed = try benchmark.Scenario.parse(contract.name); - try std.testing.expectEqual(contract.scenario, parsed); - try std.testing.expectEqualStrings(contract.name, parsed.name()); - } - - try std.testing.expectError(error.InvalidBenchmarkScenario, benchmark.Scenario.parse("unbounded")); -} - -test "Phase 5 visual motion scene names and parser match the automated matrix" { - const MotionScene = struct { - name: []const u8, - motion: session.Phase5VisualMotion, - }; - const scenes = [_]MotionScene{ - .{ .name = "lod-handoff-traversal", .motion = .lod_handoff_traversal }, - .{ .name = "fog-rapid-turn", .motion = .fog_rapid_turn }, - .{ .name = "teleport-handoff", .motion = .teleport_handoff }, - }; - - for (scenes) |scene| { - const parsed = session.parsePhase5VisualScene(scene.name) orelse return error.DocumentedMotionSceneMissing; - try std.testing.expectEqual(scene.motion, parsed.motion); - try std.testing.expect(std.mem.indexOf(u8, BENCHMARK_DOC, scene.name) != null); - } - try std.testing.expect(session.parsePhase5VisualScene("unbounded-motion") == null); -} - -test "Phase 5 benchmark SLO table matches runtime thresholds" { - const presets = [_][]const u8{ "low", "medium", "high", "ultra", "extreme" }; - - for (presets) |preset| { - const documented = try benchmarkSloRow(preset); - const runtime = benchmark.thresholdsForPreset(preset); - - try std.testing.expectEqual(runtime.fps_p1_min, documented[0]); - try std.testing.expectEqual(runtime.max_frame_ms, documented[1]); - try std.testing.expectEqual(runtime.draw_calls_max, documented[2]); - try std.testing.expectEqual(runtime.vertices_max, documented[3]); - try std.testing.expectEqual(runtime.gpu_memory_mb_max, documented[4]); - } -} - -test "Phase 5 LOD preset budget table matches runtime configuration" { - inline for (@typeInfo(rhi.RenderDistancePreset).@"enum".fields) |field| { - const preset: rhi.RenderDistancePreset = @enumFromInt(field.value); - const documented = try lodBudgetRow(field.name); - const runtime = rhi.getPresetConfig(preset); - - try std.testing.expectEqual(runtime.horizon_radius, documented.horizon_radius); - try std.testing.expectEqual(runtime.horizontal_detail, documented.horizontal_detail); - try std.testing.expectEqual(runtime.vertical_span_budget, documented.vertical_span_budget); - try std.testing.expectEqual(runtime.memory_budget_mb, documented.memory_budget_mb); - try std.testing.expectEqual(runtime.lod_store_size_cap_mb, documented.store_size_cap_mb); - try std.testing.expectEqual(runtime.max_uploads_per_frame, documented.max_uploads_per_frame); - } -} - -fn benchmarkSloRow(preset: []const u8) ![5]f64 { - const row = findTableRow(BENCHMARK_DOC, preset) orelse return error.DocumentedPresetMissing; - if (row.count != 6) return error.InvalidDocumentedPreset; - const cells = row.cells[0..row.count]; - - return .{ - try parseDocumentNumber(cells[1]), - try parseDocumentNumber(cells[2]), - try parseDocumentNumber(cells[3]), - try parseDocumentNumber(cells[4]), - try parseDocumentNumber(cells[5]), - }; -} - -const LodBudget = struct { - horizon_radius: i32, - horizontal_detail: [5]u32, - vertical_span_budget: u8, - memory_budget_mb: u32, - store_size_cap_mb: u32, - max_uploads_per_frame: u32, -}; - -fn lodBudgetRow(preset: []const u8) !LodBudget { - const row = findTableRow(LOD_QUALITY_DOC, preset) orelse return error.DocumentedPresetMissing; - if (row.count != 7) return error.InvalidDocumentedPreset; - const cells = row.cells[0..row.count]; - - return .{ - .horizon_radius = @intFromFloat(try parseDocumentNumber(cells[1])), - .horizontal_detail = try parseDetailBudget(cells[2]), - .vertical_span_budget = @intFromFloat(try parseDocumentNumber(cells[3])), - .memory_budget_mb = @intFromFloat(try parseDocumentNumber(cells[4])), - .store_size_cap_mb = @intFromFloat(try parseDocumentNumber(cells[5])), - .max_uploads_per_frame = @intFromFloat(try parseDocumentNumber(cells[6])), - }; -} - -const TableRow = struct { - cells: [8][]const u8, - count: usize, -}; - -/// Returns a Markdown table's cells, excluding its leading and trailing pipes. -fn findTableRow(document: []const u8, preset: []const u8) ?TableRow { - var lines = std.mem.splitScalar(u8, document, '\n'); - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r"); - if (!std.mem.startsWith(u8, trimmed, "|")) continue; - - var cells: [8][]const u8 = undefined; - var count: usize = 0; - var fields = std.mem.splitScalar(u8, trimmed, '|'); - _ = fields.next(); // Leading pipe. - while (fields.next()) |field| { - const cell = std.mem.trim(u8, field, " \t\r"); - if (cell.len == 0) continue; // Trailing pipe. - if (count == cells.len) break; - cells[count] = cell; - count += 1; - } - - if (count > 0 and std.ascii.eqlIgnoreCase(cells[0], preset)) return .{ .cells = cells, .count = count }; - } - return null; -} - -/// Accepts human-readable table values such as `3,500,000` and `2,200 MB`. -fn parseDocumentNumber(value: []const u8) !f64 { - var normalized: [64]u8 = undefined; - var len: usize = 0; - for (value) |byte| { - if (std.ascii.isDigit(byte) or byte == '.' or byte == '-') { - if (len == normalized.len) return error.DocumentedNumberTooLong; - normalized[len] = byte; - len += 1; - } - } - if (len == 0) return error.InvalidDocumentedNumber; - return std.fmt.parseFloat(f64, normalized[0..len]); -} - -fn parseDetailBudget(value: []const u8) ![5]u32 { - var detail: [5]u32 = undefined; - var fields = std.mem.splitScalar(u8, value, '/'); - for (&detail) |*entry| { - const field = fields.next() orelse return error.InvalidDetailBudget; - entry.* = @intFromFloat(try parseDocumentNumber(field)); - } - if (fields.next() != null) return error.InvalidDetailBudget; - return detail; -} diff --git a/scripts/benchmark_baseline.py b/scripts/benchmark_baseline.py index 462d8cef..cceda853 100644 --- a/scripts/benchmark_baseline.py +++ b/scripts/benchmark_baseline.py @@ -1,61 +1,18 @@ #!/usr/bin/env python3 -"""Validate, pair, assemble, and compare reproducible benchmark evidence. +"""Validate and assemble ordinary benchmark result artifacts.""" -Steady-state counters in a result are sampled deltas. `startup_evidence` is -intentionally cumulative at readiness, so a compact pair can make honest -startup and resident-geometry claims without treating warmup as steady state. -""" from __future__ import annotations import argparse import json import math -import tempfile from pathlib import Path from typing import Any -SCHEMA_VERSION = 3 + +SCHEMA_VERSION = 4 PRESETS = ("low", "medium", "high") -POLICY_PRESETS = PRESETS + ("ultra", "extreme") SCENARIOS = ("stationary", "traversal", "rapid-turn", "teleport-eviction") -PROVENANCE_PATHS = ( - "world_seed", "build.mode", "build.world", "build.fixture", "build.headless", - "build.resolution", "provenance.gpu_adapter", "provenance.gpu_driver", - "provenance.runner", "provenance.zig_toolchain", -) -HORIZON_PROVENANCE_PATH = "build.horizon_distance" -LOD_MEMORY_BUDGET_PATH = "build.benchmark_lod_memory_budget_mb" -LOD_READINESS_TARGET_PATH = "build.benchmark_require_gpu_candidates" -PAIR_COMPATIBILITY_PATHS = ("preset", "scenario", *PROVENANCE_PATHS, "duration_s", "completion.requested_duration_s") -RESULT_COMPATIBILITY_PATHS = (*PROVENANCE_PATHS, "duration_s", "completion.requested_duration_s") -STARTUP_NUMERIC_PATHS = ( - "startup_evidence.readiness_elapsed_s", "startup_evidence.upload_total_bytes", - "startup_evidence.far_expanded_upload_bytes", "startup_evidence.compact_upload_bytes", - "startup_evidence.worker_generation_total_ms", - "startup_evidence.worker_mesh_construction_total_ms", - "startup_evidence.worker_far_expanded_mesh_construction_ms", - "startup_evidence.worker_compact_encode_ms", - "startup_evidence.compact_submissions", - "startup_evidence.pool_gpu_allocated_bytes", - "startup_evidence.direct_mesh_gpu_bytes", - "startup_evidence.compact_pool_allocated_bytes", - "startup_evidence.pool_cpu_shadow_bytes", -) -MATERIAL_REDUCTION = 0.20 -GPU_CULLING_MIN_DURATION_S = 60 -GPU_CULLING_MIN_HORIZON_DISTANCE = 4096 -GPU_CULLING_FIXTURE = "gpu-culling-scale" -GPU_CULLING_MIN_READINESS_TARGET = 1024 -GPU_CULLING_CPU_P95_IMPROVEMENT = 0.01 -GPU_CULLING_CPU_P99_REGRESSION = 0.01 -GPU_CULLING_TOTAL_GPU_P99_REGRESSION = 0.01 - - -def get_path(value: dict[str, Any], path: str) -> Any: - current: Any = value - for part in path.split("."): - current = current[part] - return current def load(path: Path) -> dict[str, Any]: @@ -67,542 +24,161 @@ def load(path: Path) -> dict[str, Any]: def require(value: dict[str, Any], path: str, source: str) -> Any: + current: Any = value try: - return get_path(value, path) + for part in path.split("."): + current = current[part] except (KeyError, TypeError) as exc: raise ValueError(f"{source}: missing {path}") from exc + return current -def optional(value: dict[str, Any], path: str) -> Any | None: - try: - return get_path(value, path) - except (KeyError, TypeError): - return None - - -def require_compatible(left: dict[str, Any], right: dict[str, Any], paths: tuple[str, ...], source: str) -> None: - for path in paths: - if require(left, path, f"{source}/left") != require(right, path, f"{source}/right"): - raise ValueError(f"{source}: incompatible {path}") - # Schema-v3 captures before the independent horizon option lack this field. - # Preserve validation of checked historical evidence, but never allow a new - # horizon-recording result to be compared to such a legacy artifact. - left_horizon = optional(left, HORIZON_PROVENANCE_PATH) - right_horizon = optional(right, HORIZON_PROVENANCE_PATH) - if left_horizon != right_horizon: - raise ValueError(f"{source}: incompatible {HORIZON_PROVENANCE_PATH}") - # Default new controls for schema-v3 captures that predate them, but never - # pair explicit captures with a different override or readiness gate. - for path in (LOD_MEMORY_BUDGET_PATH, LOD_READINESS_TARGET_PATH): - if (optional(left, path) or 0) != (optional(right, path) or 0): - raise ValueError(f"{source}: incompatible {path}") - - -def is_known_label(value: Any) -> bool: - return isinstance(value, str) and value.strip() and value.strip().lower() not in {"unknown", "unspecified", "n/a"} - - -def is_number(value: Any) -> bool: - return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) +def require_number(value: dict[str, Any], path: str, source: str) -> None: + number = require(value, path, source) + if not isinstance(number, (int, float)) or isinstance(number, bool): + raise ValueError(f"{source}: {path} must be numeric") + if not math.isfinite(number): + raise ValueError(f"{source}: {path} must be finite") -def validate_result(value: dict[str, Any], source: str = "result") -> None: +def validate_result(value: dict[str, Any], source: str) -> None: if value.get("schema_version") != SCHEMA_VERSION or value.get("artifact_type") != "benchmark-result": raise ValueError(f"{source}: requires benchmark-result schema_version {SCHEMA_VERSION}") - if require(value, "preset", source) not in POLICY_PRESETS or require(value, "scenario", source) not in SCENARIOS: - raise ValueError(f"{source}: unknown preset/scenario") - if require(value, "completion.scenario_completed", source) is not True: - raise ValueError(f"{source}: scenario completion evidence is absent") - if require(value, "completion.warmup_ready", source) is not True or require(value, "completion.warmup_timed_out", source) is not False: - raise ValueError(f"{source}: readiness warmup did not complete") - if require(value, "completion.sampled_duration_s", source) < require(value, "completion.requested_duration_s", source) or require(value, "completion.sampled_frame_count", source) <= 0: - raise ValueError(f"{source}: sampled duration/frame evidence is incomplete") - if not is_number(require(value, "duration_s", source)) or require(value, "duration_s", source) != require(value, "completion.requested_duration_s", source): - raise ValueError(f"{source}: configured duration must match requested duration evidence") - if require(value, "lod.profiling_enabled", source) is not True or require(value, "lod.profiling_frame_count", source) != require(value, "completion.sampled_frame_count", source): - raise ValueError(f"{source}: LOD profiling does not cover every sampled frame") - result_horizon = optional(value, "horizon_distance") - build_horizon = optional(value, HORIZON_PROVENANCE_PATH) - if result_horizon is not None or build_horizon is not None: - if not all(isinstance(entry, int) and not isinstance(entry, bool) and entry > 0 for entry in (result_horizon, build_horizon)) or result_horizon != build_horizon: - raise ValueError(f"{source}: horizon_distance and {HORIZON_PROVENANCE_PATH} must be matching positive integers") - memory_budget = optional(value, LOD_MEMORY_BUDGET_PATH) or 0 - readiness_target = optional(value, LOD_READINESS_TARGET_PATH) or 0 - if not isinstance(memory_budget, int) or isinstance(memory_budget, bool) or not 0 <= memory_budget <= 4096: - raise ValueError(f"{source}: {LOD_MEMORY_BUDGET_PATH} must be an integer from 0 to 4096") - if not isinstance(readiness_target, int) or isinstance(readiness_target, bool) or readiness_target < 0: - raise ValueError(f"{source}: {LOD_READINESS_TARGET_PATH} must be a nonnegative integer") - completion_region_target = optional(value, "completion.lod_renderable_region_target") or 0 - completion_candidate_target = optional(value, "completion.gpu_candidate_target") or 0 - if completion_region_target != readiness_target or completion_candidate_target != readiness_target: - raise ValueError(f"{source}: completion readiness targets must match {LOD_READINESS_TARGET_PATH}") - if readiness_target: - if require(value, "startup_evidence.lod_renderable_regions", source) < readiness_target: - raise ValueError(f"{source}: common renderable LOD-region readiness target was not met") - if require(value, "lod.gpu_culling.requested", source) is True and require(value, "lod.gpu_culling.candidate_count_max", source) < readiness_target: - raise ValueError(f"{source}: GPU candidate max is below its documented readiness target") - for path in ("lod.pressure.wait_idle_count_total", "lod.pressure.gpu_culling_overflows_max", "lod.pressure.gpu_culling_validation_mismatches_max"): - if require(value, path, source) != 0: - raise ValueError(f"{source}: {path} must be zero") - for path in ("gpu_ms.total.p95", "gpu_ms.total.p99", "lod.cpu_frame_ms.p95", "lod.cpu_frame_ms.p99", "lod.gpu_frame_ms.p95", "lod.gpu_frame_ms.p99", "lod.memory_bytes.logical_ram_bytes.p95_bytes", "lod.memory_bytes.logical_ram_bytes.p99_bytes", "lod.memory_bytes.logical_vram_bytes.p95_bytes", "lod.memory_bytes.logical_vram_bytes.p99_bytes"): - if not is_number(require(value, path, source)): - raise ValueError(f"{source}: {path} must be numeric") - for path in ("lod.memory_bytes.logical_ram_bytes.max_bytes", "lod.memory_bytes.logical_vram_bytes.max_bytes", "lod.memory_bytes.logical_ram_bytes.p50_bytes", "lod.memory_bytes.logical_vram_bytes.p50_bytes"): - if not is_number(require(value, path, source)) or require(value, path, source) <= 0: - raise ValueError(f"{source}: {path} must be positive LOD memory evidence") - if require(value, "completion.evidence_mode", source) is True: - if require(value, "startup_evidence.readiness_observed", source) is not True: - raise ValueError(f"{source}: readiness startup evidence is absent") - for path in STARTUP_NUMERIC_PATHS: - if not is_number(require(value, path, source)) or require(value, path, source) < 0: - raise ValueError(f"{source}: {path} must be a nonnegative number") - for path in PROVENANCE_PATHS[-4:]: - if not is_known_label(require(value, path, source)): - raise ValueError(f"{source}: evidence mode requires a known {path}") - - -def startup_far_geometry(value: dict[str, Any]) -> int: - """Resident representation-owned far geometry, excluding the near pool.""" - return sum(int(require(value, f"startup_evidence.{field}", "result")) for field in ("direct_mesh_gpu_bytes", "compact_pool_allocated_bytes")) - - -def required_reduction(off: float, auto: float, label: str) -> dict[str, Any]: - """Enforce a reduction only when the off run has a nonzero basis.""" - if off == 0: - return {"off": off, "auto": auto, "status": "not-applicable-off-zero", "reduction_fraction": None, "delta": auto - off} - fraction = (off - auto) / off - if fraction + 1e-12 < MATERIAL_REDUCTION: - raise ValueError(f"{label}: reduction {fraction:.2%} is below required {MATERIAL_REDUCTION:.0%}") - return {"off": off, "auto": auto, "status": "measured", "reduction_fraction": fraction, "delta": auto - off} - - -def startup_comparison(off: dict[str, Any], auto: dict[str, Any], field: str) -> dict[str, Any]: - off_value = require(off, f"startup_evidence.{field}", "off") - auto_value = require(auto, f"startup_evidence.{field}", "auto") - if off_value == 0 and auto_value == 0: - return {"off": off_value, "auto": auto_value, "status": "not-applicable-both-zero", "delta": None} - return {"off": off_value, "auto": auto_value, "status": "measured", "delta": auto_value - off_value} - - -def worker_representation_reduction(off: dict[str, Any], auto: dict[str, Any], label: str) -> dict[str, Any]: - """Compare far worker work only when both representation clocks are usable.""" - off_value = ( - require(off, "startup_evidence.worker_far_expanded_mesh_construction_ms", "off") - + require(off, "startup_evidence.worker_compact_encode_ms", "off") - ) - auto_value = ( - require(auto, "startup_evidence.worker_far_expanded_mesh_construction_ms", "auto") - + require(auto, "startup_evidence.worker_compact_encode_ms", "auto") - ) - if off_value > 0 and auto_value > 0: - return required_reduction(off_value, auto_value, label) - return { - "off": off_value, - "auto": auto_value, - "status": "not-comparable-missing-stable-worker-basis", - "reduction_fraction": None, - "delta": auto_value - off_value, - } - - -def validate_compact_result(value: dict[str, Any], source: str = "result") -> None: - validate_result(value, source) - mode = value.get("compact_mode") - if mode not in ("off", "auto"): - raise ValueError(f"{source}: compact_mode must be off or auto") - if require(value, "completion.evidence_mode", source) is not True: - raise ValueError(f"{source}: compact evidence requires evidence mode") - if mode == "auto" and require(value, "completion.compact_ready", source) is not True: - raise ValueError(f"{source}: auto compact evidence did not become ready") - - -def validate_pair(off: dict[str, Any], auto: dict[str, Any], source: str) -> dict[str, Any]: - validate_compact_result(off, f"{source}/off") - validate_compact_result(auto, f"{source}/auto") - if off.get("compact_mode") != "off" or auto.get("compact_mode") != "auto": - raise ValueError(f"{source}: expected off/auto compact pair") - require_compatible(off, auto, PAIR_COMPATIBILITY_PATHS, source) - if require(off, "build.fixture", f"{source}/off") == GPU_CULLING_FIXTURE: - raise ValueError(f"{source}: gpu-culling-scale is dedicated culling evidence, not a normal compact baseline") - off_residency = require(off, "startup_evidence.compact_pool_allocated_bytes", "off") - auto_residency = require(auto, "startup_evidence.compact_pool_allocated_bytes", "auto") - off_submissions = require(off, "startup_evidence.compact_submissions", "off") - auto_submissions = require(auto, "startup_evidence.compact_submissions", "auto") - if off_residency != 0 or off_submissions != 0: - raise ValueError(f"{source}: off must have zero compact readiness residency and submissions") - if auto_residency <= 0 or auto_submissions <= 0: - raise ValueError(f"{source}: auto requires nonzero compact readiness residency and submissions") - comparisons = { - "far_resident_geometry": required_reduction(startup_far_geometry(off), startup_far_geometry(auto), f"{source}: far resident geometry"), - "far_representation_upload_bytes": required_reduction( - require(off, "startup_evidence.far_expanded_upload_bytes", "off") + require(off, "startup_evidence.compact_upload_bytes", "off"), - require(auto, "startup_evidence.far_expanded_upload_bytes", "auto") + require(auto, "startup_evidence.compact_upload_bytes", "auto"), - f"{source}: far representation upload bytes", - ), - "worker_far_representation_work": worker_representation_reduction(off, auto, f"{source}: worker far representation work"), - # Contextual totals are retained for diagnostics, but near streaming - # makes them invalid acceptance metrics for the far representation. - "near_pool_geometry": startup_comparison(off, auto, "pool_gpu_allocated_bytes"), - "cpu_expanded_shadow": startup_comparison(off, auto, "pool_cpu_shadow_bytes"), - "startup_upload_bytes": startup_comparison(off, auto, "upload_total_bytes"), - "startup_worker_mesh_construction_ms": startup_comparison(off, auto, "worker_mesh_construction_total_ms"), - } - return comparisons - - -def stamp_compact(path: Path, mode: str) -> None: - if mode not in ("off", "auto"): - raise ValueError(f"unknown compact mode {mode}") - value = load(path) - value["compact_mode"] = mode - validate_compact_result(value, str(path)) - path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") - - -def validate_compact_matrix(root: Path) -> None: + if value.get("preset") not in (*PRESETS, "ultra", "extreme"): + raise ValueError(f"{source}: unknown preset") + if value.get("scenario") not in SCENARIOS: + raise ValueError(f"{source}: unknown scenario") + for path in ( + "world_seed", + "render_distance", + "frames", + "duration_s", + "fps.avg", + "fps.p1", + "frame_ms.avg", + "frame_ms.p99", + "gpu_ms.total.avg", + "draw_calls_avg", + "vertices_avg", + "chunks_rendered_avg", + "gpu_memory_mb_max", + "completion.sampled_duration_s", + "completion.requested_duration_s", + "completion.sampled_frame_count", + ): + require_number(value, path, source) + if require(value, "frames", source) <= 0 or require(value, "duration_s", source) <= 0: + raise ValueError(f"{source}: benchmark has no sampled frames or duration") + completion = require(value, "completion", source) + if not isinstance(completion, dict) or completion.get("scenario_completed") is not True or completion.get("warmup_ready") is not True or completion.get("warmup_timed_out") is not False: + raise ValueError(f"{source}: benchmark completion evidence is incomplete") + if completion["sampled_duration_s"] < completion["requested_duration_s"] or completion["sampled_frame_count"] != value["frames"]: + raise ValueError(f"{source}: benchmark completion evidence does not match samples") + for path in ("build.mode", "build.world", "provenance.gpu_adapter", "provenance.gpu_driver", "provenance.runner", "provenance.zig_toolchain"): + if not isinstance(require(value, path, source), str): + raise ValueError(f"{source}: {path} must be a string") + legacy_fixture = value.get("build", {}).get("fixture") + if legacy_fixture not in (None, ""): + raise ValueError(f"{source}: benchmark fixture metadata is no longer supported") + if require(value, "build.headless", source) is not True: + raise ValueError(f"{source}: benchmark results must be headless") + resolution = require(value, "build.resolution", source) + if not isinstance(resolution, list) or len(resolution) != 2 or not all(isinstance(entry, int) and entry > 0 for entry in resolution): + raise ValueError(f"{source}: build.resolution must contain two positive dimensions") + + +def validate_baseline(value: dict[str, Any], source: str) -> None: + if value.get("schema_version") != SCHEMA_VERSION or value.get("artifact_type") != "benchmark-baseline": + raise ValueError(f"{source}: requires benchmark-baseline schema_version {SCHEMA_VERSION}") + results = require(value, "results", source) + if not isinstance(results, dict): + raise ValueError(f"{source}: results must be an object") for preset in PRESETS: for scenario in SCENARIOS: - off = load(root / "off" / preset / f"{scenario}.json") - auto = load(root / "auto" / preset / f"{scenario}.json") - validate_pair(off, auto, f"compact pair {preset}/{scenario}") - - -def artifact_paths(inputs: list[Path]) -> list[Path]: - paths: list[Path] = [] - for input_path in inputs: - paths.extend(sorted(input_path.rglob("*.json")) if input_path.is_dir() else [input_path]) - return paths + validate_result(require(results, f"{preset}.{scenario}", source), f"{source}:{preset}/{scenario}") -def assemble(inputs: list[Path], output: Path, overwrite: bool) -> None: +def assemble(paths: list[Path], output: Path, overwrite: bool) -> None: if output.exists() and not overwrite: raise ValueError(f"refusing to overwrite {output}; pass --overwrite") - pairs: dict[str, dict[str, dict[str, dict[str, Any]]]] = {preset: {} for preset in PRESETS} - provenance: dict[str, Any] | None = None - capture_duration_s: float | int | None = None - for path in artifact_paths(inputs): + results: dict[str, dict[str, dict[str, Any]]] = {preset: {} for preset in PRESETS} + for path in paths: value = load(path) - validate_compact_result(value, str(path)) - preset, scenario, mode = value["preset"], value["scenario"], value["compact_mode"] - if preset not in PRESETS: - continue - slot = pairs[preset].setdefault(scenario, {}) - if mode in slot: - raise ValueError(f"duplicate {mode} artifact for {preset}/{scenario}: {path}") - current = {key: require(value, key, str(path)) for key in PROVENANCE_PATHS} - if provenance is None: - provenance = current - elif current != provenance: - raise ValueError(f"{path}: incompatible provenance {current!r}, expected {provenance!r}") - duration = require(value, "completion.requested_duration_s", str(path)) - if capture_duration_s is None: - capture_duration_s = duration - elif duration != capture_duration_s: - raise ValueError(f"{path}: incompatible requested duration {duration!r}, expected {capture_duration_s!r}") - slot[mode] = value - missing = [f"{preset}/{scenario}/{mode}" for preset in PRESETS for scenario in SCENARIOS for mode in ("off", "auto") if mode not in pairs[preset].get(scenario, {})] + validate_result(value, str(path)) + preset, scenario = value["preset"], value["scenario"] + if preset in results: + if scenario in results[preset]: + raise ValueError(f"duplicate benchmark result for {preset}/{scenario}: {path}") + results[preset][scenario] = value + missing = [f"{preset}/{scenario}" for preset in PRESETS for scenario in SCENARIOS if scenario not in results[preset]] if missing: - raise ValueError("missing required paired captures: " + ", ".join(missing)) - comparisons: dict[str, dict[str, Any]] = {preset: {} for preset in PRESETS} - results: dict[str, dict[str, Any]] = {preset: {} for preset in PRESETS} - for preset in PRESETS: - for scenario in SCENARIOS: - pair = pairs[preset][scenario] - comparisons[preset][scenario] = {"off": pair["off"], "auto": pair["auto"], "comparison": validate_pair(pair["off"], pair["auto"], f"baseline pair {preset}/{scenario}")} - results[preset][scenario] = pair["auto"] - baseline = { - "schema_version": SCHEMA_VERSION, "artifact_type": "benchmark-baseline", "status": "ready", "generated": True, - "required_presets": list(PRESETS), "required_scenarios": list(SCENARIOS), "provenance": provenance, - "capture_duration_s": capture_duration_s, - "results": results, "compact_pairs": comparisons, - } - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(baseline, indent=2) + "\n", encoding="utf-8") - - -def validate_baseline(path: Path, allow_insufficient: bool) -> None: - baseline = load(path) - if baseline.get("status") == "insufficient": - if allow_insufficient: - return - raise ValueError(f"{path}: baseline is insufficient: {baseline.get('reason', 'no reason supplied')}") - if baseline.get("schema_version") != SCHEMA_VERSION or baseline.get("artifact_type") != "benchmark-baseline" or baseline.get("status") != "ready": - raise ValueError(f"{path}: not a ready schema-v{SCHEMA_VERSION} benchmark baseline") - for preset in PRESETS: - for scenario in SCENARIOS: - auto = require(baseline, f"results.{preset}.{scenario}", str(path)) - pair = require(baseline, f"compact_pairs.{preset}.{scenario}", str(path)) - if auto != require(pair, "auto", str(path)): - raise ValueError(f"{path}: results {preset}/{scenario} is not its paired auto artifact") - actual = validate_pair(require(pair, "off", str(path)), require(pair, "auto", str(path)), f"{path}:{preset}/{scenario}") - if require(pair, "comparison", str(path)) != actual: - raise ValueError(f"{path}: stored paired comparison does not match source evidence for {preset}/{scenario}") - if require(auto, "completion.requested_duration_s", str(path)) != require(baseline, "capture_duration_s", str(path)): - raise ValueError(f"{path}: baseline mixes requested benchmark durations") - for key in PROVENANCE_PATHS: - if require(auto, key, str(path)) != require(baseline, "provenance", str(path)).get(key): - raise ValueError(f"{path}: baseline provenance does not match {preset}/{scenario} {key}") - - -def require_positive_number(value: dict[str, Any], path: str, source: str) -> float: - number = require(value, path, source) - if not is_number(number) or number <= 0: - raise ValueError(f"{source}: {path} must be a positive finite number") - return float(number) - - -def culling_comparison(cpu: dict[str, Any], gpu: dict[str, Any], source: str) -> dict[str, Any]: - """Validate a matched CPU/GPU culling pair and return its derived proof. - - Ratios intentionally use the raw artifact values: this is an acceptance - gate, so there is no rounding or epsilon that could turn a near miss into a - pass. - """ - validate_result(cpu, f"{source}/cpu") - validate_result(gpu, f"{source}/gpu") - require_compatible(cpu, gpu, PAIR_COMPATIBILITY_PATHS, source) - if require(cpu, "preset", f"{source}/cpu") not in ("high", "extreme") or require(cpu, "scenario", f"{source}/cpu") != "traversal": - raise ValueError(f"{source}: requires high or extreme traversal results") - for value, label in ((cpu, "cpu"), (gpu, "gpu")): - if require(value, "build.fixture", f"{source}/{label}") != GPU_CULLING_FIXTURE: - raise ValueError(f"{source}: {label} must use fixture {GPU_CULLING_FIXTURE!r}") - if cpu.get("compact_mode") != "auto" or gpu.get("compact_mode") != "auto": - raise ValueError(f"{source}: requires auto-compact source results") - if require(cpu, "completion.requested_duration_s", f"{source}/cpu") < GPU_CULLING_MIN_DURATION_S: - raise ValueError(f"{source}: requires at least {GPU_CULLING_MIN_DURATION_S} sampled seconds") - horizon_distance = require(cpu, HORIZON_PROVENANCE_PATH, f"{source}/cpu") - if not isinstance(horizon_distance, int) or isinstance(horizon_distance, bool) or horizon_distance < GPU_CULLING_MIN_HORIZON_DISTANCE: - raise ValueError(f"{source}: requires a documented horizon distance of at least {GPU_CULLING_MIN_HORIZON_DISTANCE} chunks") - memory_budget = require(cpu, LOD_MEMORY_BUDGET_PATH, f"{source}/cpu") - readiness_target = require(cpu, LOD_READINESS_TARGET_PATH, f"{source}/cpu") - if not isinstance(memory_budget, int) or isinstance(memory_budget, bool) or not 0 < memory_budget <= 4096: - raise ValueError(f"{source}: requires a documented nonzero LOD memory budget no larger than 4096 MiB") - if not isinstance(readiness_target, int) or isinstance(readiness_target, bool) or readiness_target < GPU_CULLING_MIN_READINESS_TARGET: - raise ValueError(f"{source}: requires a documented readiness target of at least {GPU_CULLING_MIN_READINESS_TARGET}") - for value, label in ((cpu, "cpu"), (gpu, "gpu")): - if require(value, "completion.lod_renderable_region_target", f"{source}/{label}") != readiness_target or require(value, "completion.gpu_candidate_target", f"{source}/{label}") != readiness_target: - raise ValueError(f"{source}: {label} completion readiness provenance differs from build target") - if require(value, "startup_evidence.lod_renderable_regions", f"{source}/{label}") < readiness_target: - raise ValueError(f"{source}: {label} did not meet the common renderable-region readiness target") - if require(cpu, "lod.gpu_culling.requested", f"{source}/cpu") is not False: - raise ValueError(f"{source}: CPU source must explicitly request GPU culling off") - if require(gpu, "lod.gpu_culling.requested", f"{source}/gpu") is not True: - raise ValueError(f"{source}: GPU source must explicitly request GPU culling on") - - threshold = require(gpu, "lod.gpu_culling.threshold", f"{source}/gpu") - candidate_max = require(gpu, "lod.gpu_culling.candidate_count_max", f"{source}/gpu") - candidate_total = require(gpu, "lod.gpu_culling.candidate_count_total", f"{source}/gpu") - draw_submissions = require(gpu, "lod.gpu_culling.draw_submission_count_total", f"{source}/gpu") - if not all(isinstance(entry, int) and not isinstance(entry, bool) for entry in (threshold, candidate_max, candidate_total, draw_submissions)): - raise ValueError(f"{source}: GPU culling count evidence must be integer-valued") - if threshold <= 0 or candidate_max < readiness_target or candidate_total <= threshold or draw_submissions <= 0: - raise ValueError(f"{source}: GPU culling did not reach its documented candidate readiness target") - completed_generation = require(gpu, "lod.gpu_culling.validation_completed_generation", f"{source}/gpu") - completed_count = require(gpu, "lod.gpu_culling.validation_completed_count", f"{source}/gpu") - generation = require(gpu, "lod.gpu_culling.validation_generation", f"{source}/gpu") - if not all(isinstance(entry, int) and not isinstance(entry, bool) for entry in (generation, completed_generation, completed_count)): - raise ValueError(f"{source}: delayed validation evidence must be integer-valued") - if completed_count <= 0 or completed_generation <= 0 or generation < completed_generation: - raise ValueError(f"{source}: delayed GPU validation did not complete") - # Timestamp queries are consumed after frame-slot reuse, so short gaps can - # legitimately make the median zero even while thousands of validated - # dispatches are sampled. Keep p50/p95 reported and finite; positive average - # and p99 plus dispatch/validation counters prove active compute work. - for percentile in ("p50", "p95"): - value = require(gpu, f"gpu_ms.lod_culling.{percentile}", f"{source}/gpu") - if not is_number(value) or value < 0: - raise ValueError(f"{source}/gpu: gpu_ms.lod_culling.{percentile} must be a nonnegative finite number") - require_positive_number(gpu, "gpu_ms.lod_culling.avg", f"{source}/gpu") - require_positive_number(gpu, "gpu_ms.lod_culling.p99", f"{source}/gpu") - - # `frame_ms` is the measured CPU frame duration. The LOD-only category is - # retained in the source artifacts for attribution, but includes candidate - # preparation that intentionally trades CPU setup for indirect submission. - cpu_p95 = require_positive_number(cpu, "frame_ms.p95", f"{source}/cpu") - cpu_p99 = require_positive_number(cpu, "frame_ms.p99", f"{source}/cpu") - gpu_p95 = require_positive_number(gpu, "frame_ms.p95", f"{source}/gpu") - gpu_p99 = require_positive_number(gpu, "frame_ms.p99", f"{source}/gpu") - cpu_total_gpu_p99 = require_positive_number(cpu, "gpu_ms.total.p99", f"{source}/cpu") - gpu_total_gpu_p99 = require_positive_number(gpu, "gpu_ms.total.p99", f"{source}/gpu") - cpu_draw_calls = require_positive_number(cpu, "draw_calls_avg", f"{source}/cpu") - gpu_draw_calls = require_positive_number(gpu, "draw_calls_avg", f"{source}/gpu") - p95_improvement = (cpu_p95 - gpu_p95) / cpu_p95 - p99_regression = (gpu_p99 - cpu_p99) / cpu_p99 - total_gpu_p99_regression = (gpu_total_gpu_p99 - cpu_total_gpu_p99) / cpu_total_gpu_p99 - if p95_improvement < GPU_CULLING_CPU_P95_IMPROVEMENT: - raise ValueError(f"{source}: CPU frame p95 improvement {p95_improvement:.2%} is below {GPU_CULLING_CPU_P95_IMPROVEMENT:.0%}") - if p99_regression > GPU_CULLING_CPU_P99_REGRESSION: - raise ValueError(f"{source}: CPU frame p99 regression {p99_regression:.2%} exceeds {GPU_CULLING_CPU_P99_REGRESSION:.0%}") - if total_gpu_p99_regression > GPU_CULLING_TOTAL_GPU_P99_REGRESSION: - raise ValueError(f"{source}: total GPU p99 regression {total_gpu_p99_regression:.2%} exceeds {GPU_CULLING_TOTAL_GPU_P99_REGRESSION:.0%}") - return { - "cpu_frame_ms": {"cpu": {"p95": cpu_p95, "p99": cpu_p99}, "gpu": {"p95": gpu_p95, "p99": gpu_p99}, "p95_improvement_fraction": p95_improvement, "p99_regression_fraction": p99_regression}, - "gpu_total_ms": {"cpu": {"p99": cpu_total_gpu_p99}, "gpu": {"p99": gpu_total_gpu_p99}, "p99_regression_fraction": total_gpu_p99_regression}, - "draw_calls": {"cpu": cpu_draw_calls, "gpu": gpu_draw_calls, "delta": gpu_draw_calls - cpu_draw_calls}, - "gpu_culling": {"fixture": GPU_CULLING_FIXTURE, "horizon_distance": horizon_distance, "lod_memory_budget_mb": memory_budget, "readiness_target": readiness_target, "threshold": threshold, "candidate_count_total": candidate_total, "candidate_count_max": candidate_max, "draw_submission_count_total": draw_submissions, "validation_generation": generation, "validation_completed_generation": completed_generation, "validation_completed_count": completed_count, "timestamp_ms": {key: require(gpu, f"gpu_ms.lod_culling.{key}", f"{source}/gpu") for key in ("p50", "p95", "p99")}}, - } - - -def assemble_gpu_culling(cpu_path: Path, gpu_path: Path, output: Path, overwrite: bool) -> None: - if output.exists() and not overwrite: - raise ValueError(f"refusing to overwrite {output}; pass --overwrite") - cpu, gpu = load(cpu_path), load(gpu_path) - comparison = culling_comparison(cpu, gpu, "GPU culling pair") - artifact = { - "schema_version": SCHEMA_VERSION, - "artifact_type": "gpu-culling-baseline", - "status": "ready", - "generated": True, - "policy": {"fixture": GPU_CULLING_FIXTURE, "minimum_duration_s": GPU_CULLING_MIN_DURATION_S, "minimum_horizon_distance_chunks": GPU_CULLING_MIN_HORIZON_DISTANCE, "minimum_readiness_target": GPU_CULLING_MIN_READINESS_TARGET, "cpu_frame_p95_improvement_min_fraction": GPU_CULLING_CPU_P95_IMPROVEMENT, "cpu_frame_p99_regression_max_fraction": GPU_CULLING_CPU_P99_REGRESSION, "total_gpu_p99_regression_max_fraction": GPU_CULLING_TOTAL_GPU_P99_REGRESSION}, - "sources": {"cpu": cpu, "gpu": gpu}, - "comparison": comparison, - } + raise ValueError("missing benchmark results: " + ", ".join(missing)) output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(artifact, indent=2) + "\n", encoding="utf-8") - + output.write_text(json.dumps({"schema_version": SCHEMA_VERSION, "artifact_type": "benchmark-baseline", "results": results}, indent=2) + "\n", encoding="utf-8") -def validate_gpu_culling_baseline(path: Path, allow_insufficient: bool) -> None: - artifact = load(path) - if artifact.get("status") == "insufficient": - if allow_insufficient: - return - raise ValueError(f"{path}: GPU culling baseline is insufficient: {artifact.get('reason', 'no reason supplied')}") - if artifact.get("schema_version") != SCHEMA_VERSION or artifact.get("artifact_type") != "gpu-culling-baseline" or artifact.get("status") != "ready": - raise ValueError(f"{path}: not a ready schema-v{SCHEMA_VERSION} GPU culling baseline") - expected_policy = {"fixture": GPU_CULLING_FIXTURE, "minimum_duration_s": GPU_CULLING_MIN_DURATION_S, "minimum_horizon_distance_chunks": GPU_CULLING_MIN_HORIZON_DISTANCE, "minimum_readiness_target": GPU_CULLING_MIN_READINESS_TARGET, "cpu_frame_p95_improvement_min_fraction": GPU_CULLING_CPU_P95_IMPROVEMENT, "cpu_frame_p99_regression_max_fraction": GPU_CULLING_CPU_P99_REGRESSION, "total_gpu_p99_regression_max_fraction": GPU_CULLING_TOTAL_GPU_P99_REGRESSION} - if require(artifact, "policy", str(path)) != expected_policy: - raise ValueError(f"{path}: GPU culling policy differs from the checked gate") - actual = culling_comparison(require(artifact, "sources.cpu", str(path)), require(artifact, "sources.gpu", str(path)), str(path)) - if require(artifact, "comparison", str(path)) != actual: - raise ValueError(f"{path}: stored GPU culling comparison does not match full source artifacts") - -def compatibility(baseline_path: Path, result_path: Path, preset: str, scenario: str) -> None: - validate_baseline(baseline_path, False) - result = load(result_path) - validate_compact_result(result, str(result_path)) - if result.get("compact_mode") != "auto": - raise ValueError(f"{result_path}: regression comparison requires intended auto compact evidence") - baseline = load(baseline_path) - expected = require(baseline, f"results.{preset}.{scenario}", str(baseline_path)) - if result.get("preset") != preset or result.get("scenario") != scenario: - raise ValueError(f"{result_path}: does not match requested {preset}/{scenario}") - require_compatible(result, expected, RESULT_COMPATIBILITY_PATHS, "benchmark provenance") +def compatibility(baseline: Path, result: Path, preset: str, scenario: str) -> None: + baseline_value = load(baseline) + validate_baseline(baseline_value, str(baseline)) + result_value = load(result) + validate_result(result_value, str(result)) + expected = require(baseline_value, f"results.{preset}.{scenario}", str(baseline)) + if result_value["preset"] != preset or result_value["scenario"] != scenario: + raise ValueError(f"{result}: does not match requested {preset}/{scenario}") + for path in ("preset", "scenario", "world_seed", "render_distance", "duration_s", "build.mode", "build.world", "build.headless", "build.resolution", "provenance.gpu_adapter", "provenance.gpu_driver", "provenance.runner", "provenance.zig_toolchain"): + if require(expected, path, str(baseline)) != require(result_value, path, str(result)): + raise ValueError(f"benchmark provenance differs for {path}") def self_test() -> None: - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - template: dict[str, Any] = { - "schema_version": SCHEMA_VERSION, "artifact_type": "benchmark-result", "preset": "low", "scenario": "stationary", "world_seed": 1, - "duration_s": 5, "horizon_distance": 512, - "build": {"mode": "ReleaseFast", "world": "overworld", "fixture": "", "headless": True, "resolution": [1920, 1080], "horizon_distance": 512, "benchmark_lod_memory_budget_mb": 0, "benchmark_require_gpu_candidates": 0}, - "provenance": {"gpu_adapter": "Lavapipe", "gpu_driver": "Mesa pinned by Nix", "runner": "test runner", "zig_toolchain": "Zig 0.16.0"}, - "completion": {"scenario_completed": True, "warmup_ready": True, "warmup_timed_out": False, "compact_ready": True, "evidence_mode": True, "sampled_duration_s": 5, "requested_duration_s": 5, "sampled_frame_count": 1, "lod_renderable_region_target": 0, "gpu_candidate_target": 0}, - "startup_evidence": {"readiness_observed": True, "readiness_elapsed_s": 2, "upload_total_bytes": 100, "far_expanded_upload_bytes": 100, "compact_upload_bytes": 0, "worker_generation_total_ms": 2, "worker_mesh_construction_total_ms": 10, "worker_far_expanded_mesh_construction_ms": 10, "worker_compact_encode_ms": 0, "compact_submissions": 0, "pool_gpu_allocated_bytes": 5000, "direct_mesh_gpu_bytes": 1000, "compact_pool_allocated_bytes": 0, "pool_cpu_shadow_bytes": 1000, "lod_renderable_regions": 0, "gpu_culling_candidate_count": 0}, - "gpu_ms": {"total": {"p95": 1, "p99": 1}}, "draw_calls_avg": 10, "lod": {"profiling_enabled": True, "profiling_frame_count": 1, "cpu_frame_ms": {"p95": 1, "p99": 1}, "gpu_frame_ms": {"p95": 1, "p99": 1}, "memory_bytes": {"logical_ram_bytes": {"p50_bytes": 1, "p95_bytes": 1, "p99_bytes": 1, "max_bytes": 1}, "logical_vram_bytes": {"p50_bytes": 1, "p95_bytes": 1, "p99_bytes": 1, "max_bytes": 1}}, "pressure": {"wait_idle_count_total": 0, "gpu_culling_overflows_max": 0, "gpu_culling_validation_mismatches_max": 0}}, - } - for mode in ("off", "auto"): - for preset in PRESETS: - for scenario in SCENARIOS: - value = json.loads(json.dumps(template)); value["preset"] = preset; value["scenario"] = scenario; value["compact_mode"] = mode - if mode == "auto": - value["startup_evidence"].update({"compact_submissions": 2, "pool_gpu_allocated_bytes": 9000, "direct_mesh_gpu_bytes": 0, "compact_pool_allocated_bytes": 800, "far_expanded_upload_bytes": 0, "compact_upload_bytes": 80, "worker_far_expanded_mesh_construction_ms": 0, "worker_compact_encode_ms": 8, "pool_cpu_shadow_bytes": 2000}) - path = root / mode / preset / f"{scenario}.json"; path.parent.mkdir(parents=True, exist_ok=True); path.write_text(json.dumps(value), encoding="utf-8") - validate_compact_matrix(root) - output = root / "baseline.json"; assemble([root], output, False); validate_baseline(output, False) - compatibility(output, root / "auto" / "low" / "stationary.json", "low", "stationary") - # Near-pool and CPU-shadow changes are contextual only, whereas far - # representation evidence remains an acceptance requirement. - assert validate_pair(load(root / "off" / "low" / "stationary.json"), load(root / "auto" / "low" / "stationary.json"), "self-test")["near_pool_geometry"]["delta"] == 4000 - assert required_reduction(0, 1, "self-test")["status"] == "not-applicable-off-zero" - try: - required_reduction(100, 90, "self-test") - except ValueError: - pass - else: - raise AssertionError("insufficient far reduction accepted") - cpu = load(root / "auto" / "high" / "traversal.json") - gpu = json.loads(json.dumps(cpu)) - cpu["lod"].update({"cpu_frame_ms": {"p95": 0.9670, "p99": 1.3290}, "gpu_culling": {"requested": False, "threshold": 1, "candidate_count_total": 0, "candidate_count_max": 0, "draw_submission_count_total": 0, "validation_generation": 0, "validation_completed_generation": 0, "validation_completed_count": 0}}) - cpu["frame_ms"] = {"p95": 1.0, "p99": 2.0} - cpu["gpu_ms"]["total"]["p99"] = 2.0 - gpu["lod"].update({"cpu_frame_ms": {"p95": 0.9251, "p99": 1.2137}, "gpu_culling": {"requested": True, "threshold": 1, "candidate_count_total": 1000, "candidate_count_max": GPU_CULLING_MIN_READINESS_TARGET, "draw_submission_count_total": 20, "validation_generation": 10, "validation_completed_generation": 9, "validation_completed_count": 9}}) - gpu["frame_ms"] = {"p95": 0.98, "p99": 2.01} - gpu["gpu_ms"]["total"]["p99"] = 2.01 - gpu["gpu_ms"]["lod_culling"] = {"avg": 0.01, "p50": 0.01, "p95": 0.02, "p99": 0.03} - for value in (cpu, gpu): - value["duration_s"] = 60 - value["horizon_distance"] = GPU_CULLING_MIN_HORIZON_DISTANCE - value["build"]["horizon_distance"] = GPU_CULLING_MIN_HORIZON_DISTANCE - value["build"].update({"benchmark_lod_memory_budget_mb": 2048, "benchmark_require_gpu_candidates": GPU_CULLING_MIN_READINESS_TARGET}) - value["build"]["fixture"] = GPU_CULLING_FIXTURE - value["completion"].update({"sampled_duration_s": 60, "requested_duration_s": 60, "lod_renderable_region_target": GPU_CULLING_MIN_READINESS_TARGET, "gpu_candidate_target": GPU_CULLING_MIN_READINESS_TARGET}) - value["startup_evidence"]["lod_renderable_regions"] = GPU_CULLING_MIN_READINESS_TARGET - cpu_path, gpu_path = root / "cpu.json", root / "gpu.json" - cpu_path.write_text(json.dumps(cpu), encoding="utf-8") - gpu_path.write_text(json.dumps(gpu), encoding="utf-8") - culling_output = root / "gpu-culling.json" - assemble_gpu_culling(cpu_path, gpu_path, culling_output, False) - validate_gpu_culling_baseline(culling_output, False) - cpu["build"]["fixture"] = "" - try: - culling_comparison(cpu, gpu, "self-test") - except ValueError: - pass - else: - raise AssertionError("GPU culling baseline accepted a non-scale fixture") - cpu["build"]["fixture"] = GPU_CULLING_FIXTURE - gpu["frame_ms"]["p95"] = 0.995 - try: - culling_comparison(cpu, gpu, "self-test") - except ValueError: - pass - else: - raise AssertionError("insufficient GPU culling p95 improvement accepted") - gpu["frame_ms"]["p95"] = 0.98 - for value in (cpu, gpu): value["build"]["horizon_distance"] = GPU_CULLING_MIN_HORIZON_DISTANCE - 1 - try: - culling_comparison(cpu, gpu, "self-test") - except ValueError: - pass - else: - raise AssertionError("small GPU culling horizon accepted") - for value in (cpu, gpu): - value["build"]["horizon_distance"] = GPU_CULLING_MIN_HORIZON_DISTANCE - value["preset"] = "extreme" - culling_comparison(cpu, gpu, "self-test") - try: - invalid_provenance = load(root / "off" / "low" / "stationary.json") - invalid_provenance["provenance"]["gpu_adapter"] = "unknown" - validate_compact_result(invalid_provenance) - except ValueError: - return - raise AssertionError("unknown evidence provenance accepted") + result: dict[str, Any] = { + "schema_version": SCHEMA_VERSION, "artifact_type": "benchmark-result", "preset": "low", "scenario": "stationary", "world_seed": 12345, "render_distance": 6, "frames": 2, "duration_s": 1.0, + "fps": {"avg": 60.0, "p1": 58.0}, "frame_ms": {"avg": 16.0, "p99": 18.0}, "gpu_ms": {"total": {"avg": 8.0}}, "draw_calls_avg": 4.0, "vertices_avg": 12.0, "chunks_rendered_avg": 1.0, "gpu_memory_mb_max": 64.0, + "build": {"mode": "ReleaseFast", "world": "normal", "headless": True, "resolution": [1920, 1080]}, + "provenance": {"gpu_adapter": "test-gpu", "gpu_driver": "test-driver", "runner": "self-test", "zig_toolchain": "zig-test"}, + "completion": {"scenario_completed": True, "sampled_duration_s": 1.0, "requested_duration_s": 1.0, "sampled_frame_count": 2, "warmup_ready": True, "warmup_timed_out": False}, + } + validate_result(result, "self-test") + result["fps"]["avg"] = float("nan") + try: + validate_result(result, "self-test") + except ValueError: + return + raise AssertionError("non-finite metrics must be rejected") def main() -> None: - parser = argparse.ArgumentParser(); sub = parser.add_subparsers(dest="command", required=True) - assemble_parser = sub.add_parser("assemble"); assemble_parser.add_argument("--output", type=Path, required=True); assemble_parser.add_argument("--overwrite", action="store_true"); assemble_parser.add_argument("inputs", nargs="+", type=Path) - validate_parser = sub.add_parser("validate"); validate_parser.add_argument("path", type=Path); validate_parser.add_argument("--allow-insufficient", action="store_true") - result_parser = sub.add_parser("validate-result"); result_parser.add_argument("path", type=Path) - stamp_parser = sub.add_parser("stamp-compact"); stamp_parser.add_argument("path", type=Path); stamp_parser.add_argument("mode") - matrix_parser = sub.add_parser("validate-compact-matrix"); matrix_parser.add_argument("root", type=Path) - compatibility_parser = sub.add_parser("compatibility"); compatibility_parser.add_argument("baseline", type=Path); compatibility_parser.add_argument("result", type=Path); compatibility_parser.add_argument("--preset", required=True); compatibility_parser.add_argument("--scenario", required=True) - assemble_culling_parser = sub.add_parser("assemble-gpu-culling"); assemble_culling_parser.add_argument("--output", type=Path, required=True); assemble_culling_parser.add_argument("--overwrite", action="store_true"); assemble_culling_parser.add_argument("cpu", type=Path); assemble_culling_parser.add_argument("gpu", type=Path) - validate_culling_parser = sub.add_parser("validate-gpu-culling"); validate_culling_parser.add_argument("path", type=Path); validate_culling_parser.add_argument("--allow-insufficient", action="store_true") - sub.add_parser("self-test"); args = parser.parse_args() - if args.command == "assemble": assemble(args.inputs, args.output, args.overwrite) - elif args.command == "validate": validate_baseline(args.path, args.allow_insufficient) - elif args.command == "validate-result": validate_result(load(args.path), str(args.path)) - elif args.command == "stamp-compact": stamp_compact(args.path, args.mode) - elif args.command == "validate-compact-matrix": validate_compact_matrix(args.root) - elif args.command == "compatibility": compatibility(args.baseline, args.result, args.preset, args.scenario) - elif args.command == "assemble-gpu-culling": assemble_gpu_culling(args.cpu, args.gpu, args.output, args.overwrite) - elif args.command == "validate-gpu-culling": validate_gpu_culling_baseline(args.path, args.allow_insufficient) - else: self_test() + parser = argparse.ArgumentParser() + subcommands = parser.add_subparsers(dest="command", required=True) + assemble_parser = subcommands.add_parser("assemble") + assemble_parser.add_argument("--output", type=Path, required=True) + assemble_parser.add_argument("--overwrite", action="store_true") + assemble_parser.add_argument("inputs", nargs="+", type=Path) + validate_parser = subcommands.add_parser("validate") + validate_parser.add_argument("path", type=Path) + result_parser = subcommands.add_parser("validate-result") + result_parser.add_argument("path", type=Path) + compatibility_parser = subcommands.add_parser("compatibility") + compatibility_parser.add_argument("baseline", type=Path) + compatibility_parser.add_argument("result", type=Path) + compatibility_parser.add_argument("--preset", required=True) + compatibility_parser.add_argument("--scenario", required=True) + subcommands.add_parser("self-test") + args = parser.parse_args() + if args.command == "assemble": + assemble(args.inputs, args.output, args.overwrite) + elif args.command == "validate": + validate_baseline(load(args.path), str(args.path)) + elif args.command == "validate-result": + validate_result(load(args.path), str(args.path)) + elif args.command == "compatibility": + compatibility(args.baseline, args.result, args.preset, args.scenario) + else: + self_test() if __name__ == "__main__": - try: main() - except (ValueError, OSError, json.JSONDecodeError) as error: raise SystemExit(f"benchmark baseline validation failed: {error}") + try: + main() + except (OSError, ValueError, json.JSONDecodeError) as error: + raise SystemExit(f"benchmark baseline validation failed: {error}") diff --git a/scripts/check_phase5_benchmark_config.sh b/scripts/check_phase5_benchmark_config.sh deleted file mode 100644 index 3deaee39..00000000 --- a/scripts/check_phase5_benchmark_config.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash -# Validates the build-time benchmark scenario contract without starting Vulkan. -set -euo pipefail - -scenarios=(stationary traversal rapid-turn teleport-eviction) - -for scenario in "${scenarios[@]}"; do - zig build -Dbenchmark-scenario="$scenario" --help >/dev/null -done - -if zig build -Dbenchmark-scenario=unbounded --help >/dev/null 2>&1; then - printf 'benchmark build configuration accepted an unbounded scenario\n' >&2 - exit 1 -fi - -zig build -Dbenchmark-horizon-distance=4096 --help >/dev/null - -zig build -Dbenchmark=true -Dbenchmark-fixture=gpu-culling-scale -Dbenchmark-horizon-distance=4096 -Dbenchmark-lod-memory-budget-mb=2048 -Dbenchmark-require-gpu-candidates=1024 --help >/dev/null - -if zig build -Dbenchmark-fixture=gpu-culling-scale --help >/dev/null 2>&1; then - printf 'gpu-culling-scale fixture was accepted outside benchmark mode\n' >&2 - exit 1 -fi - -if zig build -Dbenchmark=true -Dbenchmark-fixture=gpu-culling-scale -Dbenchmark-horizon-distance=4096 -Dbenchmark-lod-memory-budget-mb=2048 -Dbenchmark-require-gpu-candidates=512 --help >/dev/null 2>&1; then - printf 'gpu-culling-scale fixture accepted a sub-target readiness gate\n' >&2 - exit 1 -fi - -if zig build -Dbenchmark-horizon-distance=-1 --help >/dev/null 2>&1; then - printf 'benchmark build configuration accepted a negative horizon override\n' >&2 - exit 1 -fi diff --git a/scripts/check_phase5_visual_smoke.py b/scripts/check_phase5_visual_smoke.py deleted file mode 100644 index d236350a..00000000 --- a/scripts/check_phase5_visual_smoke.py +++ /dev/null @@ -1,541 +0,0 @@ -#!/usr/bin/env python3 -"""Validate deterministic Phase 5 visual-capture evidence without a golden. - -The gate is intentionally tolerant of small Vulkan-driver differences, but it -rejects empty images, unhealthy regions, scene-scale compact divergence, and -missing runtime evidence for the compact/water/handoff paths. -""" - -import argparse -import json -import os -import re -import struct -import sys -import tempfile -import zlib - - -MOTION_SCENE_KINDS = { - "lod-handoff-traversal": "traversal", - "fog-rapid-turn": "rapid-turn", - "teleport-handoff": "teleport", -} -GPU_VALIDATION_SCENES = {"lod-handoff", "saved-world-reload", *MOTION_SCENE_KINDS} - - -def read_png(path): - with open(path, "rb") as source: - data = source.read() - if data[:8] != b"\x89PNG\r\n\x1a\n": - raise ValueError("not a PNG") - - offset = 8 - chunks = [] - width = height = color_type = bit_depth = None - while offset + 12 <= len(data): - length = struct.unpack(">I", data[offset:offset + 4])[0] - kind = data[offset + 4:offset + 8] - payload = data[offset + 8:offset + 8 + length] - if len(payload) != length: - raise ValueError("truncated PNG chunk") - offset += length + 12 - if kind == b"IHDR": - width, height, bit_depth, color_type, compression, filtering, interlace = struct.unpack(">IIBBBBB", payload) - if (bit_depth, color_type, compression, filtering, interlace) != (8, 2, 0, 0, 0): - raise ValueError("expected non-interlaced 8-bit RGB PNG") - elif kind == b"IDAT": - chunks.append(payload) - elif kind == b"IEND": - break - if not width or not height or not chunks: - raise ValueError("missing PNG image data") - - raw = zlib.decompress(b"".join(chunks)) - stride = width * 3 - if len(raw) != height * (stride + 1): - raise ValueError("unexpected PNG scanline size") - rows = [] - previous = bytearray(stride) - cursor = 0 - for _ in range(height): - filter_type = raw[cursor] - cursor += 1 - current = bytearray(raw[cursor:cursor + stride]) - cursor += stride - for index in range(stride): - left = current[index - 3] if index >= 3 else 0 - above = previous[index] - upper_left = previous[index - 3] if index >= 3 else 0 - if filter_type == 1: - current[index] = (current[index] + left) & 0xff - elif filter_type == 2: - current[index] = (current[index] + above) & 0xff - elif filter_type == 3: - current[index] = (current[index] + ((left + above) // 2)) & 0xff - elif filter_type == 4: - p = left + above - upper_left - pa, pb, pc = abs(p - left), abs(p - above), abs(p - upper_left) - predictor = left if pa <= pb and pa <= pc else (above if pb <= pc else upper_left) - current[index] = (current[index] + predictor) & 0xff - elif filter_type != 0: - raise ValueError("unsupported PNG filter") - rows.append(bytes(current)) - previous = current - return width, height, rows - - -def sampled_rgb(image, sample_width=128, sample_height=72, roi=None): - width, height, rows = image - if roi is None: - roi = (0.0, 0.0, 1.0, 1.0) - left, top, roi_width, roi_height = roi - samples = [] - for y in range(sample_height): - source_y = min(height - 1, int((top + (y + 0.5) * roi_height / sample_height) * height)) - row = rows[source_y] - for x in range(sample_width): - source_x = min(width - 1, int((left + (x + 0.5) * roi_width / sample_width) * width)) - offset = source_x * 3 - samples.append((row[offset], row[offset + 1], row[offset + 2])) - return samples - - -def image_metrics(image): - samples = sampled_rgb(image) - luma = sorted((54 * red + 183 * green + 19 * blue) // 256 for red, green, blue in samples) - luma_bins = {value // 16 for value in luma} - non_black = sum(value > 12 for value in luma) / len(luma) - p05 = luma[len(luma) * 5 // 100] - p95 = luma[len(luma) * 95 // 100] - p01 = luma[len(luma) // 100] - p99 = luma[len(luma) * 99 // 100] - mean = sum(luma) / len(luma) - saturated = sum(max(pixel) - min(pixel) >= 24 for pixel in samples) / len(samples) - return { - "width": image[0], - "height": image[1], - "mean_luma": round(mean, 3), - "p05_luma": p05, - "p95_luma": p95, - "luma_range_p05_p95": p95 - p05, - "luma_range_p01_p99": p99 - p01, - "luma_bin_count": len(luma_bins), - "non_black_fraction": round(non_black, 5), - "saturated_fraction": round(saturated, 5), - } - - -def normalized_mae(first, second, roi=None): - first_samples = sampled_rgb(first, roi=roi) - second_samples = sampled_rgb(second, roi=roi) - return sum(abs(a - b) for first_pixel, second_pixel in zip(first_samples, second_samples) for a, b in zip(first_pixel, second_pixel)) / (len(first_samples) * 3 * 255) - - -def require_healthy(name, image, metrics): - if metrics["non_black_fraction"] < float(os.environ.get("PHASE5_VISUAL_MIN_NON_BLACK", "0.12")): - raise ValueError(f"{name} is black or nearly black") - if metrics["luma_range_p01_p99"] < int(os.environ.get("PHASE5_VISUAL_MIN_LUMA_RANGE", "22")): - raise ValueError(f"{name} has insufficient image variation") - if metrics["luma_bin_count"] < int(os.environ.get("PHASE5_VISUAL_MIN_LUMA_BINS", "5")): - raise ValueError(f"{name} is visually empty") - if metrics["saturated_fraction"] < float(os.environ.get("PHASE5_VISUAL_MIN_SATURATED", "0.01")): - raise ValueError(f"{name} has no material or sky colour") - - # A central ROI catches a valid sky clear paired with missing terrain, and - # a lower ROI catches a presentation path that only clears the top half. - for roi_name, roi in (("central", (0.20, 0.20, 0.60, 0.60)), ("lower", (0.10, 0.55, 0.80, 0.40))): - roi_metrics = image_metrics_from_samples(sampled_rgb(image, roi=roi)) - if roi_metrics["non_black_fraction"] < 0.10 or roi_metrics["luma_range_p01_p99"] < 10: - raise ValueError(f"{name} has an unhealthy {roi_name} ROI") - - -def image_metrics_from_samples(samples): - luma = sorted((54 * red + 183 * green + 19 * blue) // 256 for red, green, blue in samples) - return { - "non_black_fraction": sum(value > 12 for value in luma) / len(luma), - "luma_range_p01_p99": luma[len(luma) * 99 // 100] - luma[len(luma) // 100], - } - - -def parse_evidence(path, scene, run_id): - with open(path, encoding="utf-8", errors="replace") as source: - log = source.read() - escaped_run = re.escape(run_id) - captures = re.findall(rf"PHASE5_CAPTURE: run={escaped_run} scene=([^ ]+) chunks_rendered=([0-9]+) compact_allocated=([0-9]+) compact_submissions=([0-9]+)", log) - matching = [(int(chunks), int(allocated), int(submissions)) for name, chunks, allocated, submissions in captures if name == scene] - return { - "compact_allocated_bytes": max((allocated for _, allocated, _ in matching), default=0), - "compact_submissions": max((submissions for _, _, submissions in matching), default=0), - "capture_counters": matching, - "fixture_applied": bool(re.search(rf"PHASE5_FIXTURE: run={escaped_run} scene={re.escape(scene)} applied=1", log)), - "settled": bool(re.search(rf"PHASE5_READY: run={escaped_run} scene={re.escape(scene)} stable_frames=[1-9][0-9]*", log)), - "water_fixture_observations": max((int(value) for value in re.findall(rf"PHASE5_WATER_FIXTURE: run={escaped_run} scene={re.escape(scene)} observations=([0-9]+)", log)), default=0), - "gpu_culling": [ - {"candidates": int(candidates), "validation_mismatches": int(mismatches), "overflows": int(overflows)} - for candidates, mismatches, overflows in re.findall(rf"PHASE5_GPU_CULLING: run={escaped_run} scene={re.escape(scene)} candidates=([0-9]+) validation_mismatches=([0-9]+) overflows=([0-9]+)", log) - ], - "gpu_validation_complete": [ - {"generation": int(generation), "validations": int(validations), "validation_mismatches": int(mismatches)} - for generation, validations, mismatches in re.findall(rf"PHASE5_GPU_VALIDATION_COMPLETE: run={escaped_run} scene={re.escape(scene)} generation=([1-9][0-9]*) validations=([1-9][0-9]*) validation_mismatches=([0-9]+)", log) - ], - "motion": [ - { - "kind": kind, - "frames": int(frames), - "distance": float(distance), - "yaw_degrees": float(yaw_degrees), - } - for kind, frames, distance, yaw_degrees in re.findall(rf"PHASE5_MOTION: run={escaped_run} scene={re.escape(scene)} kind=([^ ]+) completed=1 frames=([1-9][0-9]*) distance=([0-9]+(?:\.[0-9]+)?) yaw_degrees=([0-9]+(?:\.[0-9]+)?)", log) - ], - "save_flushed": bool(re.search(rf"PHASE5_SAVE_FLUSH: run={escaped_run} scene=saved-world-create saved=1 failures=0", log)), - "save_loaded": [ - {"wet_cells": int(wet_cells), "dry_cells": int(dry_cells)} - for wet_cells, dry_cells in re.findall(rf"PHASE5_SAVE_LOADED: run={escaped_run} scene=saved-world-reload wet_cells=([1-9][0-9]*) dry_cells=([1-9][0-9]*)", log) - ], - "compact_wet_dry": [ - { - "dry_compact_bytes": int(dry_compact), - "wet_fallback_bytes": int(wet_fallback), - "wet_cells": int(wet_cells), - "dry_cells": int(dry_cells), - } - for dry_compact, wet_fallback, wet_cells, dry_cells in re.findall(rf"PHASE5_COMPACT_WET_DRY: run={escaped_run} scene=saved-world-reload dry_compact_bytes=([0-9]+) wet_fallback_bytes=([0-9]+) wet_cells=([0-9]+) dry_cells=([0-9]+)", log) - ], - } - - -def require_capture_evidence(name, evidence): - if evidence["compact_allocated_bytes"] <= 0: - raise ValueError(f"{name}: compact-auto capture did not allocate compact residency") - if not evidence["fixture_applied"]: - raise ValueError(f"{name}: production-world fixture was never applied") - if not evidence["settled"]: - raise ValueError(f"{name}: fixture/LOD queues never reached consecutive-frame readiness") - if not evidence["capture_counters"]: - raise ValueError(f"{name}: missing Phase 5 capture counters") - if max(chunks for chunks, _, _ in evidence["capture_counters"]) <= 0: - raise ValueError(f"{name}: no full-detail chunks were rendered") - if max(allocated for _, allocated, _ in evidence["capture_counters"]) <= 0: - raise ValueError(f"{name}: compact allocation was not present at capture") - if evidence["compact_submissions"] <= 0: - raise ValueError(f"{name}: compact-auto capture made no compact submission") - if name == "water": - if evidence["water_fixture_observations"] <= 0: - raise ValueError("water: deterministic water fixture was never resident") - if name in GPU_VALIDATION_SCENES: - if not evidence["gpu_culling"]: - raise ValueError(f"{name}: GPU culling never activated") - if max(gpu["candidates"] for gpu in evidence["gpu_culling"]) <= 0: - raise ValueError(f"{name}: GPU culling submitted no candidates") - if any(gpu["validation_mismatches"] != 0 for gpu in evidence["gpu_culling"]): - raise ValueError(f"{name}: GPU culling validation reported mismatches") - if any(gpu["overflows"] != 0 for gpu in evidence["gpu_culling"]): - raise ValueError(f"{name}: GPU culling reported overflows") - if not evidence["gpu_validation_complete"]: - raise ValueError(f"{name}: delayed GPU validation readback never completed") - if any(validation["validations"] <= 0 for validation in evidence["gpu_validation_complete"]): - raise ValueError(f"{name}: delayed GPU validation readback has no completed validations") - if any(validation["validation_mismatches"] != 0 for validation in evidence["gpu_validation_complete"]): - raise ValueError(f"{name}: completed GPU validation readback reported mismatches") - if name in MOTION_SCENE_KINDS: - motions = evidence["motion"] - if not motions: - raise ValueError(f"{name}: deterministic motion never completed") - motion = motions[-1] - if motion["kind"] != MOTION_SCENE_KINDS[name]: - raise ValueError(f"{name}: incorrect motion evidence kind") - if motion["frames"] <= 0: - raise ValueError(f"{name}: motion evidence has no frames") - if name == "fog-rapid-turn": - if motion["yaw_degrees"] <= 0: - raise ValueError(f"{name}: turn evidence has no yaw change") - elif motion["distance"] <= 0: - raise ValueError(f"{name}: movement evidence has no displacement") - - -def check_scene(name, off_path, force_path, force_log, run_id): - off = read_png(off_path) - force = read_png(force_path) - if off[:2] != force[:2]: - raise ValueError(f"{name}: capture dimensions differ: {off[:2]} vs {force[:2]}") - off_metrics = image_metrics(off) - force_metrics = image_metrics(force) - require_healthy(f"{name} expanded capture", off, off_metrics) - require_healthy(f"{name} compact-auto capture", force, force_metrics) - - full_difference = normalized_mae(off, force) - # Horizon and foreground ROIs make a partially blank compact stream fail - # even when its whole-frame average is hidden by a large sky clear. - roi_differences = { - "horizon": normalized_mae(off, force, (0.05, 0.20, 0.90, 0.35)), - "foreground": normalized_mae(off, force, (0.10, 0.50, 0.80, 0.45)), - } - max_difference = float(os.environ.get("PHASE5_VISUAL_MAX_NMAE", "0.32")) - max_roi_difference = float(os.environ.get("PHASE5_VISUAL_MAX_ROI_NMAE", "0.38")) - if full_difference > max_difference: - raise ValueError(f"{name}: compact-auto image diverges from expanded fallback ({full_difference:.3f})") - if max(roi_differences.values()) > max_roi_difference: - raise ValueError(f"{name}: a compact comparison ROI diverges grossly") - - evidence = parse_evidence(force_log, name, run_id) - require_capture_evidence(name, evidence) - if name == "water": - # The lower-center water fixture must contribute colour, not merely be - # generated off camera. This ROI intentionally permits lighting/texture - # variation while rejecting a flat clear or missing fluid pass. - water_metrics = image_metrics_from_samples(sampled_rgb(force, roi=(0.20, 0.35, 0.60, 0.60))) - if water_metrics["non_black_fraction"] < 0.20 or water_metrics["luma_range_p01_p99"] < 14: - raise ValueError("water: fixture ROI does not contain a healthy rendered surface") - return { - "expanded": off_metrics, - "compact_auto": force_metrics, - "normalized_mae": round(full_difference, 6), - "roi_normalized_mae": {key: round(value, 6) for key, value in roi_differences.items()}, - "evidence": evidence, - } - - -def check_handoff(path, log_path, run_id): - image = read_png(path) - metrics = image_metrics(image) - require_healthy("lod-handoff compact-auto capture", image, metrics) - evidence = parse_evidence(log_path, "lod-handoff", run_id) - require_capture_evidence("lod-handoff", evidence) - return {"compact_auto": metrics, "evidence": evidence} - - -def check_motion(name, path, log_path, run_id): - image = read_png(path) - metrics = image_metrics(image) - require_healthy(f"{name} compact-auto capture", image, metrics) - evidence = parse_evidence(log_path, name, run_id) - require_capture_evidence(name, evidence) - return {"compact_auto": metrics, "evidence": evidence} - - -def check_saved_world(create_log_path, reload_path, reload_log_path, run_id): - image = read_png(reload_path) - metrics = image_metrics(image) - require_healthy("saved-world reload compact-auto capture", image, metrics) - - create_evidence = parse_evidence(create_log_path, "saved-world-create", run_id) - if not create_evidence["save_flushed"]: - raise ValueError("saved-world: creator did not flush a clean save") - - evidence = parse_evidence(reload_log_path, "saved-world-reload", run_id) - require_capture_evidence("saved-world-reload", evidence) - if not evidence["save_loaded"]: - raise ValueError("saved-world: reload did not prove persisted wet/dry cells") - if not evidence["compact_wet_dry"]: - raise ValueError("saved-world: missing compact dry/wet fallback evidence") - wet_dry = evidence["compact_wet_dry"][-1] - if wet_dry["dry_compact_bytes"] <= 0: - raise ValueError("saved-world: dry terrain had no compact residency after reload") - if wet_dry["wet_fallback_bytes"] <= 0: - raise ValueError("saved-world: wet shoreline fallback was not retained after reload") - if wet_dry["wet_cells"] <= 0 or wet_dry["dry_cells"] <= 0: - raise ValueError("saved-world: wet/dry fixture evidence is empty") - return { - "compact_auto": metrics, - "creator_evidence": create_evidence, - "evidence": evidence, - } - - -def failed_scene_metrics(name, off_path, auto_path, auto_log, run_id, error): - """Keep the image/evidence record when a strict check rejects a scene.""" - result = {"error": str(error), "evidence": parse_evidence(auto_log, name, run_id)} - try: - result["expanded"] = image_metrics(read_png(off_path)) - except (OSError, ValueError, zlib.error) as read_error: - result["expanded_error"] = str(read_error) - try: - result["compact_auto"] = image_metrics(read_png(auto_path)) - except (OSError, ValueError, zlib.error) as read_error: - result["compact_auto_error"] = str(read_error) - return result - - -def write_png(path, pixels, width=32, height=24): - raw = b"".join(b"\0" + bytes(pixels[y * width * 3:(y + 1) * width * 3]) for y in range(height)) - def chunk(kind, payload): - return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload) & 0xffffffff) - with open(path, "wb") as output: - output.write(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")) - - -def self_test(): - with tempfile.TemporaryDirectory() as directory: - good = [] - for y in range(24): - for x in range(32): - good.extend((20 + x * 6, 35 + y * 7, 170 - x * 3)) - good_path = os.path.join(directory, "good.png") - black_path = os.path.join(directory, "black.png") - write_png(good_path, good) - write_png(black_path, [0] * len(good)) - require_healthy("self-test", read_png(good_path), image_metrics(read_png(good_path))) - try: - require_healthy("black self-test", read_png(black_path), image_metrics(read_png(black_path))) - except ValueError: - pass - else: - raise AssertionError("black image unexpectedly passed health checks") - - evidence_path = os.path.join(directory, "evidence.log") - with open(evidence_path, "w", encoding="utf-8") as output: - output.write("PHASE5_CAPTURE: run=fresh scene=lod-handoff chunks_rendered=1 compact_allocated=64 compact_submissions=1\n") - output.write("PHASE5_FIXTURE: run=fresh scene=lod-handoff applied=1\n") - output.write("PHASE5_READY: run=fresh scene=lod-handoff stable_frames=180 chunks_rendered=1 lod_loaded=1\n") - output.write("PHASE5_GPU_CULLING: run=fresh scene=lod-handoff candidates=1 validation_mismatches=0 overflows=0\n") - output.write("PHASE5_GPU_VALIDATION_COMPLETE: run=fresh scene=lod-handoff generation=6 validations=5 validation_mismatches=0\n") - require_capture_evidence("lod-handoff", parse_evidence(evidence_path, "lod-handoff", "fresh")) - try: - require_capture_evidence("lod-handoff", parse_evidence(evidence_path, "lod-handoff", "stale")) - except ValueError: - pass - else: - raise AssertionError("stale evidence unexpectedly passed") - with open(evidence_path, "w", encoding="utf-8") as output: - output.write("PHASE5_CAPTURE: run=fresh scene=lod-handoff chunks_rendered=1 compact_allocated=64 compact_submissions=1\n") - output.write("PHASE5_FIXTURE: run=fresh scene=lod-handoff applied=1\n") - output.write("PHASE5_READY: run=fresh scene=lod-handoff stable_frames=180 chunks_rendered=1 lod_loaded=1\n") - output.write("PHASE5_GPU_CULLING: run=fresh scene=lod-handoff candidates=1 validation_mismatches=0 overflows=0\n") - try: - require_capture_evidence("lod-handoff", parse_evidence(evidence_path, "lod-handoff", "fresh")) - except ValueError: - pass - else: - raise AssertionError("missing GPU validation completion unexpectedly passed") - - create_path = os.path.join(directory, "saved-create.log") - reload_path = os.path.join(directory, "saved-reload.log") - with open(create_path, "w", encoding="utf-8") as output: - output.write("PHASE5_SAVE_FLUSH: run=fresh scene=saved-world-create saved=1 failures=0\n") - with open(reload_path, "w", encoding="utf-8") as output: - output.write("PHASE5_CAPTURE: run=fresh scene=saved-world-reload chunks_rendered=1 compact_allocated=64 compact_submissions=1\n") - output.write("PHASE5_FIXTURE: run=fresh scene=saved-world-reload applied=1\n") - output.write("PHASE5_READY: run=fresh scene=saved-world-reload stable_frames=180 chunks_rendered=1 lod_loaded=1\n") - output.write("PHASE5_SAVE_LOADED: run=fresh scene=saved-world-reload wet_cells=3 dry_cells=3\n") - output.write("PHASE5_COMPACT_WET_DRY: run=fresh scene=saved-world-reload dry_compact_bytes=64 wet_fallback_bytes=32 wet_cells=3 dry_cells=3\n") - output.write("PHASE5_GPU_CULLING: run=fresh scene=saved-world-reload candidates=1 validation_mismatches=0 overflows=0\n") - output.write("PHASE5_GPU_VALIDATION_COMPLETE: run=fresh scene=saved-world-reload generation=6 validations=5 validation_mismatches=0\n") - check_saved_world(create_path, good_path, reload_path, "fresh") - with open(reload_path, "w", encoding="utf-8") as output: - output.write("PHASE5_CAPTURE: run=fresh scene=saved-world-reload chunks_rendered=1 compact_allocated=64 compact_submissions=1\n") - output.write("PHASE5_FIXTURE: run=fresh scene=saved-world-reload applied=1\n") - output.write("PHASE5_READY: run=fresh scene=saved-world-reload stable_frames=180 chunks_rendered=1 lod_loaded=1\n") - output.write("PHASE5_GPU_CULLING: run=fresh scene=saved-world-reload candidates=1 validation_mismatches=0 overflows=0\n") - output.write("PHASE5_GPU_VALIDATION_COMPLETE: run=fresh scene=saved-world-reload generation=6 validations=5 validation_mismatches=0\n") - try: - check_saved_world(create_path, good_path, reload_path, "fresh") - except ValueError: - pass - else: - raise AssertionError("missing saved-world reload evidence unexpectedly passed") - - with open(evidence_path, "w", encoding="utf-8") as output: - output.write("PHASE5_CAPTURE: run=fresh scene=fog-rapid-turn chunks_rendered=1 compact_allocated=64 compact_submissions=1\n") - output.write("PHASE5_FIXTURE: run=fresh scene=fog-rapid-turn applied=1\n") - output.write("PHASE5_MOTION: run=fresh scene=fog-rapid-turn kind=rapid-turn completed=1 frames=160 distance=0.0 yaw_degrees=720.0\n") - output.write("PHASE5_READY: run=fresh scene=fog-rapid-turn stable_frames=180 chunks_rendered=1 lod_loaded=1\n") - output.write("PHASE5_GPU_CULLING: run=fresh scene=fog-rapid-turn candidates=1 validation_mismatches=0 overflows=0\n") - output.write("PHASE5_GPU_VALIDATION_COMPLETE: run=fresh scene=fog-rapid-turn generation=6 validations=5 validation_mismatches=0\n") - check_motion("fog-rapid-turn", good_path, evidence_path, "fresh") - overflow_evidence = parse_evidence(evidence_path, "fog-rapid-turn", "fresh") - overflow_evidence["gpu_culling"][0]["overflows"] = 1 - try: - require_capture_evidence("fog-rapid-turn", overflow_evidence) - except ValueError: - pass - else: - raise AssertionError("GPU culling overflow unexpectedly passed") - try: - check_motion("teleport-handoff", good_path, evidence_path, "fresh") - except ValueError: - pass - else: - raise AssertionError("cross-scene motion evidence unexpectedly passed") - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("--manifest") - parser.add_argument("--run-id") - parser.add_argument("--scene", action="append", nargs=4, metavar=("NAME", "EXPANDED_PNG", "COMPACT_PNG", "COMPACT_LOG")) - parser.add_argument("--handoff", nargs=2, metavar=("COMPACT_PNG", "COMPACT_LOG")) - parser.add_argument("--motion", action="append", nargs=3, metavar=("NAME", "COMPACT_PNG", "COMPACT_LOG")) - parser.add_argument("--saved-world", nargs=3, metavar=("CREATE_LOG", "RELOAD_PNG", "RELOAD_LOG")) - parser.add_argument("--self-test", action="store_true") - args = parser.parse_args() - if args.self_test: - self_test() - print("Phase 5 visual checker self-test passed") - return - if not args.manifest or not args.run_id or (not args.scene and not args.handoff and not args.motion and not args.saved_world): - parser.error("--run-id, --manifest, and at least one --scene, --handoff, --motion, or --saved-world are required") - - metrics = {"scenes": {}, "scene_distances": {}, "thresholds": {"max_normalized_mae": float(os.environ.get("PHASE5_VISUAL_MAX_NMAE", "0.32")), "max_roi_normalized_mae": float(os.environ.get("PHASE5_VISUAL_MAX_ROI_NMAE", "0.38")), "min_scene_normalized_mae": float(os.environ.get("PHASE5_VISUAL_MIN_SCENE_NMAE", "0.015"))}} - try: - expanded_images = {} - for scene in args.scene or []: - name, off_path, force_path, force_log = scene - try: - metrics["scenes"][name] = check_scene(name, off_path, force_path, force_log, args.run_id) - except (OSError, ValueError, zlib.error) as error: - metrics["scenes"][name] = failed_scene_metrics(name, off_path, force_path, force_log, args.run_id, error) - raise - expanded_images[name] = read_png(off_path) - if args.handoff: - handoff_path, handoff_log = args.handoff - try: - metrics["scenes"]["lod-handoff"] = check_handoff(handoff_path, handoff_log, args.run_id) - except (OSError, ValueError, zlib.error) as error: - metrics["scenes"]["lod-handoff"] = failed_scene_metrics("lod-handoff", handoff_path, handoff_path, handoff_log, args.run_id, error) - raise - for motion in args.motion or []: - name, motion_path, motion_log = motion - if name not in MOTION_SCENE_KINDS: - raise ValueError(f"unknown Phase 5 motion scene: {name}") - try: - metrics["scenes"][name] = check_motion(name, motion_path, motion_log, args.run_id) - except (OSError, ValueError, zlib.error) as error: - metrics["scenes"][name] = failed_scene_metrics(name, motion_path, motion_path, motion_log, args.run_id, error) - raise - if args.saved_world: - create_log, reload_path, reload_log = args.saved_world - try: - metrics["scenes"]["saved-world-reload"] = check_saved_world(create_log, reload_path, reload_log, args.run_id) - except (OSError, ValueError, zlib.error) as error: - metrics["scenes"]["saved-world-reload"] = { - "error": str(error), - "creator_evidence": parse_evidence(create_log, "saved-world-create", args.run_id), - "evidence": parse_evidence(reload_log, "saved-world-reload", args.run_id), - } - try: - metrics["scenes"]["saved-world-reload"]["compact_auto"] = image_metrics(read_png(reload_path)) - except (OSError, ValueError, zlib.error) as read_error: - metrics["scenes"]["saved-world-reload"]["compact_auto_error"] = str(read_error) - raise - names = sorted(expanded_images) - for first_index, first_name in enumerate(names): - for second_name in names[first_index + 1:]: - distance = normalized_mae(expanded_images[first_name], expanded_images[second_name]) - metrics["scene_distances"][f"{first_name}:{second_name}"] = round(distance, 6) - if distance < metrics["thresholds"]["min_scene_normalized_mae"]: - raise ValueError(f"{first_name} and {second_name} are unexpectedly the same scene") - finally: - # Preserve diagnostics and parsed evidence on failure for CI artifacts. - with open(args.manifest, "w", encoding="utf-8") as output: - json.dump(metrics, output, indent=2, sort_keys=True) - output.write("\n") - print(json.dumps(metrics, sort_keys=True)) - - -if __name__ == "__main__": - try: - main() - except (OSError, ValueError, zlib.error) as error: - print(f"Phase 5 visual smoke failed: {error}", file=sys.stderr) - raise SystemExit(1) diff --git a/scripts/compare_benchmarks.sh b/scripts/compare_benchmarks.sh index ccae8449..954765a7 100755 --- a/scripts/compare_benchmarks.sh +++ b/scripts/compare_benchmarks.sh @@ -44,7 +44,7 @@ read_json() { jq -r "$1" "$2" } -[[ -n "$preset" && -n "$scenario" ]] || { printf 'Schema-v2 comparison requires --preset and --scenario.\n' >&2; exit 2; } +[[ -n "$preset" && -n "$scenario" ]] || { printf 'Schema-v4 comparison requires --preset and --scenario.\n' >&2; exit 2; } python3 scripts/benchmark_baseline.py validate "$baseline" bash scripts/validate_benchmark_artifact.sh --result "$new" python3 scripts/benchmark_baseline.py compatibility "$baseline" "$new" --preset "$preset" --scenario "$scenario" diff --git a/scripts/run_benchmark.sh b/scripts/run_benchmark.sh index e7703bb8..d2747181 100755 --- a/scripts/run_benchmark.sh +++ b/scripts/run_benchmark.sh @@ -4,21 +4,10 @@ set -euo pipefail duration=60 presets="low,medium,high" scenarios="stationary,traversal,rapid-turn,teleport-eviction" -compact_modes="off,auto" benchmark_world="overworld" -benchmark_fixture="" output_dir="docs/benchmarks/results" per_preset_timeout=600 overwrite=false -gpu_adapter="${ZIGCRAFT_BENCHMARK_GPU_ADAPTER:-unknown}" -gpu_driver="${ZIGCRAFT_BENCHMARK_GPU_DRIVER:-unknown}" -runner="${ZIGCRAFT_BENCHMARK_RUNNER:-unknown}" -zig_toolchain="${ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN:-unknown}" -gpu_culling=off -gpu_culling_threshold=1 -benchmark_horizon_distance=0 -benchmark_lod_memory_budget_mb=0 -benchmark_require_gpu_candidates=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -31,18 +20,8 @@ while [[ $# -gt 0 ]]; do shift 2 ;; --scenarios) scenarios="$2"; shift 2 ;; - --compact-modes) compact_modes="$2"; shift 2 ;; --benchmark-world) benchmark_world="$2"; shift 2 ;; - --benchmark-fixture) benchmark_fixture="$2"; shift 2 ;; - --gpu-adapter) gpu_adapter="$2"; shift 2 ;; - --gpu-driver) gpu_driver="$2"; shift 2 ;; - --runner) runner="$2"; shift 2 ;; - --zig-toolchain) zig_toolchain="$2"; shift 2 ;; - --gpu-culling) gpu_culling="$2"; shift 2 ;; - --gpu-culling-threshold) gpu_culling_threshold="$2"; shift 2 ;; - --benchmark-horizon-distance) benchmark_horizon_distance="$2"; shift 2 ;; - --benchmark-lod-memory-budget-mb) benchmark_lod_memory_budget_mb="$2"; shift 2 ;; - --benchmark-require-gpu-candidates) benchmark_require_gpu_candidates="$2"; shift 2 ;; + --benchmark-fixture) printf '%s\n' '--benchmark-fixture was removed; benchmark fixtures are unsupported.' >&2; exit 2 ;; --overwrite) overwrite=true; shift ;; --output-dir) output_dir="$2" @@ -59,70 +38,23 @@ while [[ $# -gt 0 ]]; do esac done -case "$gpu_culling" in off|on) ;; *) printf 'Unknown GPU culling mode: %s (expected off or on)\n' "$gpu_culling" >&2; exit 2 ;; esac -[[ "$gpu_culling_threshold" =~ ^[1-9][0-9]*$ ]] || { printf 'GPU culling threshold must be a positive integer: %s\n' "$gpu_culling_threshold" >&2; exit 2; } -[[ "$benchmark_horizon_distance" =~ ^(0|[1-9][0-9]*)$ ]] || { printf 'Benchmark horizon distance must be zero or a positive integer: %s\n' "$benchmark_horizon_distance" >&2; exit 2; } -if ! [[ "$benchmark_lod_memory_budget_mb" =~ ^[0-9]+$ ]] || (( benchmark_lod_memory_budget_mb > 4096 )); then - printf 'Benchmark LOD memory budget must be an integer from 0 to 4096 MiB: %s\n' "$benchmark_lod_memory_budget_mb" >&2 - exit 2 -fi -[[ "$benchmark_require_gpu_candidates" =~ ^[0-9]+$ ]] || { printf 'Benchmark GPU candidate readiness target must be a nonnegative integer: %s\n' "$benchmark_require_gpu_candidates" >&2; exit 2; } -gpu_culling_enabled=0 -gpu_culling_validate=0 -benchmark_gpu_culling=false -if [[ "$gpu_culling" == on ]]; then - gpu_culling_enabled=1 - gpu_culling_validate=1 - benchmark_gpu_culling=true -fi - mkdir -p "$output_dir" -for label_name in gpu_adapter gpu_driver runner zig_toolchain; do - label="${!label_name}" - case "${label,,}" in - ""|unknown|unspecified|n/a) - printf 'Evidence mode requires a known %s label (CLI flag or ZIGCRAFT_BENCHMARK_* env).\n' "$label_name" >&2 - exit 2 - ;; - *) ;; - esac -done - IFS=',' read -r -a preset_list <<< "$presets" IFS=',' read -r -a scenario_list <<< "$scenarios" -IFS=',' read -r -a compact_mode_list <<< "$compact_modes" -for compact_mode in "${compact_mode_list[@]}"; do -case "$compact_mode" in off|auto) ;; *) printf 'Unknown compact mode: %s\n' "$compact_mode" >&2; exit 2;; esac for preset in "${preset_list[@]}"; do for scenario in "${scenario_list[@]}"; do - output_file="$output_dir/$compact_mode/$preset/$scenario.json" + output_file="$output_dir/$preset/$scenario.json" [[ ! -e "$output_file" || "$overwrite" == true ]] || { printf 'Refusing to overwrite %s\n' "$output_file" >&2; exit 1; } mkdir -p "$(dirname "$output_file")" printf 'Running benchmark preset %s -> %s\n' "$preset" "$output_file" - benchmark_cmd=(zig build benchmark -Doptimize=ReleaseFast -Dbenchmark-gpu-culling="$benchmark_gpu_culling" -Dbenchmark-preset="$preset" -Dbenchmark-scenario="$scenario" -Dbenchmark-world="$benchmark_world" -Dbenchmark-fixture="$benchmark_fixture" -Dbenchmark-horizon-distance="$benchmark_horizon_distance" -Dbenchmark-lod-memory-budget-mb="$benchmark_lod_memory_budget_mb" -Dbenchmark-require-gpu-candidates="$benchmark_require_gpu_candidates" -Dbenchmark-duration="$duration" -Dbenchmark-output="$output_file") - require_compact=0 - if [[ "$compact_mode" == auto ]]; then require_compact=1; fi - benchmark_env=( - "ZIGCRAFT_LOD_COMPACT=$compact_mode" - "ZIGCRAFT_LOD_GPU_CULLING=$gpu_culling_enabled" - "ZIGCRAFT_LOD_GPU_CULLING_VALIDATE=$gpu_culling_validate" - "ZIGCRAFT_LOD_GPU_CULLING_THRESHOLD=$gpu_culling_threshold" - "ZIGCRAFT_BENCHMARK_EVIDENCE=1" - "ZIGCRAFT_BENCHMARK_REQUIRE_COMPACT=$require_compact" - "ZIGCRAFT_BENCHMARK_GPU_ADAPTER=$gpu_adapter" - "ZIGCRAFT_BENCHMARK_GPU_DRIVER=$gpu_driver" - "ZIGCRAFT_BENCHMARK_RUNNER=$runner" - "ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN=$zig_toolchain" - ) + benchmark_cmd=(zig build benchmark -Doptimize=ReleaseFast -Dbenchmark-preset="$preset" -Dbenchmark-scenario="$scenario" -Dbenchmark-world="$benchmark_world" -Dbenchmark-duration="$duration" -Dbenchmark-output="$output_file") if [[ -n "${IN_NIX_SHELL:-}" ]]; then - env "${benchmark_env[@]}" timeout --preserve-status "${per_preset_timeout}s" "${benchmark_cmd[@]}" + timeout --preserve-status "${per_preset_timeout}s" "${benchmark_cmd[@]}" else - env "${benchmark_env[@]}" timeout --preserve-status "${per_preset_timeout}s" devenv shell --profile graphics -- "${benchmark_cmd[@]}" + timeout --preserve-status "${per_preset_timeout}s" devenv shell --profile graphics -- "${benchmark_cmd[@]}" fi - python3 "$(dirname "$0")/benchmark_baseline.py" validate-result "$output_file" - python3 "$(dirname "$0")/benchmark_baseline.py" stamp-compact "$output_file" "$compact_mode" + bash "$(dirname "$0")/validate_benchmark_artifact.sh" --result "$output_file" done done -done diff --git a/scripts/run_phase5_visual_smoke.sh b/scripts/run_phase5_visual_smoke.sh deleted file mode 100644 index 27186803..00000000 --- a/scripts/run_phase5_visual_smoke.sh +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env bash -# Captures deterministic production-world fixtures through expanded and -# production compact-auto paths. This is deliberately a bounded -# smoke/regression gate rather than a driver-specific pixel golden. -set -euo pipefail - -output_dir=${PHASE5_VISUAL_OUTPUT_DIR:-zig-out/phase5-visual-smoke} -run_id=${PHASE5_VISUAL_RUN_ID:-"$(date -u +%Y%m%dT%H%M%SZ)-$$"} -run_dir="$output_dir/run-$run_id" -save_dir="$run_dir/saved-world" -frame=${PHASE5_VISUAL_SCREENSHOT_FRAME:-900} -delay=${PHASE5_VISUAL_SCREENSHOT_DELAY_SECONDS:-5} -capture_timeout=${PHASE5_VISUAL_CAPTURE_TIMEOUT:-110s} -mkdir -p "$run_dir" -# A re-used run ID must not silently reload an older qualification save. -rm -rf "$save_dir" - -capture() { - local scene=$1 - local mode=$2 - local output="$run_dir/$scene-$mode.png" - local capture_log="$run_dir/$scene-$mode.log" - # The scope is unique per gate. Remove exact target artifacts as an - # additional guard when a caller deliberately reuses a run identifier. - rm -f "$output" "$capture_log" "$run_dir/$scene-$mode.engine.log" - echo "Phase 5 visual capture: run=$run_id scene=$scene mode=$mode" | tee "$capture_log" - local gpu_culling=0 - local disable_lod_mdi=1 - local scene_frame="$frame" - local scene_delay="$delay" - local timeout_budget="$capture_timeout" - local save_environment=() - if [[ ( "$scene" == "lod-handoff" || "$scene" == "lod-handoff-traversal" || "$scene" == "fog-rapid-turn" || "$scene" == "teleport-handoff" || "$scene" == "saved-world-reload" ) && "$mode" == "auto" ]]; then - gpu_culling=1 - disable_lod_mdi=0 - fi - if [[ "$scene" == saved-world-* ]]; then - save_environment+=("ZIGCRAFT_SAVE_DIR=$save_dir") - fi - # Motion completes after 180 rendered frames, then streaming at the final - # pose must drain and remain stable for another 180 frames. Saved-world - # create/reload also waits for persistence, cache ingestion, and GPU - # validation before capture. Give these paths real wall-clock time to - # settle instead of racing a fast GPU's frame counter and failing the - # readiness assertion at frame 900. - if [[ "$scene" == saved-world-* ]]; then - scene_frame="${PHASE5_VISUAL_SAVED_SCREENSHOT_FRAME:-4800}" - scene_delay="${PHASE5_VISUAL_SAVED_SCREENSHOT_DELAY_SECONDS:-30}" - timeout_budget="${PHASE5_VISUAL_SAVED_CAPTURE_TIMEOUT:-180s}" - elif [[ "$scene" == "lod-handoff-traversal" || "$scene" == "fog-rapid-turn" || "$scene" == "teleport-handoff" ]]; then - scene_frame="${PHASE5_VISUAL_MOTION_SCREENSHOT_FRAME:-2400}" - scene_delay="${PHASE5_VISUAL_MOTION_SCREENSHOT_DELAY_SECONDS:-15}" - fi - env "${save_environment[@]}" \ - ZIGCRAFT_LOD_COMPACT="$mode" \ - ZIGCRAFT_LOD_PROFILE=1 \ - ZIGCRAFT_LOD_UPLOAD_BUDGET_MB=4 \ - ZIGCRAFT_LOD_GPU_CULLING="$gpu_culling" \ - ZIGCRAFT_LOD_GPU_CULLING_THRESHOLD=1 \ - ZIGCRAFT_LOD_GPU_CULLING_VALIDATE="$gpu_culling" \ - ZIGCRAFT_DISABLE_LOD_MDI="$disable_lod_mdi" \ - ZIGCRAFT_PHASE5_SETTLE_FRAMES="${PHASE5_VISUAL_SETTLE_FRAMES:-180}" \ - timeout --preserve-status "$timeout_budget" devenv shell --profile graphics -- zig build run \ - -Dskip-present \ - -Dauto-preset=low \ - -Dauto-world=flat \ - -Dphase5-visual-scene="$scene" \ - -Dphase5-visual-run-id="$run_id" \ - -Dscreenshot-path="$output" \ - -Dscreenshot-frame="$scene_frame" \ - -Dscreenshot-delay-seconds="$scene_delay" 2>&1 | tee -a "$capture_log" -} - -# `seam` mutates both sides of x=16 in the production flat world. `water` adds -# a deterministic full-detail water pool. `lod-handoff` looks from full detail -# into the LOD horizon and enables GPU culling/MDI in its compact-auto run. -# The three motion scenes use production compact-auto only: their session -# scripts change the real player/camera pose before a settled capture. -# Keep each capture separate: this prevents one healthy scene from -# hiding a regression in another feature. -for scene in seam water; do - capture "$scene" off - capture "$scene" auto -done -capture lod-handoff auto -capture lod-handoff-traversal auto -capture fog-rapid-turn auto -capture teleport-handoff auto - -# Production compact-auto qualification needs a second process. The creator -# persists deterministic wet/dry edits and the LOD source cache; the reload is -# the only capture that exercises those same on-disk artifacts with MDI/GPU -# validation enabled. -capture saved-world-create off -test -f "$save_dir/level.dat" -shopt -s globstar nullglob -lod_cache_files=("$save_dir/lod"/**/*.zlod) -(( ${#lod_cache_files[@]} > 0 )) -shopt -u globstar nullglob -capture saved-world-reload auto - -python3 scripts/check_phase5_visual_smoke.py \ - --run-id "$run_id" \ - --manifest "$run_dir/manifest.json" \ - --scene seam \ - "$run_dir/seam-off.png" \ - "$run_dir/seam-auto.png" \ - "$run_dir/seam-auto.log" \ - --scene water \ - "$run_dir/water-off.png" \ - "$run_dir/water-auto.png" \ - "$run_dir/water-auto.log" \ - --handoff "$run_dir/lod-handoff-auto.png" "$run_dir/lod-handoff-auto.log" \ - --motion lod-handoff-traversal "$run_dir/lod-handoff-traversal-auto.png" "$run_dir/lod-handoff-traversal-auto.log" \ - --motion fog-rapid-turn "$run_dir/fog-rapid-turn-auto.png" "$run_dir/fog-rapid-turn-auto.log" \ - --motion teleport-handoff "$run_dir/teleport-handoff-auto.png" "$run_dir/teleport-handoff-auto.log" \ - --saved-world "$run_dir/saved-world-create-off.log" "$run_dir/saved-world-reload-auto.png" "$run_dir/saved-world-reload-auto.log" diff --git a/src/game/app.zig b/src/game/app.zig index 7f16341b..bc390def 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -82,13 +82,9 @@ fn gameBuildConfig() BuildConfig { .chunk_debug_enable = build_options.chunk_debug_enable, .chunk_debug_mode = build_options.chunk_debug_mode, .screenshot_path = build_options.screenshot_path, - .phase5_visual_scene = build_options.phase5_visual_scene, - .benchmark_fixture = build_options.benchmark_fixture, - .phase5_visual_run_id = build_options.phase5_visual_run_id, .shadow_test_scene = build_options.shadow_test_scene, .shadow_test_variant = build_options.shadow_test_variant, .startup_diagnostic_seconds = build_options.startup_diagnostic_seconds, - .benchmark_lod_memory_budget_mb = if (build_options.benchmark) build_options.benchmark_lod_memory_budget_mb else 0, }; } @@ -129,11 +125,10 @@ pub const App = struct { errdefer settings_manager.deinit(); if (build_options.benchmark) { - applyBenchmarkSettings(settings_manager.ptr(), build_options.benchmark_preset, build_options.benchmark_render_distance, build_options.benchmark_horizon_distance); + applyBenchmarkSettings(settings_manager.ptr(), build_options.benchmark_preset, build_options.benchmark_render_distance); } - // Benchmark setup above already applies its preset and independent - // near/horizon overrides. Reapplying `auto_preset` here would silently - // reset a dedicated large-horizon culling capture to the preset radius. + // Benchmark setup owns its render-distance override. Reapplying + // `auto_preset` here would reset the selected benchmark distance. if (build_options.auto_preset.len > 0 and !build_options.benchmark) { _ = applyNamedPreset(settings_manager.ptr(), build_options.auto_preset, "AUTO PRESET"); } @@ -205,7 +200,7 @@ pub const App = struct { var benchmark_runner: ?*BenchmarkRunner = null; if (build_options.benchmark) { - // Benchmarks include GPU pass breakdowns, including LOD passes. + // Benchmarks include renderer GPU pass breakdowns. render_system.getRHI().timing().setTimingEnabled(true); const runner = try allocator.create(BenchmarkRunner); const benchmark_duration_s: f32 = @as(f32, @floatFromInt(build_options.benchmark_duration)); @@ -214,15 +209,11 @@ pub const App = struct { build_options.benchmark_preset, build_options.benchmark_scenario, settings_manager.settings.render_distance, - settings_manager.settings.horizon_distance, benchmark_duration_s, BENCHMARK_WORLD_SEED, build_options.benchmark_build_mode, build_options.benchmark_world, - build_options.benchmark_fixture, build_options.benchmark_output, - build_options.benchmark_lod_memory_budget_mb, - build_options.benchmark_require_gpu_candidates, ); benchmark_runner = runner; } @@ -413,6 +404,11 @@ pub const App = struct { self.input.beginFrame(); self.input.pollEvents(); + self.ui_manager.handleTimingToggle( + self.input_mapper.isActionPressed(self.input.interface(), .toggle_timing_overlay), + &self.time, + self.render_system.getRHI(), + ); // Do not record and submit one more frame after SDL reports that the // window is closing. On some WSI/driver paths that final submission // races surface teardown and returns VK_ERROR_DEVICE_LOST. @@ -449,8 +445,6 @@ pub const App = struct { try self.maybeLaunchPendingWorld(swapchain_extent); - self.ui_manager.handleTimingToggle(self.input_mapper.interface().isActionPressed(self.input.interface(), .toggle_timing_overlay), &self.time, self.render_system.getRHI()); - // UI geometry is rendered in swapchain pixels. SDL input dimensions can // remain logical on HiDPI desktops, which previously confined RmlUi to // the top-left quarter of the framebuffer. @@ -537,8 +531,7 @@ pub const App = struct { // well; otherwise the headless UI-only fallback clears the output // black and a screenshot can race that transient frame. const rendered_frame = self.render_system.getRHI().query().getDrawCallCount() > 0; - const phase5_ready = build_options.phase5_visual_run_id.len == 0 or build_options.phase5_visual_scene.len == 0 or @import("game-core").phase5CaptureReady(); - if ((!requires_world_ready or world_ready) and (!requires_world_ready or rendered_frame) and phase5_ready) { + if ((!requires_world_ready or world_ready) and (!requires_world_ready or rendered_frame)) { self.screenshot_settle_frames += 1; } else { self.screenshot_settle_frames = 0; @@ -577,11 +570,6 @@ pub const App = struct { if (should_finish) { if (build_options.screenshot_path.len > 0) { - if (build_options.phase5_visual_run_id.len > 0) if (world_stats) |stats| if (stats.lod) |lod| { - const scene = if (build_options.phase5_visual_scene.len > 0) build_options.phase5_visual_scene else build_options.shadow_test_variant; - log.log.warn("PHASE5_CAPTURE: run={s} scene={s} chunks_rendered={} compact_allocated={} compact_submissions={}", .{ build_options.phase5_visual_run_id, scene, stats.chunks_rendered, lod.compact_pool_allocated_bytes, lod.profiling.compact_submissions }); - std.debug.print("PHASE5_CAPTURE: run={s} scene={s} chunks_rendered={} compact_allocated={} compact_submissions={}\n", .{ build_options.phase5_visual_run_id, scene, stats.chunks_rendered, lod.compact_pool_allocated_bytes, lod.profiling.compact_submissions }); - }; log.log.info("SCREENSHOT: Requesting final composed frame to '{s}'", .{build_options.screenshot_path}); if (!self.render_system.getRHI().screenshot().captureFrame(build_options.screenshot_path)) { log.log.err("SCREENSHOT: Failed to request screenshot", .{}); @@ -692,24 +680,18 @@ fn applyBenchmarkPreset(settings: *Settings, preset_name: []const u8) void { } } -/// Apply the preset first, then explicit benchmark distances. Keeping the -/// override ordering in one helper makes a large horizon immune to a later -/// automatic preset application. -fn applyBenchmarkSettings(settings: *Settings, preset_name: []const u8, render_distance: i32, horizon_distance: i32) void { +/// Apply the preset first, then explicit benchmark render distance. +fn applyBenchmarkSettings(settings: *Settings, preset_name: []const u8, render_distance: i32) void { applyBenchmarkPreset(settings, preset_name); if (render_distance > 0) { settings.render_distance = render_distance; - settings.horizon_distance = render_distance; } - // The horizon does not change near-chunk residency. - if (horizon_distance > 0) settings.horizon_distance = horizon_distance; } -test "benchmark horizon override is retained after its preset" { +test "benchmark render distance override is retained after its preset" { var settings = Settings{}; - applyBenchmarkSettings(&settings, "high", 12, 4096); + applyBenchmarkSettings(&settings, "high", 12); try std.testing.expectEqual(@as(i32, 12), settings.render_distance); - try std.testing.expectEqual(@as(i32, 4096), settings.horizon_distance); } fn gpuMemoryMb(stats: @import("engine-rhi").Stats) f32 { @@ -731,8 +713,6 @@ fn applyShadowTestPreset(settings: *Settings) void { settings.window_width = 1280; settings.window_height = 720; settings.render_distance = 4; - settings.lod_enabled = false; - settings.render_distance_preset = .low; settings.shadow_sandbox_enabled = true; settings.shadow_beauty_enabled = true; settings.shadow_quality = 2; diff --git a/src/game/player_tests.zig b/src/game/player_tests.zig index 43b5af55..9811b886 100644 --- a/src/game/player_tests.zig +++ b/src/game/player_tests.zig @@ -1,325 +1,47 @@ const std = @import("std"); -const testing = std.testing; const Vec3 = @import("zig-math").Vec3; -const player_module = @import("game-core").player; -const Player = player_module.Player; -const world_runtime = @import("world-runtime"); -const IWorld = world_runtime.IWorld; -const IWorldSimulation = world_runtime.IWorldSimulation; -const BlockType = @import("world-core").BlockType; -const Face = @import("world-core").Face; - -test "Player.init creates player with correct initial state" { - const spawn_pos = Vec3.init(10, 100, 20); - const p = Player.init(spawn_pos, true); - - // Position should be at spawn - try testing.expectEqual(@as(f32, 10), p.position.x); - try testing.expectEqual(@as(f32, 100), p.position.y); - try testing.expectEqual(@as(f32, 20), p.position.z); - - // Velocity should be zero - try testing.expectEqual(@as(f32, 0), p.velocity.x); - try testing.expectEqual(@as(f32, 0), p.velocity.y); - try testing.expectEqual(@as(f32, 0), p.velocity.z); - - // Creative mode settings - try testing.expect(p.fly_mode); - try testing.expect(p.can_fly); - try testing.expect(!p.noclip); - - // Ground state - try testing.expect(!p.is_grounded); - - // Camera should be at eye height - try testing.expectEqual(@as(f32, 10), p.camera.position.x); - try testing.expectEqual(@as(f32, 100 + Player.EYE_HEIGHT), p.camera.position.y); - try testing.expectEqual(@as(f32, 20), p.camera.position.z); - - // Target block should be null initially - try testing.expect(p.target_block == null); -} - -test "Player.init survival mode disables fly" { - const spawn_pos = Vec3.init(0, 64, 0); - const p = Player.init(spawn_pos, false); - - try testing.expect(!p.fly_mode); - try testing.expect(!p.can_fly); - try testing.expect(!p.noclip); -} - -test "Player.getAABB returns correct collision box" { - const p = Player.init(Vec3.init(0, 0, 0), true); - const aabb = p.getAABB(); - - const half_width = Player.WIDTH / 2.0; - - // Min corner - try testing.expectApproxEqAbs(@as(f32, -half_width), aabb.min.x, 0.0001); - try testing.expectApproxEqAbs(@as(f32, 0), aabb.min.y, 0.0001); - try testing.expectApproxEqAbs(@as(f32, -half_width), aabb.min.z, 0.0001); - - // Max corner - try testing.expectApproxEqAbs(@as(f32, half_width), aabb.max.x, 0.0001); - try testing.expectApproxEqAbs(@as(f32, Player.HEIGHT), aabb.max.y, 0.0001); - try testing.expectApproxEqAbs(@as(f32, half_width), aabb.max.z, 0.0001); -} - -test "Player.getAABB at non-zero position" { - const p = Player.init(Vec3.init(100, 50, -30), true); - const aabb = p.getAABB(); - - const half_width = Player.WIDTH / 2.0; - - try testing.expectApproxEqAbs(@as(f32, 100 - half_width), aabb.min.x, 0.0001); - try testing.expectApproxEqAbs(@as(f32, 50), aabb.min.y, 0.0001); - try testing.expectApproxEqAbs(@as(f32, -30 - half_width), aabb.min.z, 0.0001); - - try testing.expectApproxEqAbs(@as(f32, 100 + half_width), aabb.max.x, 0.0001); - try testing.expectApproxEqAbs(@as(f32, 50 + Player.HEIGHT), aabb.max.y, 0.0001); - try testing.expectApproxEqAbs(@as(f32, -30 + half_width), aabb.max.z, 0.0001); -} - -test "Player.getEyePosition returns correct height" { - const p = Player.init(Vec3.init(10, 20, 30), true); - const eye_pos = p.getEyePosition(); - - try testing.expectEqual(@as(f32, 10), eye_pos.x); - try testing.expectEqual(@as(f32, 20 + Player.EYE_HEIGHT), eye_pos.y); - try testing.expectEqual(@as(f32, 30), eye_pos.z); -} - -test "Player.setCreativeMode enables fly mode" { - var p = Player.init(Vec3.init(0, 0, 0), false); - - try testing.expect(!p.can_fly); - try testing.expect(!p.fly_mode); - - p.setCreativeMode(true); - - try testing.expect(p.can_fly); - try testing.expect(p.fly_mode); - try testing.expect(!p.noclip); -} - -test "Player.setCreativeMode disables fly mode" { - var p = Player.init(Vec3.init(0, 0, 0), true); - - try testing.expect(p.can_fly); - try testing.expect(p.fly_mode); - - // Enable noclip first - p.noclip = true; - - p.setCreativeMode(false); - - try testing.expect(!p.can_fly); - try testing.expect(!p.fly_mode); - try testing.expect(!p.noclip); -} - -test "Player.toggleNoclip only works in fly mode" { - var p = Player.init(Vec3.init(0, 0, 0), true); - - try testing.expect(!p.noclip); - - // Should toggle on - p.toggleNoclip(); - try testing.expect(p.noclip); - - // Should toggle off - p.toggleNoclip(); - try testing.expect(!p.noclip); -} - -test "Player.toggleNoclip does nothing when not in fly mode" { - var p = Player.init(Vec3.init(0, 0, 0), false); - - try testing.expect(!p.noclip); - try testing.expect(!p.fly_mode); - - p.toggleNoclip(); - - // Should remain false since fly_mode is false - try testing.expect(!p.noclip); -} - -test "Player constants are reasonable values" { - // Width should be less than 1 (to fit through 1-block gap) - try testing.expect(Player.WIDTH < 1.0); - try testing.expect(Player.WIDTH > 0.0); - - // Height should be reasonable - try testing.expect(Player.HEIGHT > 1.0); - try testing.expect(Player.HEIGHT < 3.0); - - // Eye height should be less than total height - try testing.expect(Player.EYE_HEIGHT < Player.HEIGHT); - try testing.expect(Player.EYE_HEIGHT > 0.0); - - // Speeds should be positive - try testing.expect(Player.WALK_SPEED > 0.0); - try testing.expect(Player.FLY_SPEED > 0.0); - - // Gravity should be positive - try testing.expect(Player.GRAVITY > 0.0); - - // Jump velocity should be positive - try testing.expect(Player.JUMP_VELOCITY > 0.0); - - // Terminal velocity magnitude should be positive - try testing.expect(Player.TERMINAL_VELOCITY > 0.0); - - // Double tap threshold should be reasonable - try testing.expect(Player.DOUBLE_TAP_THRESHOLD > 0.0); - try testing.expect(Player.DOUBLE_TAP_THRESHOLD < 1.0); - - // Reach distance should be positive - try testing.expect(Player.REACH_DISTANCE > 0.0); -} - -test "Player block mutation errors are handled" { - const FailingWorld = struct { - set_block_calls: usize = 0, - - const VTABLE = IWorld.VTable{ - .update = update, - .render = render, - .renderOpaque = render, - .renderFluid = render, - .deinit = deinit, - .getRenderStats = getRenderStats, - .getStats = getStats, - .getLODStats = getLODStats, - .isLODEnabled = isLODEnabled, - .shadowScene = shadowScene, - .enableSaveManager = enableSaveManager, - .takeSaveFailureWarningCount = takeSaveFailureWarningCount, - .pauseGeneration = pauseGeneration, - .isPaused = isPaused, - .collisionWorld = collisionWorld, - .getBlock = getBlock, - .setBlock = setBlock, - .getColumnInfo = getColumnInfo, - .getDebugLightInfo = getDebugLightInfo, - .getRegionInfo = getRegionInfo, - .getGenerator = getGenerator, - .getGeneratorName = getGeneratorName, - .getRenderDistance = getRenderDistance, - .setRenderDistance = setRenderDistance, - .getHorizonDistance = getHorizonDistance, - .setHorizonDistance = setHorizonDistance, - .isLODRenderingEnabled = isLODRenderingEnabled, - .toggleLODRendering = toggleLODRendering, - .getChunkStateCounts = getChunkStateCounts, - .isStartupBusy = isStartupBusy, - .getWorldStateData = getWorldStateData, - .lpvWorld = lpvWorld, - .graphicsRenderView = graphicsRenderView, - .getGpuMeshDispatch = getGpuMeshDispatch, - .isGpuCullingEnabled = isGpuCullingEnabled, - }; - - fn interface(self: *@This()) IWorldSimulation { - return .{ .world = .{ .ptr = self, .vtable = &VTABLE } }; - } - - fn update(_: *anyopaque, _: Vec3, _: f32) anyerror!void {} - fn render(_: *anyopaque, _: @import("engine-math").Mat4, _: Vec3, _: bool) void {} - fn deinit(_: *anyopaque) void {} - fn getRenderStats(_: *anyopaque) @import("world-runtime").RenderStats { - return .{}; - } - fn getStats(_: *anyopaque) @import("world-runtime").WorldStatsData { - return .{ .chunks_loaded = 0, .total_vertices = 0, .gen_queue = 0, .mesh_queue = 0, .upload_queue = 0 }; - } - fn getLODStats(_: *anyopaque) ?@import("world-lod").LODStats { - return null; - } - fn isLODEnabled(_: *anyopaque) bool { - return false; - } - fn shadowScene(_: *anyopaque) @import("engine-rhi").IShadowScene { - return undefined; - } - fn enableSaveManager(_: *anyopaque, _: []const u8, _: []const u8) anyerror!void {} - fn takeSaveFailureWarningCount(_: *anyopaque) usize { - return 0; - } - fn pauseGeneration(_: *anyopaque) void {} - fn isPaused(_: *anyopaque) bool { - return false; - } - fn collisionWorld(_: *anyopaque) @import("engine-physics").VoxelCollisionWorld { - return undefined; - } - fn getBlock(_: *anyopaque, _: i32, _: i32, _: i32) BlockType { - return .stone; - } - fn setBlock(ptr: *anyopaque, _: i32, _: i32, _: i32, _: BlockType) anyerror!void { - const self: *@This() = @ptrCast(@alignCast(ptr)); - self.set_block_calls += 1; - return error.TestExpectedError; - } - fn getColumnInfo(_: *anyopaque, _: i32, _: i32) @import("world-worldgen").ColumnInfo { - return undefined; - } - fn getDebugLightInfo(_: *anyopaque, _: i32, _: i32, _: i32) ?@import("world-runtime").DebugLightInfo { - return null; - } - fn getRegionInfo(_: *anyopaque, _: i32, _: i32) @import("world-worldgen").RegionInfo { - return undefined; - } - fn getGenerator(_: *anyopaque) @import("world-worldgen").Generator { - return undefined; - } - fn getGeneratorName(_: *anyopaque) []const u8 { - return "failing"; - } - fn getRenderDistance(_: *anyopaque) i32 { - return 0; - } - fn setRenderDistance(_: *anyopaque, _: i32) void {} - fn getHorizonDistance(_: *anyopaque) i32 { - return 0; - } - fn setHorizonDistance(_: *anyopaque, _: i32) void {} - fn isLODRenderingEnabled(_: *anyopaque) bool { - return false; - } - fn toggleLODRendering(_: *anyopaque) bool { - return false; - } - fn getChunkStateCounts(_: *anyopaque) @import("world-core").ChunkStateCounts { - return .{}; - } - fn isStartupBusy(_: *anyopaque) bool { - return false; - } - fn getWorldStateData(_: *anyopaque) @import("world-core").WorldStateData { - return .{ .generator_name = "failing", .seed = 0, .gen_queue = 0, .mesh_queue = 0, .upload_queue = 0 }; - } - fn lpvWorld(_: *anyopaque) @import("engine-rhi").ILPVWorld { - return undefined; - } - fn graphicsRenderView(ptr: *anyopaque) @import("engine-rhi").IWorldRenderView { - return .{ .ptr = ptr, .vtable = &.{ .prepareLODCulling = prepareLODCulling, .render = render, .renderOpaque = render, .renderFluid = render } }; - } - fn prepareLODCulling(_: *anyopaque, _: @import("engine-math").Mat4, _: Vec3) void {} - fn getGpuMeshDispatch(_: *anyopaque) @import("world-runtime").GpuMeshDispatch { - return .{ .dispatch_fn = null, .dispatch_ctx = null }; - } - - fn isGpuCullingEnabled(_: *anyopaque) bool { - return false; - } - }; - - var world = FailingWorld{}; - var player = Player.init(Vec3.init(0, 10, 0), true); - player.target_block = .{ .x = 1, .y = 2, .z = 3, .face = Face.east, .distance = 1.0 }; - - player.breakTargetBlock(world.interface()); - try testing.expectEqual(@as(usize, 1), world.set_block_calls); +const Player = @import("game-core").player.Player; + +test "Player initialization preserves spawn state and camera eye height" { + const player = Player.init(Vec3.init(10, 100, 20), true); + try std.testing.expectEqual(@as(f32, 10), player.position.x); + try std.testing.expectEqual(@as(f32, 100), player.position.y); + try std.testing.expectEqual(@as(f32, 20), player.position.z); + try std.testing.expectEqual(@as(f32, 0), player.velocity.y); + try std.testing.expect(player.fly_mode); + try std.testing.expect(player.can_fly); + try std.testing.expect(!player.noclip); + try std.testing.expect(!player.is_grounded); + try std.testing.expectEqual(@as(f32, 100 + Player.EYE_HEIGHT), player.camera.position.y); + try std.testing.expect(player.target_block == null); +} + +test "Player survival mode and creative mode manage flight" { + var player = Player.init(Vec3.zero, false); + try std.testing.expect(!player.fly_mode); + try std.testing.expect(!player.can_fly); + player.setCreativeMode(true); + try std.testing.expect(player.fly_mode); + try std.testing.expect(player.can_fly); + player.toggleNoclip(); + try std.testing.expect(player.noclip); + player.setCreativeMode(false); + try std.testing.expect(!player.fly_mode); + try std.testing.expect(!player.can_fly); + try std.testing.expect(!player.noclip); +} + +test "Player collision box and eye position follow player coordinates" { + const player = Player.init(Vec3.init(100, 50, -30), true); + const box = player.getAABB(); + try std.testing.expectApproxEqAbs(@as(f32, 100 - Player.WIDTH / 2.0), box.min.x, 0.0001); + try std.testing.expectApproxEqAbs(@as(f32, 50 + Player.HEIGHT), box.max.y, 0.0001); + try std.testing.expectEqual(@as(f32, -30), player.getEyePosition().z); +} + +test "Player constants remain usable gameplay values" { + try std.testing.expect(Player.WIDTH > 0 and Player.WIDTH < 1); + try std.testing.expect(Player.EYE_HEIGHT > 0 and Player.EYE_HEIGHT < Player.HEIGHT); + try std.testing.expect(Player.WALK_SPEED > 0 and Player.FLY_SPEED > 0); + try std.testing.expect(Player.GRAVITY > 0 and Player.JUMP_VELOCITY > 0); } diff --git a/src/game/session_tests.zig b/src/game/session_tests.zig index 2cdcdf42..c77c43cd 100644 --- a/src/game/session_tests.zig +++ b/src/game/session_tests.zig @@ -1,88 +1,18 @@ const std = @import("std"); -const testing = std.testing; -const session_module = @import("game-core").session; -const BuildConfig = session_module.BuildConfig; +const session = @import("game-core").session; const Settings = @import("game-core").Settings; -test "distance metadata keeps render distance uncapped and bounds the user LOD horizon" { +test "render distance metadata and far plane support chunk-only streaming" { const range = Settings.metadata.render_distance.kind.int_range; - try testing.expectEqual(@as(i32, 2), range.min); - try testing.expectEqual(std.math.maxInt(i32), range.max); - - const horizon_range = Settings.metadata.horizon_distance.kind.int_range; - try testing.expectEqual(@as(i32, 256), horizon_range.min); - try testing.expectEqual(@as(i32, 512), horizon_range.max); -} - -test "camera far plane covers the configured LOD horizon" { - try testing.expectEqual(@as(f32, 10_000.0), session_module.cameraFarPlaneForHorizon(256)); - try testing.expectEqual(@as(f32, 17_408.0), session_module.cameraFarPlaneForHorizon(1024)); - try testing.expectEqual(@as(f32, 17_408.0), session_module.cameraFarPlaneForDistances(1024, 256)); - try testing.expect(session_module.cameraFarPlaneForHorizon(std.math.maxInt(i32)) > 34_000_000_000.0); -} - -fn chunkDebugRestoreEnabled(build_config: BuildConfig, name: []const u8) bool { - if (!build_config.chunk_debug_mode) return false; - - var it = std.mem.tokenizeScalar(u8, build_config.chunk_debug_enable, ','); - while (it.next()) |token| { - const trimmed = std.mem.trim(u8, token, " \t"); - if (std.ascii.eqlIgnoreCase(trimmed, name)) return true; - } - return false; -} - -test "chunkDebugRestoreEnabled returns false when chunk_debug_mode is false" { - const build_config = BuildConfig{ .chunk_debug_mode = false }; - try testing.expect(!chunkDebugRestoreEnabled(build_config, "lod")); - try testing.expect(!chunkDebugRestoreEnabled(build_config, "water")); -} - -test "chunkDebugRestoreEnabled finds single enabled feature" { - const build_config = BuildConfig{ - .chunk_debug_mode = true, - .chunk_debug_enable = "lod", - }; - try testing.expect(chunkDebugRestoreEnabled(build_config, "lod")); - try testing.expect(!chunkDebugRestoreEnabled(build_config, "water")); -} - -test "chunkDebugRestoreEnabled finds feature among comma-separated list" { - const build_config = BuildConfig{ - .chunk_debug_mode = true, - .chunk_debug_enable = "lod,water,caves", - }; - try testing.expect(chunkDebugRestoreEnabled(build_config, "lod")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "water")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "caves")); - try testing.expect(!chunkDebugRestoreEnabled(build_config, "decorations")); -} - -test "chunkDebugRestoreEnabled trims whitespace" { - const build_config = BuildConfig{ - .chunk_debug_mode = true, - .chunk_debug_enable = " lod , water , caves ", - }; - try testing.expect(chunkDebugRestoreEnabled(build_config, "lod")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "water")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "caves")); -} - -test "chunkDebugRestoreEnabled is case insensitive" { - const build_config = BuildConfig{ - .chunk_debug_mode = true, - .chunk_debug_enable = "LOD", - }; - try testing.expect(chunkDebugRestoreEnabled(build_config, "lod")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "LOD")); - try testing.expect(chunkDebugRestoreEnabled(build_config, "Lod")); + try std.testing.expectEqual(@as(i32, 2), range.min); + try std.testing.expectEqual(std.math.maxInt(i32), range.max); + try std.testing.expectEqual(@as(f32, 10_000), session.cameraFarPlaneForRenderDistance(1)); + try std.testing.expectEqual(@as(f32, 10_000), session.cameraFarPlaneForRenderDistance(256)); + try std.testing.expectEqual(@as(f32, 16_640), session.cameraFarPlaneForRenderDistance(1024)); } -test "chunkDebugRestoreEnabled returns false for missing feature" { - const build_config = BuildConfig{ - .chunk_debug_mode = true, - .chunk_debug_enable = "lod,water", - }; - try testing.expect(!chunkDebugRestoreEnabled(build_config, "caves")); - try testing.expect(!chunkDebugRestoreEnabled(build_config, "decorations")); +test "chunk debug settings retain non-LOD feature selection" { + const config = session.BuildConfig{ .chunk_debug_mode = true, .chunk_debug_enable = "water,caves,decorations" }; + try std.testing.expect(config.chunk_debug_mode); + try std.testing.expectEqualStrings("water,caves,decorations", config.chunk_debug_enable); } diff --git a/src/integration_test.zig b/src/integration_test.zig index 448bd638..d49601fe 100644 --- a/src/integration_test.zig +++ b/src/integration_test.zig @@ -18,9 +18,7 @@ const Screen = @import("game-ui").screen; const rhi = @import("engine-rhi"); const UISystem = @import("engine-ui").UISystem; const c = @import("c").c; -const engine_math = @import("engine-math"); const world_core = @import("world-core"); -const world_lod = @import("world-lod"); const world_meshing = @import("world-meshing"); const world_runtime = @import("world-runtime"); const SaveManager = @import("world-persistence").SaveManager; @@ -28,71 +26,6 @@ const SaveManager = @import("world-persistence").SaveManager; const EngineContext = Screen.EngineContext; const IScreen = Screen.IScreen; -fn initEditPersistenceLODManager( - allocator: std.mem.Allocator, - config: *world_lod.lod_chunk.LODConfig, - atlas: *const @import("engine-assets").TextureAtlas, - context: *u8, -) !*world_lod.LODManager { - const LODLevel = world_lod.LODLevel; - const LODSimplifiedData = world_lod.lod_chunk.LODSimplifiedData; - const uploads = world_lod.lod_upload_queue; - - const bridge = world_lod.LODGPUBridge{ - .on_upload = struct { - fn f(_: *world_lod.LODMesh, _: *anyopaque) rhi.RhiError!void {} - }.f, - .on_destroy = struct { - fn f(_: *world_lod.LODMesh, _: *anyopaque) void {} - }.f, - .on_wait_idle = struct { - fn f(_: *anyopaque) void {} - }.f, - .ctx = @ptrCast(context), - }; - const render_interface = world_lod.LODRenderInterface{ - .render_fn = struct { - fn f( - _: *anyopaque, - _: *const [LODLevel.count]uploads.MeshMap, - _: *const [LODLevel.count]uploads.RegionMap, - _: world_lod.lod_chunk.ILODConfig, - _: engine_math.Mat4, - _: engine_math.Vec3, - _: ?world_lod.LODManager.ChunkChecker, - _: ?*anyopaque, - _: bool, - _: ?i32, - _: uploads.LODRenderLayer, - _: ?*world_lod.LODStats, - _: ?*world_lod.lod_stats.LODProfilingCollector, - ) void {} - }.f, - .deinit_fn = struct { - fn f(_: *anyopaque) void {} - }.f, - .ptr = @ptrCast(context), - }; - const generator = world_lod.LODGenerator{ - .ptr = @ptrCast(context), - .generate_heightmap_only = struct { - fn f(_: *anyopaque, _: *LODSimplifiedData, _: i32, _: i32, _: LODLevel, _: ?*const std.atomic.Value(bool)) void {} - }.f, - .maybe_recenter_cache = struct { - fn f(_: *anyopaque, _: i32, _: i32) bool { - return false; - } - }.f, - .seed = 923, - .identity_hash = 923, - .version = 1, - }; - - const manager = try world_lod.LODManager.init(allocator, config.interface(), bridge, render_interface, generator, atlas); - manager.cleanup_covered_regions = false; - return manager; -} - /// CPU-only world fixture for save/reload integration evidence. Its undefined /// graphics/streaming members are intentionally never reached: this test uses /// only the production storage and persistence facade methods. @@ -104,14 +37,10 @@ fn initStorageOnlyPersistenceWorld(allocator: std.mem.Allocator) world_runtime.W .allocator = allocator, .generator = undefined, .render_distance = 8, - .lod_chunk_render_radius_limit = 8, - .horizon_distance = 512, .rhi = undefined, .paused = false, .safe_mode = false, .safe_render_distance = 8, - .lod = null, - .lod_enabled = false, .save_manager = null, .gpu_block_buffer = null, .mutation = undefined, @@ -256,12 +185,9 @@ test "smoke test: launch, generate, render, exit" { // inexpensive even when the local test settings use a large radius. const settings = app.engineContext().settings; const requested_detail = if (settings.render_distance > 2) settings.render_distance - 1 else settings.render_distance + 1; - const requested_horizon = if (settings.horizon_distance > requested_detail) settings.horizon_distance - 1 else settings.horizon_distance + 1; settings.render_distance = requested_detail; - settings.horizon_distance = requested_horizon; try app.runSingleFrame(); - try testing.expectEqual(requested_detail, world_screen.session.world.streamer.lod_coordinator.targetRenderDistance()); - try testing.expectEqual(world_lod.lod_chunk.LODConfig.normalizeUserHorizonDistance(requested_detail, requested_horizon), world_screen.session.world.horizon_distance); + try testing.expectEqual(requested_detail, world_screen.session.world.render_distance); var upload_screen: ?*UploadScreen = null; const upload_factory = try Screen.makeScreenFactory(UploadScreenFactory, test_allocator, .{ .context = app.engineContext(), .result = &upload_screen }); @@ -271,9 +197,8 @@ test "smoke test: launch, generate, render, exit" { // The overlay draws the world's menu-safe background, requests replacement // during draw, and App applies it only after endFrame. This is the real - // Quit-to-Title ordering, including suppression of distant LOD beneath the - // retained pause overlay, a GPU idle drain, and boundary-time construction - // of the replacement's Vulkan resources. + // Quit-to-Title ordering retains the pause overlay until the frame boundary, + // drains GPU work, then constructs replacement Vulkan resources. const fault_count_before_replace = app.render_system.getRHI().query().getFaultCount(); try app.runSingleFrame(); const active_upload_screen = upload_screen.?; @@ -329,7 +254,7 @@ test "smoke test: launch, generate, render, exit" { try testing.expectEqual(@as(u32, 0), val_count); } -test "end-to-end edited terrain survives async save reload compact rebuild and corruption recovery" { +test "end-to-end edited terrain survives save reload" { const allocator = testing.allocator; var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -338,7 +263,7 @@ test "end-to-end edited terrain survives async save reload compact rebuild and c const save_path = try dir.realpath(".", &path_buf); // Persist a real world-owned full-detail chunk, destroy that storage-only - // world state, then recreate it and reload before LOD ingestion. + // world state, then recreate it and reload it. var source_world = initStorageOnlyPersistenceWorld(allocator); source_world.save_manager = try SaveManager.init(allocator, save_path, "edit-persistence", 923, "integration"); const edited_data = try source_world.storage.getOrCreate(0, 0); @@ -359,81 +284,4 @@ test "end-to-end edited terrain survives async save reload compact rebuild and c const load_result = reloaded_world.loadChunkFromSave(0, 0, &reloaded_chunk); try testing.expect(load_result == .success or load_result == .success_relight_required); try testing.expectEqual(world_core.BlockType.grass, reloaded_chunk.getBlock(0, 96, 0)); - - var config = world_lod.lod_chunk.LODConfig{}; - var atlas = @import("engine-assets").TextureAtlas{ - .texture = undefined, - .normal_texture = null, - .roughness_texture = null, - .displacement_texture = null, - .allocator = allocator, - .pack_manager = null, - .tile_size = 16, - .atlas_size = 256, - .has_pbr = false, - .tile_mappings = [_]@import("engine-assets").TextureAtlas.BlockTiles{@import("engine-assets").TextureAtlas.BlockTiles.uniform(0)} ** world_core.MAX_BLOCK_TYPES, - }; - var manager_context: u8 = 0; - const key = world_lod.lod_chunk.LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod3 }; - var manager = try initEditPersistenceLODManager(allocator, &config, &atlas, &manager_context); - - const region = try allocator.create(world_lod.LODChunk); - region.* = world_lod.LODChunk.init(key.rx, key.rz, key.lod); - region.data = .{ .simplified = try world_lod.lod_chunk.LODSimplifiedData.init(allocator, key.lod) }; - region.state = .renderable; - try manager.regions[@intFromEnum(key.lod)].put(key, region); - try manager.enableCache(save_path); - - manager.ingestChunk(0, 0, &reloaded_chunk, .edited); - try testing.expectEqual(@as(f32, 96.0), region.data.simplified.getHeight(0, 0)); - try testing.expectEqual(world_core.LODColumnProvenance.edited, region.data.simplified.getColumnProvenance(0, 0)); - - // More than the former expiry window of unresolved retries must retain the - // edit. This exercises the public drain path under sustained unload pressure. - manager.requestIngestion(4096, -4096, .edited); - for (0..1000) |_| manager.drainPendingIngestions(); - var found_durable_edit = false; - for (manager.ingestion_queue.pending_ingestions.items) |entry| { - if (entry.cx == 4096 and entry.cz == -4096 and entry.provenance == .edited) found_durable_edit = true; - } - try testing.expect(found_durable_edit); - - // The update-side call only snapshots/enqueues; the explicit test boundary - // waits for the dedicated cache worker and applies its completion. - manager.flushDirtyStores(); - manager.flushCacheIO(); - try testing.expect(!region.store_dirty); - manager.deinit(); - - // Recreate manager state, reload the persisted source through the manager - // API, and deterministically rebuild the production compact representation. - manager = try initEditPersistenceLODManager(allocator, &config, &atlas, &manager_context); - defer manager.deinit(); - try manager.enableCache(save_path); - var loaded = manager.loadCachedSourceData(key) orelse return error.ExpectedCacheHit; - defer loaded.deinit(); - try testing.expectEqual(@as(f32, 96.0), loaded.getHeight(0, 0)); - try testing.expectEqual(world_core.LODColumnProvenance.edited, loaded.getColumnProvenance(0, 0)); - - var first_mesh = world_lod.LODMesh.init(allocator, .lod3); - defer first_mesh.releasePendingCompactTile(); - var second_mesh = world_lod.LODMesh.init(allocator, .lod3); - defer second_mesh.releasePendingCompactTile(); - try first_mesh.buildCompactTile(&loaded); - try second_mesh.buildCompactTile(&loaded); - const first_sample = first_mesh.compact_tile.?.sample(0, 0).?; - const second_sample = second_mesh.compact_tile.?.sample(0, 0).?; - try testing.expectEqualSlices(u8, first_sample.bytes[0..], second_sample.bytes[0..]); - try testing.expectEqual(@as(f32, 96.0), first_sample.decode().terrain_height); - try testing.expectEqual(world_core.LODColumnProvenance.edited, first_sample.decode().provenance); - - // A malformed source-store container is discarded rather than being - // returned as valid edited terrain on the next reload. - const container_path = try world_lod.lod_store.containerPath(allocator, save_path, manager.cacheKey(key)); - defer allocator.free(container_path); - const corrupt_file = try fs.cwd().createFile(container_path, .{ .truncate = true }); - try corrupt_file.writeAll("not a region container"); - corrupt_file.close(); - try testing.expect(manager.loadCachedSourceData(key) == null); - try testing.expectError(error.FileNotFound, fs.cwd().openFile(container_path, .{})); } diff --git a/src/interface_mock_tests.zig b/src/interface_mock_tests.zig index 7976d0d2..241ed26e 100644 --- a/src/interface_mock_tests.zig +++ b/src/interface_mock_tests.zig @@ -1,570 +1,21 @@ const std = @import("std"); -const testing = std.testing; -const Vec3 = @import("zig-math").Vec3; -const Mat4 = @import("zig-math").Mat4; -const IRenderSettings = @import("engine-core").interfaces.IRenderSettings; -const IChunkStorage = @import("world-meshing").IChunkStorage; -const ChunkStorage = @import("world-meshing").ChunkStorage; -const ChunkData = @import("world-meshing").ChunkData; -const IWorld = @import("world-runtime").IWorld; -const GpuMeshDispatch = @import("world-runtime").GpuMeshDispatch; -const WorldStatsData = @import("world-runtime").WorldStatsData; -const RenderStats = @import("world-runtime").RenderStats; -const BlockType = @import("world-core").BlockType; -const VoxelCollisionWorld = @import("engine-physics").VoxelCollisionWorld; -const shadow_scene = @import("engine-shadows").shadow_scene; -const ILPVWorld = @import("engine-rhi").ILPVWorld; -const GraphicsWorldRenderView = @import("engine-rhi").IWorldRenderView; -const ShadowConfig = @import("engine-rhi").ShadowConfig; -const Settings = @import("game-core").settings.data.Settings; -const settings_apply = @import("game-core").settings.apply_logic; - -pub const std_options: std.Options = .{ .log_level = .err }; - -const MockChunkStorage = struct { - get_calls: usize = 0, - count_calls: usize = 0, - total_calls: usize = 0, - renderable_calls: usize = 0, - count_value: usize = 11, - total_value: u64 = 22, - renderable_value: bool = true, - - pub fn interface(self: *MockChunkStorage) IChunkStorage { - return .{ .ptr = self, .vtable = &VTABLE }; - } - - const VTABLE = IChunkStorage.VTable{ - .get = get, - .count = count, - .totalVertexCount = totalVertexCount, - .isChunkRenderable = isChunkRenderable, - }; - - fn get(ptr: *anyopaque, cx: i32, cz: i32) ?*ChunkData { - _ = cx; - _ = cz; - const self: *MockChunkStorage = @ptrCast(@alignCast(ptr)); - self.get_calls += 1; - return null; - } - - fn count(ptr: *anyopaque) usize { - const self: *MockChunkStorage = @ptrCast(@alignCast(ptr)); - self.count_calls += 1; - return self.count_value; - } - - fn totalVertexCount(ptr: *anyopaque) u64 { - const self: *MockChunkStorage = @ptrCast(@alignCast(ptr)); - self.total_calls += 1; - return self.total_value; - } - - fn isChunkRenderable(ptr: *anyopaque, cx: i32, cz: i32) bool { - _ = cx; - _ = cz; - const self: *MockChunkStorage = @ptrCast(@alignCast(ptr)); - self.renderable_calls += 1; - return self.renderable_value; - } -}; - -const MockRenderSettings = struct { - call_count: usize = 0, - wireframe: bool = false, - vsync: bool = false, - textures_enabled: bool = false, - anisotropic_filtering: u8 = 0, - fxaa: bool = false, - bloom: bool = false, - bloom_intensity: f32 = 0, - taa_blend_factor: f32 = 0, - taa_velocity_rejection: f32 = 0, - vignette_enabled: bool = false, - vignette_intensity: f32 = 0, - film_grain_enabled: bool = false, - film_grain_intensity: f32 = 0, - volumetric_density: f32 = 0, - debug_shadow_view: bool = false, - shadow_resolution: u32 = 0, - msaa_samples: u8 = 0, - - pub fn interface(self: *MockRenderSettings) IRenderSettings { - return .{ .ptr = self, .vtable = &VTABLE }; - } - - const VTABLE = IRenderSettings.VTable{ - .setWireframe = setWireframe, - .setVSync = setVSync, - .setTexturesEnabled = setTexturesEnabled, - .setAnisotropicFiltering = setAnisotropicFiltering, - .setFXAA = setFXAA, - .setBloom = setBloom, - .setBloomIntensity = setBloomIntensity, - .setTAABlendFactor = setTAABlendFactor, - .setTAAVelocityRejection = setTAAVelocityRejection, - .setVignetteEnabled = setVignetteEnabled, - .setVignetteIntensity = setVignetteIntensity, - .setFilmGrainEnabled = setFilmGrainEnabled, - .setFilmGrainIntensity = setFilmGrainIntensity, - .setVolumetricDensity = setVolumetricDensity, - .setDebugShadowView = setDebugShadowView, - .setShadowResolution = setShadowResolution, - .setMSAA = setMSAA, - }; - - fn setWireframe(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.wireframe = enabled; - self.call_count += 1; - } - - fn setShadowResolution(ptr: *anyopaque, resolution: u32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.shadow_resolution = resolution; - self.call_count += 1; - } - - fn setVSync(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.vsync = enabled; - self.call_count += 1; - } - - fn setTexturesEnabled(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.textures_enabled = enabled; - self.call_count += 1; - } - - fn setAnisotropicFiltering(ptr: *anyopaque, level: u8) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.anisotropic_filtering = level; - self.call_count += 1; - } - - fn setFXAA(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.fxaa = enabled; - self.call_count += 1; - } - - fn setBloom(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.bloom = enabled; - self.call_count += 1; - } - - fn setBloomIntensity(ptr: *anyopaque, intensity: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.bloom_intensity = intensity; - self.call_count += 1; - } - - fn setTAABlendFactor(ptr: *anyopaque, value: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.taa_blend_factor = value; - self.call_count += 1; - } - - fn setTAAVelocityRejection(ptr: *anyopaque, value: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.taa_velocity_rejection = value; - self.call_count += 1; - } - - fn setVignetteEnabled(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.vignette_enabled = enabled; - self.call_count += 1; - } - - fn setVignetteIntensity(ptr: *anyopaque, intensity: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.vignette_intensity = intensity; - self.call_count += 1; - } - - fn setFilmGrainEnabled(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.film_grain_enabled = enabled; - self.call_count += 1; - } - - fn setFilmGrainIntensity(ptr: *anyopaque, intensity: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.film_grain_intensity = intensity; - self.call_count += 1; - } - - fn setVolumetricDensity(ptr: *anyopaque, density: f32) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.volumetric_density = density; - self.call_count += 1; - } - - fn setDebugShadowView(ptr: *anyopaque, enabled: bool) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.debug_shadow_view = enabled; - self.call_count += 1; - } - - fn setMSAA(ptr: *anyopaque, samples: u8) void { - const self: *MockRenderSettings = @ptrCast(@alignCast(ptr)); - self.msaa_samples = samples; - self.call_count += 1; - } -}; - -const MockWorld = struct { - update_calls: usize = 0, - render_calls: usize = 0, - deinit_calls: usize = 0, - get_render_stats_calls: usize = 0, - get_stats_calls: usize = 0, - get_lod_stats_calls: usize = 0, - is_lod_enabled_calls: usize = 0, - shadow_scene_calls: usize = 0, - shadow_pass_calls: usize = 0, - last_render_lod: bool = false, - render_stats: RenderStats = .{ .chunks_total = 4, .chunks_rendered = 3, .chunks_culled = 1, .vertices_rendered = 99 }, - stats: WorldStatsData = .{ .chunks_loaded = 8, .total_vertices = 123, .gen_queue = 5, .mesh_queue = 6, .upload_queue = 7 }, - lod_enabled: bool = false, - - pub fn interface(self: *MockWorld) IWorld { - return .{ .ptr = self, .vtable = &VTABLE }; - } - - const VTABLE = IWorld.VTable{ - .update = update, - .render = render, - .renderOpaque = renderOpaque, - .renderFluid = renderFluid, - .deinit = deinit, - .getRenderStats = getRenderStats, - .getStats = getStats, - .getLODStats = getLODStats, - .isLODEnabled = isLODEnabled, - .shadowScene = shadowScene, - .enableSaveManager = enableSaveManager, - .takeSaveFailureWarningCount = takeSaveFailureWarningCount, - .pauseGeneration = pauseGeneration, - .isPaused = isPaused, - .collisionWorld = collisionWorld, - .getBlock = getBlock, - .setBlock = setBlock, - .getColumnInfo = getColumnInfo, - .getDebugLightInfo = getDebugLightInfo, - .getRegionInfo = getRegionInfo, - .getGenerator = getGenerator, - .getGeneratorName = getGeneratorName, - .getRenderDistance = getRenderDistance, - .setRenderDistance = setRenderDistance, - .getHorizonDistance = getHorizonDistance, - .setHorizonDistance = setHorizonDistance, - .isLODRenderingEnabled = isLODRenderingEnabled, - .toggleLODRendering = toggleLODRendering, - .getChunkStateCounts = getChunkStateCounts, - .isStartupBusy = isStartupBusy, - .getWorldStateData = getWorldStateData, - .lpvWorld = lpvWorld, - .graphicsRenderView = graphicsRenderView, - .getGpuMeshDispatch = getGpuMeshDispatch, - .isGpuCullingEnabled = isGpuCullingEnabled, - }; - - const SHADOW_VTABLE = shadow_scene.IShadowScene.VTable{ - .renderShadowPass = renderShadowPass, - }; - - fn update(ptr: *anyopaque, player_pos: Vec3, dt: f32) anyerror!void { - _ = player_pos; - _ = dt; - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.update_calls += 1; - } - - fn render(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - _ = view_proj; - _ = camera_pos; - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.render_calls += 1; - self.last_render_lod = render_lod; - } - - fn prepareLODCulling(_: *anyopaque, _: Mat4, _: Vec3) void {} - - fn renderOpaque(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - render(ptr, view_proj, camera_pos, render_lod); - } - - fn renderFluid(ptr: *anyopaque, view_proj: Mat4, camera_pos: Vec3, render_lod: bool) void { - render(ptr, view_proj, camera_pos, render_lod); - } - - fn deinit(ptr: *anyopaque) void { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.deinit_calls += 1; - } - - fn getRenderStats(ptr: *anyopaque) RenderStats { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.get_render_stats_calls += 1; - return self.render_stats; - } - - fn getStats(ptr: *anyopaque) WorldStatsData { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.get_stats_calls += 1; - return self.stats; - } - - fn getLODStats(ptr: *anyopaque) ?@import("world-lod").LODStats { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.get_lod_stats_calls += 1; - return null; - } - - fn isLODEnabled(ptr: *anyopaque) bool { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.is_lod_enabled_calls += 1; - return self.lod_enabled; - } - - fn shadowScene(ptr: *anyopaque) shadow_scene.IShadowScene { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.shadow_scene_calls += 1; - return .{ .ptr = self, .vtable = &SHADOW_VTABLE }; - } - - fn renderShadowPass(ptr: *anyopaque, light_space_matrix: Mat4, camera_pos: Vec3, caster_min: Vec3, caster_max: Vec3, shadow_config: ShadowConfig) void { - _ = light_space_matrix; - _ = camera_pos; - _ = caster_min; - _ = caster_max; - _ = shadow_config; - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.shadow_pass_calls += 1; - } - - fn enableSaveManager(ptr: *anyopaque, save_dir_path: []const u8, world_name: []const u8) anyerror!void { - _ = ptr; - _ = save_dir_path; - _ = world_name; - } - - fn takeSaveFailureWarningCount(ptr: *anyopaque) usize { - _ = ptr; - return 0; - } - - fn pauseGeneration(ptr: *anyopaque) void { - _ = ptr; - } - - fn isPaused(ptr: *anyopaque) bool { - _ = ptr; - return false; - } - - fn collisionWorld(ptr: *anyopaque) VoxelCollisionWorld { - _ = ptr; - return undefined; - } - - fn getBlock(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32) BlockType { - _ = ptr; - _ = world_x; - _ = world_y; - _ = world_z; - return .air; - } - - fn setBlock(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32, block: BlockType) anyerror!void { - _ = ptr; - _ = world_x; - _ = world_y; - _ = world_z; - _ = block; - } - - fn getColumnInfo(ptr: *anyopaque, world_x: i32, world_z: i32) @import("world-worldgen").ColumnInfo { - _ = ptr; - _ = world_x; - _ = world_z; - return undefined; - } - - fn getDebugLightInfo(ptr: *anyopaque, world_x: i32, world_y: i32, world_z: i32) ?@import("world-runtime").DebugLightInfo { - _ = ptr; - _ = world_x; - _ = world_y; - _ = world_z; - return null; - } - - fn getRegionInfo(ptr: *anyopaque, world_x: i32, world_z: i32) @import("world-worldgen").RegionInfo { - _ = ptr; - _ = world_x; - _ = world_z; - return undefined; - } - - fn getGenerator(ptr: *anyopaque) @import("world-worldgen").Generator { - _ = ptr; - return undefined; - } - - fn getGeneratorName(ptr: *anyopaque) []const u8 { - _ = ptr; - return "mock"; - } - - fn getRenderDistance(ptr: *anyopaque) i32 { - _ = ptr; - return 0; - } - - fn setRenderDistance(ptr: *anyopaque, distance: i32) void { - _ = ptr; - _ = distance; - } - - fn getHorizonDistance(ptr: *anyopaque) i32 { - _ = ptr; - return 0; - } - - fn setHorizonDistance(ptr: *anyopaque, distance: i32) void { - _ = ptr; - _ = distance; - } - - fn isLODRenderingEnabled(ptr: *anyopaque) bool { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - return self.lod_enabled; - } - - fn toggleLODRendering(ptr: *anyopaque) bool { - const self: *MockWorld = @ptrCast(@alignCast(ptr)); - self.lod_enabled = !self.lod_enabled; - return self.lod_enabled; - } - - fn getChunkStateCounts(ptr: *anyopaque) @import("engine-ui").chunk_inspector_overlay.ChunkStateCounts { - _ = ptr; - return .{}; - } - - fn isStartupBusy(ptr: *anyopaque) bool { - _ = ptr; - return false; - } - - fn getWorldStateData(ptr: *anyopaque) @import("engine-ui").chunk_inspector_overlay.WorldStateData { - _ = ptr; - return .{ .generator_name = "mock", .seed = 0, .gen_queue = 0, .mesh_queue = 0, .upload_queue = 0 }; - } - - fn lpvWorld(ptr: *anyopaque) ILPVWorld { - _ = ptr; - return undefined; - } - - fn graphicsRenderView(ptr: *anyopaque) GraphicsWorldRenderView { - return .{ .ptr = ptr, .vtable = &.{ .prepareLODCulling = prepareLODCulling, .render = render, .renderOpaque = renderOpaque, .renderFluid = renderFluid } }; - } - - fn getGpuMeshDispatch(ptr: *anyopaque) GpuMeshDispatch { - _ = ptr; - return .{ .dispatch_fn = null, .dispatch_ctx = null }; - } - - fn isGpuCullingEnabled(ptr: *anyopaque) bool { - _ = ptr; - return false; - } -}; - -test "IChunkStorage mock dispatch" { - var storage = MockChunkStorage{}; - const storage_if = storage.interface(); - - try testing.expect(storage_if.get(1, 2) == null); - try testing.expectEqual(@as(usize, 11), storage_if.count()); - try testing.expectEqual(@as(u64, 22), storage_if.totalVertexCount()); - try testing.expect(storage_if.isChunkRenderable(4, 5)); - try testing.expectEqual(@as(usize, 1), storage.get_calls); - try testing.expectEqual(@as(usize, 1), storage.count_calls); - try testing.expectEqual(@as(usize, 1), storage.total_calls); - try testing.expectEqual(@as(usize, 1), storage.renderable_calls); -} - -test "ChunkStorage interface forwards empty storage" { - var storage = ChunkStorage.init(testing.allocator); - defer storage.deinitWithoutRHI(); - - const storage_if = storage.interface(); - try testing.expect(storage_if.get(0, 0) == null); - try testing.expectEqual(@as(usize, 0), storage_if.count()); - try testing.expectEqual(@as(u64, 0), storage_if.totalVertexCount()); +const InputBinding = @import("engine-input").InputBinding; +const MouseButton = @import("engine-core").interfaces.MouseButton; +const Settings = @import("game-core").Settings; + +test "input binding interface equality distinguishes binding kinds" { + const left = InputBinding{ .mouse_button = MouseButton.left }; + try std.testing.expect(left.eql(.{ .mouse_button = .left })); + try std.testing.expect(!left.eql(.{ .mouse_button = .right })); + try std.testing.expect(!left.eql(.{ .none = {} })); } -test "Render settings interface applies settings" { +test "settings interface data remains independently configurable" { var settings = Settings{}; settings.vsync = false; settings.wireframe_enabled = true; - settings.textures_enabled = false; - settings.debug_shadows_active = true; - settings.anisotropic_filtering = 8; - settings.msaa_samples = 2; - settings.taa_blend_factor = 0.75; - settings.taa_velocity_rejection = 0.11; - - var mock = MockRenderSettings{}; - settings_apply.applyToRenderSettings(&settings, mock.interface()); - - try testing.expectEqual(false, mock.vsync); - try testing.expectEqual(true, mock.wireframe); - try testing.expectEqual(false, mock.textures_enabled); - try testing.expectEqual(true, mock.debug_shadow_view); - try testing.expectEqual(@as(u8, 8), mock.anisotropic_filtering); - try testing.expectEqual(@as(u8, 2), mock.msaa_samples); - try testing.expectEqual(true, mock.fxaa); - try testing.expectApproxEqAbs(@as(f32, 0.75), mock.taa_blend_factor, 0.0001); - try testing.expectApproxEqAbs(@as(f32, 0.11), mock.taa_velocity_rejection, 0.0001); - try testing.expectEqual(@as(usize, 9), mock.call_count); -} - -test "IWorld mock dispatch" { - var world = MockWorld{}; - const world_if = world.interface(); - - try world_if.update(Vec3.init(1, 2, 3), 0.25); - world_if.render(Mat4.identity, Vec3.zero, true); - try testing.expect(world.last_render_lod); - - const render_stats = world_if.getRenderStats(); - const stats = world_if.getStats(); - try testing.expectEqual(@as(u32, 4), render_stats.chunks_total); - try testing.expectEqual(@as(u32, 3), render_stats.chunks_rendered); - try testing.expectEqual(@as(usize, 8), stats.chunks_loaded); - try testing.expectEqual(@as(u64, 123), stats.total_vertices); - try testing.expect(world_if.getLODStats() == null); - try testing.expect(!world_if.isLODEnabled()); - - const shadow = world_if.shadowScene(); - shadow.renderShadowPass(Mat4.identity, Vec3.zero, Vec3.zero, Vec3.zero, .{}); - world_if.deinit(); - - try testing.expectEqual(@as(usize, 1), world.update_calls); - try testing.expectEqual(@as(usize, 1), world.render_calls); - try testing.expectEqual(@as(usize, 1), world.get_render_stats_calls); - try testing.expectEqual(@as(usize, 1), world.get_stats_calls); - try testing.expectEqual(@as(usize, 1), world.get_lod_stats_calls); - try testing.expectEqual(@as(usize, 1), world.is_lod_enabled_calls); - try testing.expectEqual(@as(usize, 1), world.shadow_scene_calls); - try testing.expectEqual(@as(usize, 1), world.shadow_pass_calls); - try testing.expectEqual(@as(usize, 1), world.deinit_calls); + settings.render_distance = 24; + try std.testing.expect(!settings.vsync); + try std.testing.expect(settings.wireframe_enabled); + try std.testing.expectEqual(@as(i32, 24), settings.render_distance); } diff --git a/src/lod_bench_main.zig b/src/lod_bench_main.zig deleted file mode 100644 index 3144b005..00000000 --- a/src/lod_bench_main.zig +++ /dev/null @@ -1,74 +0,0 @@ -//! CPU-only LOD heightmap generation benchmark (no graphics/Vulkan needed). -//! -//! Measures the actual per-region cost of `generateHeightmapOnly` for each LOD -//! level, so we can tell whether LOD loading is bounded by generation cost or by -//! worker throughput/contention. Run with: `zig build lod-bench`. - -const std = @import("std"); -const world_worldgen = @import("world-worldgen"); -const world_core = @import("world-core"); -const LODLevel = world_core.LODLevel; -const LODSimplifiedData = world_core.LODSimplifiedData; - -fn nowMs() i64 { - return std.Io.Clock.real.now(std.Options.debug_io).toMilliseconds(); -} - -pub fn main(init: std.process.Init) !void { - var stdout_buffer: [4096]u8 = undefined; - var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buffer); - const stdout = &stdout_writer.interface; - - const allocator = init.arena.allocator(); - - const seed: u64 = 1337; - var gen = try world_worldgen.createGeneratorById("overworld", seed, allocator); - defer gen.deinit(allocator); - - try stdout.print("=== LOD heightmap generation benchmark (generator='overworld' seed={}) ===\n", .{seed}); - - const levels = [_]LODLevel{ .lod1, .lod2, .lod3, .lod4 }; - const samples: u32 = 6; - - for (levels) |lvl| { - var min_ms: f64 = std.math.floatMax(f64); - var max_ms: f64 = 0; - var sum_ms: f64 = 0; - var width: u32 = 0; - - var done: u32 = 0; - var i: i32 = 0; - while (done < samples) : (i += 1) { - var data = try LODSimplifiedData.init(allocator, lvl); - defer data.deinit(); - width = data.width; - - const t0 = nowMs(); - gen.generateHeightmapOnly(&data, i * 7, i * 5, lvl, null); - const dt = nowMs() - t0; - - const ms = @as(f64, @floatFromInt(@max(dt, 0))); - if (ms < min_ms) min_ms = ms; - if (ms > max_ms) max_ms = ms; - sum_ms += ms; - done += 1; - } - - const avg = sum_ms / @as(f64, @floatFromInt(samples)); - try stdout.print(" {s}: grid={}x{} (~{} cols) avg={d:.1}ms min={d:.1}ms max={d:.1}ms\n", .{ - @tagName(lvl), - width, - width, - width * width, - avg, - min_ms, - max_ms, - }); - } - - try stdout.print("\nInterpretation:\n", .{}); - try stdout.print(" - If LOD4 avg < ~500ms: generation is cheap; LOD loading is bounded by\n", .{}); - try stdout.print(" worker throughput/contention, not per-region cost.\n", .{}); - try stdout.print(" - If LOD4 avg > ~2000ms: generation itself is the bottleneck.\n", .{}); - try stdout.flush(); -} diff --git a/src/tests.zig b/src/tests.zig index 93f762d1..6fd97fba 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -2,7 +2,8 @@ //! //! This file imports all test modules. Individual tests live in their //! respective modules (math_tests, noise_tests, etc.) or in dedicated -//! test files alongside the source they validate. +//! test files alongside the source they validate. Module-owned test blocks +//! are registered from direct source roots in build.zig. //! //! Run with: zig build test @@ -12,6 +13,43 @@ pub const std_options: std.Options = .{ .log_level = .err, }; +test "WorldStreamer prioritizes chunks in the player movement direction" { + const PlayerMovement = @import("world-runtime").PlayerMovement; + const Vec3 = @import("engine-math").Vec3; + + var movement = PlayerMovement{}; + try std.testing.expect(movement.update(Vec3.init(8, 0, 0), 1.0)); + try std.testing.expect(movement.priorityWeight(1, 0) < movement.priorityWeight(-1, 0)); +} + +test "benchmark warmup waits for stable geometry before sampling" { + const BenchmarkRunner = @import("game-core").BenchmarkRunner; + const GpuTimingResults = @import("engine-rhi").GpuTimingResults; + const WorldStats = @import("engine-ui").WorldStats; + + var runner = try BenchmarkRunner.init(std.testing.allocator, "low", "stationary", 6, 1, 1, "Debug", "test", "unused.json"); + defer runner.deinit(); + const world_stats = WorldStats{ + .chunks_total = 1, + .chunks_rendered = 1, + .chunks_culled = 0, + .vertices_rendered = 3, + .gen_queue = 0, + .mesh_queue = 0, + .upload_queue = 0, + }; + const gpu_timing = std.mem.zeroes(GpuTimingResults); + + try runner.recordFrame(0.5, 120, gpu_timing, world_stats, 1, 1); + try std.testing.expect(!runner.warmup_ready); + try runner.recordFrame(0.5, 120, gpu_timing, world_stats, 1, 1); + try std.testing.expect(runner.warmup_ready); + try std.testing.expectEqual(@as(usize, 0), runner.samples.items.len); + + try runner.recordFrame(0.5, 120, gpu_timing, world_stats, 1, 1); + try std.testing.expectEqual(@as(usize, 1), runner.samples.items.len); +} + test { // Inline module test files (Issue #551) _ = @import("math_tests.zig"); @@ -49,7 +87,6 @@ test { _ = @import("engine-graphics").utils_tests; _ = @import("vulkan_tests.zig"); _ = @import("engine-graphics").rhi_tests; - _ = @import("engine-graphics").lod_culling_system; _ = @import("engine-rhi").rhi_contract_tests; _ = @import("engine-rhi").culling; _ = @import("engine-clouds").cloud_system; @@ -90,7 +127,6 @@ test { _ = @import("world-persistence").chunk_serializer; _ = @import("world-persistence").level_data; _ = @import("world-persistence").save_manager; - _ = @import("world-meshing").meshing.quadric_simplifier; _ = @import("world-meshing").chunk_storage_tests; _ = @import("world-meshing").chunk_storage_extended_tests; _ = @import("world-meshing").gpu_block_buffer_tests; diff --git a/src/world_inline_tests.zig b/src/world_inline_tests.zig index b49e230d..ff157711 100644 --- a/src/world_inline_tests.zig +++ b/src/world_inline_tests.zig @@ -13,7 +13,6 @@ const worldToLocal = world_core.worldToLocal; const BlockType = world_core.BlockType; const block_registry = world_core.block_registry; const ChunkMesh = @import("world-meshing").ChunkMesh; -const ChunkStorage = @import("world-meshing").ChunkStorage; const NeighborChunks = @import("world-meshing").NeighborChunks; const TextureAtlas = @import("engine-assets").TextureAtlas; const ao_calculator = @import("world-meshing").meshing.ao_calculator; @@ -23,29 +22,6 @@ const boundary = @import("world-meshing").meshing.boundary; pub const std_options: std.Options = .{ .log_level = .err }; -test "ChunkStorage terrain handoff remains ready while an existing allocation is remeshed" { - var storage = ChunkStorage.init(testing.allocator); - defer storage.deinitWithoutRHI(); - const data = try storage.getOrCreate(-2, 3); - - try testing.expect(!ChunkStorage.isChunkTerrainReadyForHandoff(-2, 3, &storage)); - - data.render.mesh.solid_allocation = .{ .offset = 0, .count = 12, .handle = 1 }; - try testing.expect(ChunkStorage.isChunkTerrainReadyForHandoff(-2, 3, &storage)); - - data.render.mesh.solid_allocation = null; - data.render.mesh.cutout_allocation = .{ .offset = 12, .count = 6, .handle = 1 }; - try testing.expect(ChunkStorage.isChunkTerrainReadyForHandoff(-2, 3, &storage)); - - data.render.mesh.cutout_allocation = null; - data.render.mesh.fluid_allocation = .{ .offset = 18, .count = 6, .handle = 1 }; - try testing.expect(!ChunkStorage.isChunkTerrainReadyForHandoff(-2, 3, &storage)); - - data.chunk.state = .renderable; - data.render.mesh.ready = true; - try testing.expect(ChunkStorage.isChunkTerrainReadyForHandoff(-2, 3, &storage)); -} - test "PackedLight init and accessors" { const light = PackedLight.init(15, 10); try testing.expectEqual(@as(u4, 15), light.getSkyLight()); diff --git a/src/worldgen_climate_snapshot_main.zig b/src/worldgen_climate_snapshot_main.zig index 6920948d..8e9c5c64 100644 --- a/src/worldgen_climate_snapshot_main.zig +++ b/src/worldgen_climate_snapshot_main.zig @@ -64,7 +64,7 @@ fn writeUsage(init: std.process.Init) !void { \\ --width Sample width (default: 128) \\ --depth Sample depth (default: 128) \\ --step World-space distance between samples (default: 4) - \\ --reduction LOD noise reduction level (default: 0) + \\ --reduction Noise octave reduction level (default: 0) \\ --format Output format (default: json) \\ --field PPM field: temperature, humidity, continentalness, erosion, ruggedness, river_mask, ridge_mask, cave_region, elevation, height \\ --output Output file or stdout (default: zig-out/worldgen-climate-snapshot.json) diff --git a/src/worldgen_tests.zig b/src/worldgen_tests.zig index a7fa2b3f..c4975731 100644 --- a/src/worldgen_tests.zig +++ b/src/worldgen_tests.zig @@ -1038,33 +1038,6 @@ test "BiomeSource selectBiome river override" { try testing.expectEqual(result, BiomeId.river); } -test "BiomeSource selectBiomeSimplified returns valid biome" { - const biome_mod = world_worldgen.biome; - const source = BiomeSource.init(); - - const climate1 = biome_mod.ClimateParams{ - .temperature = 0.9, - .humidity = 0.1, - .elevation = 0.4, - .continentalness = 0.6, - .ruggedness = 0.2, - }; - - const result1 = source.selectBiomeSimplified(climate1); - try testing.expectEqual(result1, BiomeId.desert); - - const climate2 = biome_mod.ClimateParams{ - .temperature = 0.5, - .humidity = 0.5, - .elevation = 0.2, - .continentalness = 0.1, - .ruggedness = 0.2, - }; - - const result2 = source.selectBiomeSimplified(climate2); - try testing.expectEqual(BiomeId.deep_ocean, result2); -} - test "BiomeSource getColor returns valid packed RGB" { const source = BiomeSource.init(); From 40e1b9fb179f2c689d663d6824dfe6c4aa4d4c48 Mon Sep 17 00:00:00 2001 From: MichaelFisher1997 Date: Mon, 7 Sep 2026 21:19:44 +0100 Subject: [PATCH 2/2] fix: report benchmark SLO violations Signed-off-by: MichaelFisher1997 --- modules/game-core/src/benchmark.zig | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/modules/game-core/src/benchmark.zig b/modules/game-core/src/benchmark.zig index 13eef993..1252f0a1 100644 --- a/modules/game-core/src/benchmark.zig +++ b/modules/game-core/src/benchmark.zig @@ -339,7 +339,27 @@ pub fn validateResults(results: BenchmarkResults) !void { } if (completion.evidence_mode and !validProvenance(results.provenance)) return error.MissingBenchmarkProvenance; const thresholds = thresholdsForResults(results); - if (results.fps.p1 < thresholds.fps_p1_min or results.max_frame_ms > thresholds.max_frame_ms or results.draw_calls_avg > thresholds.draw_calls_max or results.vertices_avg > thresholds.vertices_max or results.gpu_memory_mb_max > thresholds.gpu_memory_mb_max) return error.BenchmarkSloBreach; + const breached = results.fps.p1 < thresholds.fps_p1_min or + results.max_frame_ms > thresholds.max_frame_ms or + results.draw_calls_avg > thresholds.draw_calls_max or + results.vertices_avg > thresholds.vertices_max or + results.gpu_memory_mb_max > thresholds.gpu_memory_mb_max; + if (breached) { + std.log.err("benchmark SLO breach for {s}: p1 FPS {d:.2}/{d:.2}, max frame {d:.2}/{d:.2}ms, draw calls {d:.2}/{d:.2}, vertices {d:.2}/{d:.2}, GPU memory {d:.2}/{d:.2}MiB", .{ + results.preset, + results.fps.p1, + thresholds.fps_p1_min, + results.max_frame_ms, + thresholds.max_frame_ms, + results.draw_calls_avg, + thresholds.draw_calls_max, + results.vertices_avg, + thresholds.vertices_max, + results.gpu_memory_mb_max, + thresholds.gpu_memory_mb_max, + }); + return error.BenchmarkSloBreach; + } } fn baselineRenderDistanceForPreset(preset: []const u8) i32 {