diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 35e5fccd..08402a65 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -13,7 +13,12 @@ on: duration: description: Benchmark duration per preset (seconds) required: false - default: "5" + 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 }} @@ -35,7 +40,8 @@ jobs: issues: write statuses: write runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 30 + # Long scheduled/manual acceptance captures remain externally bounded. + timeout-minutes: 60 steps: - uses: actions/checkout@v4 @@ -63,7 +69,7 @@ jobs: }); const shouldRun = files.some((file) => { const name = file.filename; - return name === 'build.zig' || name.startsWith('src/') || name.startsWith('modules/') || name.startsWith('assets/shaders/'); + return name === 'build.zig' || name.startsWith('src/') || name.startsWith('modules/') || name.startsWith('assets/shaders/') || name.startsWith('scripts/') || name.startsWith('docs/benchmarks/') || name === '.github/workflows/benchmark.yml'; }); core.setOutput('run', shouldRun ? 'true' : 'false'); @@ -96,28 +102,96 @@ jobs: uses: ./.github/actions/run-with-log with: name: Benchmark - timeout: 20m + timeout: ${{ github.event_name == 'schedule' && '55m' || github.event_name == 'workflow_dispatch' && '55m' || '20m' }} log-file: benchmark.log - command: mkdir -p "$MESA_SHADER_CACHE_DIR" && nix develop .#ci-graphics --command bash scripts/run_benchmark.sh --duration "$BENCHMARK_DURATION" --presets low,medium,high --output-dir benchmark-results --per-preset-timeout 600 + command: mkdir -p "$MESA_SHADER_CACHE_DIR" && nix develop .#ci-graphics --command 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 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" - BENCHMARK_DURATION: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.duration || '5' }} + # 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 Nix environment + ZIGCRAFT_BENCHMARK_RUNNER: blacksmith-2vcpu-ubuntu-2404 GitHub Actions runner + ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN: Zig 0.16.0 supplied by the pinned Nix flake + + - 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: nix develop .#ci-unit --command 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 + 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" && nix develop .#ci-graphics --command 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 Nix environment + ZIGCRAFT_BENCHMARK_RUNNER: blacksmith-2vcpu-ubuntu-2404 GitHub Actions runner + ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN: Zig 0.16.0 supplied by the pinned Nix flake + + - 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 - name: Stop headless Wayland compositor if: always() && steps.gate.outputs.run == 'true' uses: ./.github/actions/stop-weston - - name: Compare against baseline + - name: Reject Vulkan validation errors in benchmark log if: steps.gate.outputs.run == 'true' + run: | + if rg -n -i 'vuid-|validation.*(error|failed)|(error|failed).*validation' benchmark.log; then + printf 'Vulkan validation errors were reported during the benchmark.\n' >&2 + 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: | - for preset in low medium high; do - bash scripts/compare_benchmarks.sh docs/benchmarks/baseline.json "benchmark-results/${preset}.json" --preset "$preset" - done + 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 != '' @@ -128,7 +202,7 @@ jobs: env: BENCHER_BRANCH: ${{ github.head_ref || github.ref_name }} run: | - for preset in low medium high; do + for preset in low medium high; do for scenario in stationary traversal rapid-turn teleport-eviction; do bencher run \ --project "$BENCHER_PROJECT" \ --branch "$BENCHER_BRANCH" \ @@ -136,8 +210,8 @@ jobs: --token "$BENCHER_API_TOKEN" \ --adapter json \ --err \ - "cat benchmark-results/${preset}.json" - done + "cat benchmark-results/auto/${preset}/${scenario}.json" + done; done - name: Comment Bencher dashboard if: steps.gate.outputs.run == 'true' && github.event_name == 'pull_request' && env.BENCHER_DASHBOARD_URL != '' @@ -158,7 +232,10 @@ 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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 86c232cb..29930567 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,7 @@ on: - "libs/**" - "assets/shaders/**" - "scripts/**" + - "docs/benchmarks/**" - "build.zig" - "build.zig.zon" - "flake.nix" @@ -18,6 +19,7 @@ on: - ".github/actions/setup-lavapipe/**" - ".github/vulkan/**" - ".github/workflows/build.yml" + - ".github/workflows/benchmark.yml" pull_request: branches: [dev] paths: @@ -26,6 +28,7 @@ on: - "libs/**" - "assets/shaders/**" - "scripts/**" + - "docs/benchmarks/**" - "build.zig" - "build.zig.zon" - "flake.nix" @@ -35,6 +38,7 @@ on: - ".github/actions/setup-lavapipe/**" - ".github/vulkan/**" - ".github/workflows/build.yml" + - ".github/workflows/benchmark.yml" workflow_dispatch: inputs: ref: @@ -81,6 +85,7 @@ jobs: - 'libs/**' - 'assets/shaders/**' - 'scripts/**' + - 'docs/benchmarks/**' - 'build.zig' - 'build.zig.zon' - 'flake.nix' @@ -90,6 +95,7 @@ jobs: - '.github/actions/setup-lavapipe/**' - '.github/vulkan/**' - '.github/workflows/build.yml' + - '.github/workflows/benchmark.yml' platform_changes: - 'src/**' - 'modules/**' @@ -191,6 +197,14 @@ jobs: log-file: unit-test-${{ matrix.optimize }}.log command: nix develop .#ci-unit --command zig build -Doptimize=${{ matrix.optimize }} test + - name: Run Phase 5 compact LOD gate + if: needs.changes.outputs.code_changes == 'true' && matrix.optimize == 'Debug' + run: nix develop .#ci-unit --command zig build phase5-gate + + - name: Run bounded Phase 5 streaming stress gate + if: needs.changes.outputs.code_changes == 'true' && matrix.optimize == 'Debug' + run: nix develop .#ci-unit --command zig build phase5-stress-gate -Dphase5-stress-iterations=64 + - name: Upload unit test log if: failure() uses: actions/upload-artifact@v7 @@ -221,7 +235,7 @@ jobs: contents: read id-token: write runs-on: blacksmith-2vcpu-ubuntu-2404 - timeout-minutes: 45 + timeout-minutes: 75 needs: [changes, unit-test] steps: - uses: actions/checkout@v4 @@ -274,15 +288,38 @@ 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: nix develop .#ci-graphics --command 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 grep -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 + 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 exit 1 fi - echo "No Vulkan validation errors found in integration logs." + echo "No Vulkan validation errors found in integration or Phase 5 visual logs." - name: Stop headless Wayland compositor if: always() && needs.changes.outputs.code_changes == 'true' @@ -296,6 +333,7 @@ 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/.github/workflows/opencode-pr.yml b/.github/workflows/opencode-pr.yml index f0df7790..d296b371 100644 --- a/.github/workflows/opencode-pr.yml +++ b/.github/workflows/opencode-pr.yml @@ -82,6 +82,7 @@ jobs: ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} with: model: zhipuai-coding-plan/glm-5.2 + variant: max use_github_token: true prompt: ${{ env.PROMPT }} diff --git a/.gitignore b/.gitignore index d98dbfe2..c3f5dccd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ blocks-temp/ libs/zig-image/ result node_modules/ +__pycache__/ .direnv/ *-profile* test_output.txt diff --git a/AGENTS.md b/AGENTS.md index 9b40aca1..e8a61990 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,207 +1,71 @@ -# ZigCraft Agent Guidelines - -Essential instructions for agentic coding agents operating in this Zig voxel engine repository. - ---- - -## Development Environment - -Uses Nix for dependency management. **All build/test commands MUST be wrapped in `nix develop --command`**. - -### Build & Run -```bash -nix develop --command zig build # Debug build -nix develop --command zig build run # Run application -nix develop --command zig build -Doptimize=ReleaseFast # Release build -rm -rf zig-out/ .zig-cache/ # Clean artifacts -``` - -### Testing -```bash -nix develop --command zig build test # Unit tests + shader validation -nix develop --command zig build test -- --test-filter "Vec3 addition" # Single test -nix develop --command zig build test-integration # Window init smoke test -nix develop --command zig build test-robustness # GPU robustness test -``` - -### Headless Runtime Verification -Use project skills for bounded background runs: -- `headless-crash-test` for crash, hang, startup, and world-load checks -- `headless-screenshot` for offscreen screenshots and visual evidence -- `headless-benchmark` for JSON benchmark runs with full offscreen graphics rendering -- `headless-graphics-verification` for graphics/RHI/shader verification workflows - -When launching the game from an agent, prefer `-Dskip-present` unless the user explicitly requests a visible window. Always set a tool timeout for runtime, screenshot, and benchmark commands so the agent never gets stuck on a hung game process. - -### Formatting & Git Hooks -```bash -nix develop --command zig fmt src/ # Format code -./scripts/setup-hooks.sh # Enable pre-push checks -``` - -### Debug Build Options -```bash --Ddebug_shadows # Enable shadow debug visualization --Dsmoke-test # Auto-load world and exit (for automated testing) --Dskip-present # Full offscreen graphics mode (render without showing/presenting a window) --Dbenchmark-preset=low # Graphics preset for benchmark runs (low/medium/high/ultra/extreme) --Dauto-world=normal # Auto-open a generator directly (normal/overworld or flat) --Dmonitor-index=1 # Open the game window on a specific 0-based SDL display index --Dmonitor-name=DP-2 # Move the game window to a named Hyprland monitor --Dwindow-video-driver=x11 # Force SDL's video backend (useful when Wayland ignores window placement) --Dwindow-no-focus # Create the game window without taking keyboard focus --Dstartup-diagnostic-seconds=5 # Wait N seconds, log chunk/LOD counts, and exit --Dchunk-debug-mode # Disable LOD, water, caves, and decorations for isolation --Dchunk-debug-enable=lod,caves # Re-enable individual systems in chunk-debug-mode -``` - -`-Dchunk-debug-enable=` accepts a comma-separated list of: -- `lod` -- `water` -- `watergen` -- `waterrender` -- `caves` -- `decorations` - ---- - -## Git Workflow - -- **Default branch**: `dev` -- **All PRs target `dev`** (not `main`) -- **Never push directly to `dev`** unless the user explicitly instructs you to do so -- Code changes should land through a pull request targeting `dev` so they receive PR code review -- Branch naming: `feature/*`, `bug/*`, `hotfix/*`, `ci/*` -- Conventional commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:` - ---- - -## Project Structure - -``` -modules/ - engine-core/ # Window, time, logging, job system, shared interfaces - engine-graphics/ # Render system, Vulkan backend, shaders, textures, camera, shadows - engine-rhi/ # RHI contracts, resource manager, render settings, culling interfaces - engine-math/ # Vec3, Mat4, AABB, Frustum, ray/voxel helpers - engine-input/ # Input handling - engine-ui/ # Immediate-mode UI, fonts, widgets, overlays - world-core/ # Blocks, chunk data, coordinates, light packing - world-worldgen/ # Terrain generation, biomes, caves, decorations, generator registry - world-meshing/ # Chunk storage, chunk mesh generation, GPU block buffers, meshing helpers - 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/ - game/ # Application logic, state, menus - c.zig # Central C imports (@cImport for SDL3, Vulkan) - main.zig # Entry point - tests.zig # Unit test suite aggregator -libs/ # Local dependencies (zig-math, zig-noise) -assets/shaders/ # GLSL shaders (vulkan/ contains SPIR-V) -``` - ---- - -## Code Style - -### Naming Conventions -- **Types/Structs/Enums**: `PascalCase` (`RenderSystem`, `BufferHandle`, `BlockType`) -- **Functions**: `camelCase` following Zig stdlib convention (`initRenderer`, `meshQueue`, `chunkX`) -- **Variables**: `snake_case` (`mesh_queue`, `chunk_x`) -- **Constants/Globals**: `SCREAMING_SNAKE_CASE` (`MAX_CHUNKS`, `CHUNK_SIZE_X`) -- **Files**: `snake_case.zig` - -### Import Order -```zig -// 1. Standard library -const std = @import("std"); -const Allocator = std.mem.Allocator; - -// 2. C imports (always via c.zig) -const c = @import("../c.zig").c; - -// 3. Project modules by package name -const Vec3 = @import("engine-math").Vec3; -const log = @import("engine-core").log; -``` - -### Memory Management -- Functions allocating heap memory MUST accept `std.mem.Allocator` -- Use `defer`/`errdefer` for cleanup immediately after allocation -- Prefer `std.ArrayListUnmanaged` in structs that store the allocator elsewhere -- Use `extern struct` for GPU-shared data layouts (e.g., `Vertex`) - -### Error Handling -- Propagate errors with `try`; define subsystem-specific error sets (`RhiError`) -- Log errors via `@import("engine-core").log`: `log.log.err("msg: {}", .{err})` -- Use `//!` for module-level docs, `///` for public API documentation - -### Type Patterns -- GPU resource handles are opaque `u32` (`BufferHandle`, `TextureHandle`, `ShaderHandle`) -- Invalid handles are `0` (`InvalidBufferHandle`, etc.) -- Packed data uses `packed struct` (e.g., `PackedLight` for sky/block light) -- Chunk coordinates: `i32`; local block coordinates: `u32` (0-15 for X/Z, 0-255 for Y) - ---- - -## Coordinate Systems - -- **World**: Global (x, y, z) in blocks/meters -- **Chunk**: `(chunk_x, chunk_z)` via `@divFloor(world, 16)` -- **Local**: (x, y, z) within a chunk -- Use `worldToChunk()` and `worldToLocal()` from `@import("world-core")` - ---- - -## Architecture - -### Render Hardware Interface (RHI) -- All rendering uses the `RHI` interface exported by `engine-rhi` -- Vulkan is the only backend (`rhi_vulkan.zig`) -- Extend functionality by updating `RHI.VTable` and backend implementation - -### Job System & Concurrency -- Use `JobSystem` for heavy tasks (world gen, meshing, lighting) -- **Never call RHI or windowing from worker threads** -- Synchronize shared state with `std.Thread.Mutex` -- Use `chunk.pin()`/`chunk.unpin()` when passing chunks to background jobs - ---- - -## Common Tasks - -### Adding a New Block Type -1. Add entry to `BlockType` enum in `modules/world-core/src/block.zig` -2. Register properties in `modules/world-core/src/block_registry.zig` -3. Add textures to `modules/engine-graphics/src/texture_atlas.zig` -4. Standardize PBR textures using `./scripts/process_textures.sh` -5. Update `modules/world-meshing/src/chunk_mesh.zig` for special face/transparency logic - -### Modifying Shaders -1. GLSL sources in `assets/shaders/vulkan/` -2. Vulkan SPIR-V validated during `zig build test` via `glslangValidator` -3. Uniform names must match exactly between shader source and RHI backends - -### Adding Unit Tests -- Add tests alongside modules and expose them from the owning module root; aggregate broad suites from `src/tests.zig` -- Use `std.testing` assertions: `expectEqual`, `expectApproxEqAbs`, `expect` -- Test naming: descriptive, e.g., `test "Vec3 normalize"` - ---- - -## Verification Checklist - -Before committing: -- [ ] Run `zig fmt src/` to format code -- [ ] Run `zig build test` (includes shader validation) -- [ ] Run `zig build -Doptimize=ReleaseFast` for performance-critical changes -- [ ] Run `./scripts/process_textures.sh` for any new texture assets - ---- - -## Performance Notes - -- Chunk mesh building runs on worker threads; avoid allocations in hot paths -- Use packed structs for large arrays (e.g., light data) -- Profile before optimizing; use `ReleaseFast` for benchmarks +# ZigCraft Agent Guide + +## Toolchain and commands + +- Use Zig 0.16.0 through Nix. Wrap every Zig build/test/format command in `nix develop --command`; CI uses the narrower `.#ci-unit` and `.#ci-graphics` shells. +- Build/run: + ```bash + nix develop --command zig build + nix develop --command zig build run + nix develop --command zig build -Doptimize=ReleaseFast + ``` +- Format all Zig code, not only `src/` (the local hook is less strict than CI): + ```bash + nix develop --command zig fmt src/ modules/ + nix develop --command 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. + ```bash + nix develop --command zig build test + nix develop --command zig build test -Dtest-filter="name" + # Equivalent runtime-filter form: + nix develop --command zig build test -- --test-filter "name" + ``` +- Graphics/runtime verification is separate: + ```bash + nix develop --command zig build test-integration -Dskip-present=true + nix develop --command zig build test-robustness + nix develop --command zig build phase5-gate + nix develop --command 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=`. + +## Benchmarks and generated artifacts + +- The benchmark harness is a separate build step; do not pass benchmark presets to ordinary `run` and assume benchmarking is active: + ```bash + nix develop --command zig build benchmark -Doptimize=ReleaseFast \ + -Dbenchmark-preset=low -Dbenchmark-scenario=traversal \ + -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: `nix develop --command zig build worldgen-report` and `nix develop --command zig build lod-bench`. Pass climate snapshot arguments after `--`, e.g. `nix develop --command 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. + +## Architecture boundaries + +- `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. +- 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`. +- 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 + +- Branch from and target PRs to `dev`; do not push directly to `dev` unless explicitly requested. Branch prefixes are `feature/`, `bug/`, `hotfix/`, and `ci/`. +- PR titles and commits use conventional types: `feat`, `fix`, `refactor`, `test`, `docs`, `ci`, `chore`, `perf`, `build`, `style`, or `revert`. +- Every non-merge PR commit needs a DCO trailer; create commits with `git commit -s`. diff --git a/assets/shaders/vulkan/lod_compact_terrain.frag b/assets/shaders/vulkan/lod_compact_terrain.frag new file mode 100644 index 00000000..4e8fe289 --- /dev/null +++ b/assets/shaders/vulkan/lod_compact_terrain.frag @@ -0,0 +1,47 @@ +#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; + +void main() { + if (vMaskRadius >= 1.0 && length(vFragPosWorld.xz) < vMaskRadius) 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 fog = clamp(1.0 - exp(-vDistance * global.params.y), 0.0, 1.0); + fog = max(fog, smoothstep(300.0, 1200.0, vDistance) * 0.62); + color = mix(color, global.fog_color.rgb, fog); + } + outColor = vec4(color, 1.0); +} diff --git a/assets/shaders/vulkan/lod_compact_terrain.vert b/assets/shaders/vulkan/lod_compact_terrain.vert new file mode 100644 index 00000000..14feb4cb --- /dev/null +++ b/assets/shaders/vulkan/lod_compact_terrain.vert @@ -0,0 +1,221 @@ +#version 450 + +layout(location = 0) out vec3 vColor; +layout(location = 1) flat out vec3 vNormal; +layout(location = 2) out vec2 vTexCoord; +layout(location = 3) flat out int vTileID; +layout(location = 4) out float vDistance; +layout(location = 5) out float vSkyLight; +layout(location = 6) out vec3 vBlockLight; +layout(location = 7) out vec3 vFragPosWorld; +layout(location = 8) out float vViewDepth; +layout(location = 9) out vec3 vTangent; +layout(location = 10) out vec3 vBitangent; +layout(location = 11) out float vAO; +layout(location = 12) out vec4 vClipPosCurrent; +layout(location = 13) out vec4 vClipPosPrev; +layout(location = 14) out float vMaskRadius; +layout(location = 15) out float vCloud; +layout(location = 16) out float vLODFade; + +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; + +// One sample is the little-endian 128-bit CompactLODSample wire ABI. The +// uvec4 lanes are bits [0..31], [32..63], [64..95], and [96..127]. +layout(set = 0, binding = 16) readonly buffer Samples { + uvec4 sample_words[]; +} samples; +struct CompactInstance { mat4 model; vec4 params; uvec4 words; }; +layout(set = 0, binding = 17) readonly buffer CompactInstances { + CompactInstance items[]; +} compact_instances; + +// Keep this byte-for-byte identical to rhi.CompactLODDraw. The explicit tail +// carries edge validity in the std430 tail word at offset 92. +layout(push_constant) uniform CompactDraw { + mat4 model; + float mask_radius; + float lod_fade; + uint sample_offset; + uint width; + float cell_size; + uint layer; + float skirt_depth; + uint edge_masks; +} draw_data; + +// `layer == 2` is the indirect sentinel. Direct draws retain their existing +// push-constant ABI; indirect indexed draws address this SSBO via firstInstance. +bool indirectMode() { return draw_data.layer == 2u; } +mat4 tileModel() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].model : draw_data.model; } +vec4 tileParams() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].params : vec4(draw_data.mask_radius, draw_data.lod_fade, draw_data.cell_size, draw_data.skirt_depth); } +uvec4 tileWords() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].words : uvec4(draw_data.sample_offset, draw_data.width, draw_data.layer, draw_data.edge_masks); } + +int decodeSigned16(uint word, uint shift) { + uint raw = (word >> 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 new file mode 100644 index 00000000..3e547bea --- /dev/null +++ b/assets/shaders/vulkan/lod_compact_water.frag @@ -0,0 +1,58 @@ +#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; + +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() { + if (vMaskRadius >= 1.0 && length(vFragPosWorld.xz) < vMaskRadius) discard; + // 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 fog = clamp(1.0 - exp(-vDistance * global.params.y), 0.0, 1.0); + fog = max(fog, smoothstep(280.0, 1100.0, vDistance) * 0.62); + base = mix(base, global.fog_color.rgb, fog); + } + + outColor = vec4(base, 0.78 * clamp(vLODFade, 0.0, 1.0)); +} diff --git a/assets/shaders/vulkan/lod_compact_water.vert b/assets/shaders/vulkan/lod_compact_water.vert new file mode 100644 index 00000000..ad8e9b37 --- /dev/null +++ b/assets/shaders/vulkan/lod_compact_water.vert @@ -0,0 +1,158 @@ +#version 450 + +layout(location = 0) out vec3 vColor; +layout(location = 1) flat out vec3 vNormal; +layout(location = 2) out vec2 vTexCoord; +layout(location = 3) flat out int vTileID; +layout(location = 4) out float vDistance; +layout(location = 5) out float vSkyLight; +layout(location = 6) out vec3 vBlockLight; +layout(location = 7) out vec3 vFragPosWorld; +layout(location = 8) out float vViewDepth; +layout(location = 9) out vec4 vClipPos; +layout(location = 10) out float vMaskRadius; +layout(location = 11) out float vLODFade; + +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; + +layout(set = 0, binding = 16) readonly buffer Samples { + uvec4 sample_words[]; +} samples; +struct CompactInstance { mat4 model; vec4 params; uvec4 words; }; +layout(set = 0, binding = 17) readonly buffer CompactInstances { + CompactInstance items[]; +} compact_instances; + +// Must remain byte-for-byte identical to rhi.CompactLODDraw. +layout(push_constant) uniform CompactDraw { + mat4 model; + float mask_radius; + float lod_fade; + uint sample_offset; + uint width; + float cell_size; + uint layer; + float skirt_depth; + uint edge_masks; +} draw_data; + +bool indirectMode() { return draw_data.layer == 2u; } +mat4 tileModel() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].model : draw_data.model; } +vec4 tileParams() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].params : vec4(draw_data.mask_radius, draw_data.lod_fade, draw_data.cell_size, draw_data.skirt_depth); } +uvec4 tileWords() { return indirectMode() ? compact_instances.items[gl_InstanceIndex].words : uvec4(draw_data.sample_offset, draw_data.width, draw_data.layer, draw_data.edge_masks); } + +int decodeSigned16(uint word, uint shift) { + uint raw = (word >> 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 new file mode 100644 index 00000000..7b6705e1 --- /dev/null +++ b/assets/shaders/vulkan/lod_culling.comp @@ -0,0 +1,90 @@ +#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/build.zig b/build.zig index a46b485e..0c49740a 100644 --- a/build.zig +++ b/build.zig @@ -19,9 +19,16 @@ const BuildOptions = struct { window_no_focus: bool, shadow_test_variant: []const u8, benchmark_preset: []const u8, + benchmark_scenario: []const u8, benchmark_duration: u32, benchmark_output: []const u8, benchmark_render_distance: i32, + benchmark_horizon_distance: i32, + benchmark_lod_memory_budget_mb: u32, + benchmark_require_gpu_candidates: u32, + benchmark_gpu_culling: bool, + benchmark_world: []const u8, + benchmark_fixture: []const u8, sanitize_c: ?std.zig.SanitizeC, }; @@ -66,7 +73,7 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const opts = defineBuildOptions(b); + const opts = defineBuildOptions(b, optimize); const modules = defineModules(b, target, optimize, opts); defineBuildSteps(b, target, optimize, opts, modules); } @@ -425,8 +432,11 @@ fn defineBuildSteps( const window_video_driver = opts.window_video_driver; const window_no_focus = opts.window_no_focus; const benchmark_preset = opts.benchmark_preset; + const benchmark_scenario = opts.benchmark_scenario; const benchmark_duration = opts.benchmark_duration; const benchmark_output = opts.benchmark_output; + const benchmark_world = opts.benchmark_world; + const benchmark_fixture = opts.benchmark_fixture; const sanitize_c = opts.sanitize_c; const zig_math = modules.zig_math; const zig_noise = modules.zig_noise; @@ -506,8 +516,8 @@ fn defineBuildSteps( benchmark_options.addOption(bool, "smoke_test", false); benchmark_options.addOption(bool, "chunk_debug_mode", false); benchmark_options.addOption([]const u8, "chunk_debug_enable", ""); - benchmark_options.addOption([]const u8, "auto_world", ""); - benchmark_options.addOption([]const u8, "auto_preset", ""); + benchmark_options.addOption([]const u8, "auto_world", benchmark_world); + benchmark_options.addOption([]const u8, "auto_preset", benchmark_preset); benchmark_options.addOption(u32, "startup_diagnostic_seconds", 0); benchmark_options.addOption(i32, "monitor_index", monitor_index); benchmark_options.addOption([]const u8, "monitor_name", monitor_name); @@ -517,13 +527,22 @@ fn defineBuildSteps( benchmark_options.addOption([]const u8, "screenshot_path", ""); benchmark_options.addOption(u32, "screenshot_frame", 120); benchmark_options.addOption(u32, "screenshot_delay_seconds", 0); + benchmark_options.addOption([]const u8, "phase5_visual_scene", benchmark_fixture); + benchmark_options.addOption([]const u8, "phase5_visual_run_id", ""); benchmark_options.addOption(bool, "shadow_test_scene", false); benchmark_options.addOption([]const u8, "shadow_test_variant", "dug-cave"); benchmark_options.addOption(bool, "benchmark", true); benchmark_options.addOption([]const u8, "benchmark_preset", benchmark_preset); + benchmark_options.addOption([]const u8, "benchmark_scenario", benchmark_scenario); benchmark_options.addOption(u32, "benchmark_duration", benchmark_duration); benchmark_options.addOption([]const u8, "benchmark_output", benchmark_output); benchmark_options.addOption(i32, "benchmark_render_distance", opts.benchmark_render_distance); + benchmark_options.addOption(i32, "benchmark_horizon_distance", opts.benchmark_horizon_distance); + benchmark_options.addOption(u32, "benchmark_lod_memory_budget_mb", opts.benchmark_lod_memory_budget_mb); + benchmark_options.addOption(u32, "benchmark_require_gpu_candidates", opts.benchmark_require_gpu_candidates); + benchmark_options.addOption([]const u8, "benchmark_world", benchmark_world); + benchmark_options.addOption([]const u8, "benchmark_fixture", benchmark_fixture); + benchmark_options.addOption([]const u8, "benchmark_build_mode", @tagName(optimize)); const benchmark_root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), @@ -568,6 +587,18 @@ fn defineBuildSteps( benchmark_run_cmd.step.dependOn(b.getInstallStep()); benchmark_run_cmd.step.dependOn(&shader_cmd.step); benchmark_run_cmd.setCwd(b.path(".")); + benchmark_run_cmd.setEnvironmentVariable("ZIGCRAFT_LOD_PROFILE", "1"); + // Benchmark presets must scale large persistent world buffers coherently, + // not only shader quality. This keeps their documented VRAM SLO meaningful + // on high-VRAM devices where runtime defaults otherwise reserve maximum + // pools regardless of the selected preset. + if (std.mem.eql(u8, benchmark_preset, "low")) { + benchmark_run_cmd.setEnvironmentVariable("ZIGCRAFT_VERTEX_CAPACITY_MB", "96"); + benchmark_run_cmd.setEnvironmentVariable("ZIGCRAFT_GPU_BLOCK_BUDGET_MB", "96"); + } else if (std.mem.eql(u8, benchmark_preset, "medium")) { + benchmark_run_cmd.setEnvironmentVariable("ZIGCRAFT_VERTEX_CAPACITY_MB", "96"); + benchmark_run_cmd.setEnvironmentVariable("ZIGCRAFT_GPU_BLOCK_BUDGET_MB", "96"); + } const benchmark_step = b.step("benchmark", "Run benchmark harness"); benchmark_step.dependOn(&benchmark_run_cmd.step); @@ -677,6 +708,132 @@ fn defineBuildSteps( run_exe_tests.step.dependOn(&shader_cmd.step); test_step.dependOn(&run_exe_tests.step); + // world-lod gates its dedicated test modules on builtin.is_test. Importing + // the production module into src/tests.zig leaves that module compiled with + // is_test=false, so run it as a test root as well. + const world_lod_test_root = b.createModule(.{ + .root_source_file = b.path("modules/world-lod/src/tests.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = sanitize_c, + }); + addSharedImports(world_lod_test_root, modules.zig_math, modules.zig_noise, modules.fs_module, modules.sync_module, modules.c_module, options); + world_lod_test_root.addImport("engine-core", modules.engine_core); + world_lod_test_root.addImport("engine-assets", modules.engine_assets); + world_lod_test_root.addImport("engine-graphics", modules.engine_graphics); + world_lod_test_root.addImport("engine-math", modules.engine_math); + world_lod_test_root.addImport("engine-rhi", modules.engine_rhi); + world_lod_test_root.addImport("world-meshing", modules.world_meshing); + world_lod_test_root.addImport("world-core", modules.world_core); + world_lod_test_root.addImport("world-persistence", modules.world_persistence); + world_lod_test_root.addImport("world-worldgen", modules.world_worldgen); + world_lod_test_root.addOptions("world_lod_options", opts.world_lod_options); + + const world_lod_tests = b.addTest(.{ + .root_module = world_lod_test_root, + .filters = test_filters, + }); + const run_world_lod_tests = b.addRunArtifact(world_lod_tests); + run_world_lod_tests.setEnvironmentVariable("ZIGCRAFT_LOG_LEVEL", "fatal"); + test_step.dependOn(&run_world_lod_tests.step); + + const benchmark_phase5_gate_root = b.createModule(.{ + .root_source_file = b.path("phase5_benchmark_gate_tests.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = sanitize_c, + }); + benchmark_phase5_gate_root.addImport("game-core", game_core); + benchmark_phase5_gate_root.addImport("engine-rhi", modules.engine_rhi); + const benchmark_phase5_gate_tests = b.addTest(.{ + .root_module = benchmark_phase5_gate_root, + .filters = test_filters, + }); + const run_benchmark_phase5_gate_tests = b.addRunArtifact(benchmark_phase5_gate_tests); + run_benchmark_phase5_gate_tests.setEnvironmentVariable("ZIGCRAFT_LOG_LEVEL", "fatal"); + test_step.dependOn(&run_benchmark_phase5_gate_tests.step); + + const phase5_lod_gate_root = b.createModule(.{ + .root_source_file = b.path("modules/world-lod/src/tests.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = sanitize_c, + }); + addSharedImports(phase5_lod_gate_root, modules.zig_math, modules.zig_noise, modules.fs_module, modules.sync_module, modules.c_module, options); + phase5_lod_gate_root.addImport("engine-core", modules.engine_core); + phase5_lod_gate_root.addImport("engine-assets", modules.engine_assets); + phase5_lod_gate_root.addImport("engine-graphics", modules.engine_graphics); + phase5_lod_gate_root.addImport("engine-math", modules.engine_math); + phase5_lod_gate_root.addImport("engine-rhi", modules.engine_rhi); + phase5_lod_gate_root.addImport("world-meshing", modules.world_meshing); + phase5_lod_gate_root.addImport("world-core", modules.world_core); + phase5_lod_gate_root.addImport("world-persistence", modules.world_persistence); + phase5_lod_gate_root.addImport("world-worldgen", modules.world_worldgen); + phase5_lod_gate_root.addOptions("world_lod_options", opts.world_lod_options); + const phase5_lod_gate_tests = b.addTest(.{ + .root_module = phase5_lod_gate_root, + .filters = &.{"LODManager preserves the CPU heightfield fallback for far LODs"}, + }); + const run_phase5_lod_gate_tests = b.addRunArtifact(phase5_lod_gate_tests); + run_phase5_lod_gate_tests.setEnvironmentVariable("ZIGCRAFT_LOG_LEVEL", "fatal"); + + // This is deliberately operation-count based rather than wall-clock based: + // CI machines vary widely, while the stress sequence remains reproducible. + const phase5_stress_iterations = b.option(u32, "phase5-stress-iterations", "Deterministic Phase 5 streaming stress cycles") orelse 64; + const phase5_stress_root = b.createModule(.{ + .root_source_file = b.path("modules/world-lod/src/tests.zig"), + .target = target, + .optimize = optimize, + .sanitize_c = sanitize_c, + }); + addSharedImports(phase5_stress_root, modules.zig_math, modules.zig_noise, modules.fs_module, modules.sync_module, modules.c_module, options); + phase5_stress_root.addImport("engine-core", modules.engine_core); + phase5_stress_root.addImport("engine-assets", modules.engine_assets); + phase5_stress_root.addImport("engine-graphics", modules.engine_graphics); + phase5_stress_root.addImport("engine-math", modules.engine_math); + phase5_stress_root.addImport("engine-rhi", modules.engine_rhi); + phase5_stress_root.addImport("world-meshing", modules.world_meshing); + phase5_stress_root.addImport("world-core", modules.world_core); + phase5_stress_root.addImport("world-persistence", modules.world_persistence); + phase5_stress_root.addImport("world-worldgen", modules.world_worldgen); + phase5_stress_root.addOptions("world_lod_options", opts.world_lod_options); + const phase5_stress_tests = b.addTest(.{ + .root_module = phase5_stress_root, + .filters = &.{"Phase 5 stress"}, + }); + const run_phase5_stress_tests = b.addRunArtifact(phase5_stress_tests); + run_phase5_stress_tests.setEnvironmentVariable("ZIGCRAFT_LOG_LEVEL", "fatal"); + run_phase5_stress_tests.setEnvironmentVariable("ZIGCRAFT_PHASE5_STRESS_ITERATIONS", b.fmt("{}", .{phase5_stress_iterations})); + + const phase5_benchmark_config = b.addSystemCommand(&.{ "bash", "scripts/check_phase5_benchmark_config.sh" }); + phase5_benchmark_config.setCwd(b.path(".")); + const phase5_benchmark_schema = b.addSystemCommand(&.{ "python3", "scripts/benchmark_baseline.py", "self-test" }); + phase5_benchmark_schema.setCwd(b.path(".")); + const phase5_checked_in_baseline = b.addSystemCommand(&.{ "python3", "scripts/benchmark_baseline.py", "validate", "docs/benchmarks/baseline.json" }); + phase5_checked_in_baseline.setCwd(b.path(".")); + const phase5_checked_in_acceptance = b.addSystemCommand(&.{ "python3", "scripts/benchmark_baseline.py", "validate", "docs/benchmarks/acceptance-baseline.json" }); + phase5_checked_in_acceptance.setCwd(b.path(".")); + const phase5_checked_in_gpu_culling = b.addSystemCommand(&.{ "python3", "scripts/benchmark_baseline.py", "validate-gpu-culling", "docs/benchmarks/gpu-culling-baseline.json" }); + phase5_checked_in_gpu_culling.setCwd(b.path(".")); + + const phase5_visual_smoke = b.addSystemCommand(&.{ "bash", "scripts/run_phase5_visual_smoke.sh" }); + phase5_visual_smoke.setCwd(b.path(".")); + + const phase5_gate_step = b.step("phase5-gate", "Validate Phase 5 LOD fallback and policy"); + phase5_gate_step.dependOn(&run_benchmark_phase5_gate_tests.step); + phase5_gate_step.dependOn(&run_phase5_lod_gate_tests.step); + phase5_gate_step.dependOn(&phase5_benchmark_config.step); + phase5_gate_step.dependOn(&phase5_benchmark_schema.step); + phase5_gate_step.dependOn(&phase5_checked_in_baseline.step); + phase5_gate_step.dependOn(&phase5_checked_in_acceptance.step); + phase5_gate_step.dependOn(&phase5_checked_in_gpu_culling.step); + + const phase5_stress_gate_step = b.step("phase5-stress-gate", "Run deterministic low-memory Phase 5 streaming stress coverage"); + phase5_stress_gate_step.dependOn(&run_phase5_stress_tests.step); + + const phase5_visual_gate_step = b.step("phase5-visual-gate", "Capture deterministic fixtures, bounded motion scenes, and a persisted saved-world compact-auto reload qualification"); + phase5_visual_gate_step.dependOn(&phase5_visual_smoke.step); + const engine_math_fuzz_root = b.createModule(.{ .root_source_file = b.path("modules/engine-math/src/ray_fuzz_tests.zig"), .target = target, .optimize = optimize, .sanitize_c = sanitize_c }); engine_math_fuzz_root.addImport("zig-math", zig_math); const engine_math_fuzz_tests = b.addTest(.{ .root_module = engine_math_fuzz_root, .filters = test_filters }); @@ -809,7 +966,7 @@ fn defineBuildSteps( defineShaderValidation(b, test_step); } -fn defineBuildOptions(b: *std.Build) BuildOptions { +fn defineBuildOptions(b: *std.Build, optimize: std.builtin.OptimizeMode) BuildOptions { const options = b.addOptions(); const enable_debug_shadows = b.option(bool, "debug_shadows", "Enable debug shadow visualization resources") orelse false; options.addOption(bool, "debug_shadows", enable_debug_shadows); @@ -878,6 +1035,11 @@ fn defineBuildOptions(b: *std.Build) BuildOptions { const screenshot_delay_seconds = b.option(u32, "screenshot-delay-seconds", "Seconds to wait after screenshot target is ready before capture") orelse 0; options.addOption(u32, "screenshot_delay_seconds", screenshot_delay_seconds); + const phase5_visual_scene = b.option([]const u8, "phase5-visual-scene", "Deterministic production-world fixture/camera for the Phase 5 visual gate (seam, water, lod-handoff, lod-handoff-traversal, fog-rapid-turn, teleport-handoff, saved-world-create, saved-world-reload)") orelse ""; + options.addOption([]const u8, "phase5_visual_scene", phase5_visual_scene); + const phase5_visual_run_id = b.option([]const u8, "phase5-visual-run-id", "Fresh evidence scope identifier for a Phase 5 visual-gate invocation") orelse ""; + options.addOption([]const u8, "phase5_visual_run_id", phase5_visual_run_id); + const shadow_test_scene = b.option(bool, "shadow-test-scene", "Launch the deterministic shadow/cave lighting test scene") orelse false; options.addOption(bool, "shadow_test_scene", shadow_test_scene); @@ -886,10 +1048,23 @@ fn defineBuildOptions(b: *std.Build) BuildOptions { const benchmark = b.option(bool, "benchmark", "Enable benchmark mode") orelse false; options.addOption(bool, "benchmark", benchmark); + // Benchmark runs opt into LOD CPU/pressure collection without requiring a + // process-wide environment mutation. Normal runs remain opt-in through + // ZIGCRAFT_LOD_PROFILE. + world_lod_options.addOption(bool, "benchmark_lod_profile", benchmark); + const benchmark_gpu_culling = b.option(bool, "benchmark-gpu-culling", "Request LOD GPU culling in the benchmark executable") orelse false; + world_lod_options.addOption(bool, "benchmark_gpu_culling", benchmark_gpu_culling); const benchmark_preset = b.option([]const u8, "benchmark-preset", "Graphics preset to benchmark (low, medium, high, ultra, extreme)") orelse "medium"; options.addOption([]const u8, "benchmark_preset", benchmark_preset); + const benchmark_scenario = b.option([]const u8, "benchmark-scenario", "Benchmark pose scenario (stationary, traversal, rapid-turn, teleport-eviction)") orelse "traversal"; + if (!isBenchmarkScenario(benchmark_scenario)) { + std.log.err("unsupported -Dbenchmark-scenario value '{s}' (expected stationary, traversal, rapid-turn, or teleport-eviction)", .{benchmark_scenario}); + b.invalid_user_input = true; + } + options.addOption([]const u8, "benchmark_scenario", benchmark_scenario); + const benchmark_duration = b.option(u32, "benchmark-duration", "Benchmark duration in seconds") orelse 60; options.addOption(u32, "benchmark_duration", benchmark_duration); @@ -898,6 +1073,29 @@ fn defineBuildOptions(b: *std.Build) BuildOptions { const benchmark_render_distance = b.option(i32, "benchmark-render-distance", "Override benchmark render and LOD horizon distance; 0 uses selected preset") orelse 0; options.addOption(i32, "benchmark_render_distance", benchmark_render_distance); + const benchmark_horizon_distance = b.option(i32, "benchmark-horizon-distance", "Override benchmark LOD horizon distance only; 0 uses selected preset") orelse 0; + if (benchmark_horizon_distance < 0) { + std.log.err("-Dbenchmark-horizon-distance must be zero or positive, got {}", .{benchmark_horizon_distance}); + b.invalid_user_input = true; + } + options.addOption(i32, "benchmark_horizon_distance", benchmark_horizon_distance); + const benchmark_lod_memory_budget_mb = b.option(u32, "benchmark-lod-memory-budget-mb", "Benchmark-only LOD memory budget override in MiB; 0 uses the preset (maximum 4096)") orelse 0; + if (benchmark_lod_memory_budget_mb > 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"; const sanitize_c = resolveSanitizeC(b, sanitize); @@ -921,13 +1119,27 @@ fn defineBuildOptions(b: *std.Build) BuildOptions { .window_no_focus = window_no_focus, .shadow_test_variant = shadow_test_variant, .benchmark_preset = benchmark_preset, + .benchmark_scenario = benchmark_scenario, .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, }; } +fn isBenchmarkScenario(scenario: []const u8) bool { + return std.mem.eql(u8, scenario, "stationary") or + std.mem.eql(u8, scenario, "traversal") or + std.mem.eql(u8, scenario, "rapid-turn") or + std.mem.eql(u8, scenario, "teleport-eviction"); +} + fn resolveSanitizeC(b: *std.Build, sanitize: []const u8) ?std.zig.SanitizeC { if (std.mem.eql(u8, sanitize, "none")) return null; if (std.mem.eql(u8, sanitize, "address")) return .full; diff --git a/docs/benchmarks/README.md b/docs/benchmarks/README.md index 113c7ebf..eed98a3a 100644 --- a/docs/benchmarks/README.md +++ b/docs/benchmarks/README.md @@ -1,32 +1,346 @@ # Benchmark Baselines -ZigCraft benchmark gating compares every `dev` benchmark run against `baseline.json`. +Benchmark artifacts use schema v3. The checked-in `baseline.json` has been +regenerated from real paired captures and is ready for canary comparison; no +values are invented to make a gate pass. + +The default script captures the real `overworld` generator with no fixture. A +fixture such as `lod-handoff` is diagnostic-only and must be stated in the +artifact provenance; it is not acceptance evidence. Normal compact baseline +pairs explicitly reject the dedicated `gpu-culling-scale` fixture, so they +cannot be confused with the culling workload. + +Capture low/medium/high for all four scenarios in both compact modes, then +assemble the ready 5-second canary baseline: + +```bash +ZIGCRAFT_BENCHMARK_GPU_ADAPTER='your adapter' \ +ZIGCRAFT_BENCHMARK_GPU_DRIVER='your driver' \ +ZIGCRAFT_BENCHMARK_RUNNER='your runner label' \ +ZIGCRAFT_BENCHMARK_ZIG_TOOLCHAIN='Zig 0.16.0 / your toolchain' \ +scripts/run_benchmark.sh --duration 5 --presets low,medium,high \ + --scenarios stationary,traversal,rapid-turn,teleport-eviction \ + --compact-modes off,auto --benchmark-world overworld --output-dir benchmark-results +python3 scripts/benchmark_baseline.py validate-compact-matrix benchmark-results +python3 scripts/benchmark_baseline.py assemble --output docs/benchmarks/baseline.json \ + --overwrite benchmark-results +``` + +Assembly retains both complete `off` and `auto` artifacts under `compact_pairs` +as well as the intended auto result used by regression comparison. It refuses a +missing pair, duplicate capture, incomplete startup/readiness evidence, or +incompatible seed/build/headless/resolution/hardware/toolchain provenance. +`ultra` and `extreme` remain runtime policy-covered presets but are not required +capture rows. + +The checked `acceptance-baseline.json` is the corresponding 60-second matrix. +It covers the complete traversal loop, repeated rapid turns, and repeated +teleport/eviction cycles. Regenerate it separately; never overwrite the canary +baseline used for short comparisons. + +At readiness, each artifact preserves cumulative upload bytes, worker generation +and mesh-construction time, compact submissions, and resident geometry gauges. +It also records LOD3/LOD4 representation-specific counters: +`far_expanded_upload_bytes`, `compact_upload_bytes`, +`worker_far_expanded_mesh_construction_ms`, and +`worker_compact_encode_ms`. Steady-state sampled deltas remain in `lod`. +Every pair must show zero compact residency/submissions for `off` and nonzero +values for `auto`. + +The auto result must reduce **far resident geometry** +(`direct_mesh_gpu_bytes + compact_pool_allocated_bytes`) and **far +representation upload bytes** (`far_expanded_upload_bytes + +compact_upload_bytes`) by **at least 20%** whenever the corresponding `off` +value is positive. The near pooled allocation and `pool_cpu_shadow_bytes` are +contextual fields only: they are reported but are not acceptance thresholds. +Total upload remains contextual because unrelated near streaming varies. Worker +far-representation work is the sum of expanded-worker construction and compact +encoding in each run, so auto fallbacks are not hidden. It has the same 20% +requirement only when both runs provide a positive, stable numeric timing basis. +Otherwise its delta is reported without inventing a percentage. + +`scripts/benchmark_baseline.py compatibility BASELINE RESULT --preset P +--scenario S` verifies the exact provenance before metrics are compared. ## Reference Hardware -Baselines are captured on the GitHub Actions benchmark job runner: +The checked canary and acceptance baselines were captured on: -- Runner label: `blacksmith-2vcpu-ubuntu-2404` -- Workflow: `.github/workflows/benchmark.yml` -- Graphics backend: Mesa Lavapipe via `.github/actions/setup-lavapipe` +- Runner label: `local NixOS x86_64` +- GPU: `AMD Radeon RX 5700 XT` +- Graphics driver: Mesa RADV 25.2.6 - Zig version: `0.16.0`, provided by the Nix flake - Build mode: `ReleaseFast` - Presets: `low`, `medium`, `high` -- Duration: 5 sampled seconds per preset after a 1-second warmup for the CI canary gate +- `baseline.json`: 5 sampled seconds per row after readiness +- `acceptance-baseline.json`: 60 sampled seconds per row after readiness -Lavapipe and Zig versions are pinned by the Nix inputs used by the workflow. If the flake lock changes, refresh this baseline so performance drift is tied to a known toolchain. +Each evidence artifact records explicit GPU/adapter, driver, runner, and Zig +toolchain labels. Evidence mode rejects missing or `unknown` labels. CI also +runs the full matrix on its independently labelled Lavapipe runner and enforces +the schema, material-reduction, SLO, and Vulkan-validation gates. Strict numeric +comparison is skipped when that runner does not match the checked reference +provenance; Bencher retains CI-hardware trends instead of pretending the two +devices are comparable. Refresh a baseline whenever its recorded hardware or +toolchain changes. ## Capture Procedure Run the benchmark workflow on the baseline branch or on `dev`: ```bash -gh workflow run benchmark.yml --repo OpenStaticFish/ZigCraft --ref -f duration=5 +gh workflow run benchmark.yml --repo OpenStaticFish/ZigCraft --ref -f duration=60 +``` + +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: + +```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 +nix develop --command zig build phase5-gate ``` -After the run finishes, download the `benchmark-results` artifact and replace the corresponding `low`, `medium`, and `high` entries in `baseline.json` with the captured JSON payloads. Keep `generated` set to `true`. +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. + +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: + +```bash +nix develop --command zig build phase5-visual-gate +``` + +## 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. | -This gate is intentionally a short canary so every `dev` push gets a bounded performance signal without consuming long runner time. The benchmark warmup is excluded from samples so startup shader/resource transitions do not dominate steady-state regression checks. Longer profiling runs should use the manual workflow input with a larger duration and should not replace the CI canary baseline unless that policy changes deliberately. +Record the generated JSON filename beside each optional manual capture set. ## Tolerance Policy @@ -35,14 +349,14 @@ This gate is intentionally a short canary so every `dev` push gets a bounded per - Warning threshold: 5% regression. - Failure threshold: 10% regression. - Lower `fps.p1` is a regression. -- Higher `gpu_ms.total_avg` 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 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 | | --- | ---: | ---: | ---: | ---: | ---: | diff --git a/docs/benchmarks/acceptance-baseline.json b/docs/benchmarks/acceptance-baseline.json new file mode 100644 index 00000000..d11b7aab --- /dev/null +++ b/docs/benchmarks/acceptance-baseline.json @@ -0,0 +1,16225 @@ +{ + "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-05 + }, + "upload_prep": { + "total_ms": 6.129153, + "avg_ms": 8.951327549947424e-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 6.791329, + "avg_ms": 9.153844805973771e-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 9.112609, + "avg_ms": 9.491312363295491e-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 3.1688369999999995, + "avg_ms": 8.13231278550531e-05 + }, + "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-05 + }, + "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-06 + }, + "state_transition": { + "total_ms": 1.7323709999999999, + "avg_ms": 4.869630358397751e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 6.129153, + "avg_ms": 8.951327549947424e-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 6.791329, + "avg_ms": 9.153844805973771e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 15.081804000000002, + "avg_ms": 0.00014328415892378728 + }, + "upload_submission": { + "total_ms": 9.74876599999999, + "avg_ms": 9.261781527294827e-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 9.112609, + "avg_ms": 9.491312363295491e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 3.1688369999999995, + "avg_ms": 8.13231278550531e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-06 + }, + "state_transition": { + "total_ms": 1.7323709999999999, + "avg_ms": 4.869630358397751e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 2.644522, + "avg_ms": 7.955363696528488e-05 + }, + "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-05 + }, + "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 index 68fc7637..f455b6d3 100644 --- a/docs/benchmarks/baseline.json +++ b/docs/benchmarks/baseline.json @@ -1,90 +1,16225 @@ { + "schema_version": 3, + "artifact_type": "benchmark-baseline", + "status": "ready", "generated": true, - "presets": { + "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": { - "preset": "low", - "render_distance": 6, - "gpu_memory_mb_avg": 1344.4645912635465, - "gpu_memory_mb_max": 2079.258544921875, - "frames": 1214, - "duration_s": 5, - "fps": { - "min": 7.656815528869629, - "avg": 242.66857404131517, - "max": 2751.841796875, - "p1": 15.02885633468628, - "p5": 177.1574478149414, - "p50": 421.84117126464844, - "p95": 1665.7115783691402, - "p99": 1989.8668627929687 - }, - "max_frame_ms": 130.60260009765625, - "cpu_ms_avg": 4.120846730775063, - "gpu_ms": { - "shadow_avg": 0.00036556835752747695, - "opaque_avg": 1.6955634606434447, - "total_avg": 1.9121151362258872 - }, - "draw_calls_avg": 543.7874794069193, - "vertices_avg": 830302.705107084, - "chunks_rendered_avg": 121.43574958813839 + "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-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 0.774721, + "avg_ms": 9.988666838576585e-05 + }, + "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-05 + }, + "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": { - "preset": "medium", - "render_distance": 10, - "gpu_memory_mb_avg": 1284.3805604833117, - "gpu_memory_mb_max": 2039.2586669921875, - "frames": 845, - "duration_s": 5, - "fps": { - "min": 7.548447132110596, - "avg": 168.7653895847781, - "max": 1213.2109375, - "p1": 13.016612815856934, - "p5": 97.95580291748047, - "p50": 262.6923828125, - "p95": 819.2251708984372, - "p99": 1075.7478027343748 - }, - "max_frame_ms": 132.4775848388672, - "cpu_ms_avg": 5.925385545344042, - "gpu_ms": { - "shadow_avg": 0.20434230724735372, - "opaque_avg": 1.8485858451685258, - "total_avg": 2.8808859789865258 - }, - "draw_calls_avg": 2460.2473372781064, - "vertices_avg": 1359609.3656804734, - "chunks_rendered_avg": 210.47100591715977 + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 0.307413, + "avg_ms": 9.43274010432648e-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 0.19633899999999993, + "avg_ms": 7.328816722657705e-05 + }, + "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": { - "preset": "high", - "render_distance": 12, - "gpu_memory_mb_avg": 1280.9022313486232, - "gpu_memory_mb_max": 1991.2586669921875, - "frames": 808, - "duration_s": 5, - "fps": { - "min": 5.337591648101807, - "avg": 160.93166696206083, - "max": 1203.9185791015625, - "p1": 11.711967334747314, - "p5": 100.45688095092774, - "p50": 239.04169464111328, - "p95": 819.6603210449218, - "p99": 915.9270965576171 - }, - "max_frame_ms": 187.35040283203125, - "cpu_ms_avg": 6.213817447350166, - "gpu_ms": { - "shadow_avg": 0.269094777118304, - "opaque_avg": 1.8477888158937492, - "total_avg": 3.139283932376616 - }, - "draw_calls_avg": 2457.022277227723, - "vertices_avg": 1357648.5965346536, - "chunks_rendered_avg": 210.29579207920793 + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-06 + }, + "state_transition": { + "total_ms": 0.1657129999999999, + "avg_ms": 6.858981788079466e-05 + }, + "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" + } } }, - "note": "Generated from 5-second Lavapipe ReleaseFast benchmark runs after the benchmark warmup for CI canary gating." + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "upload_prep": { + "total_ms": 0.774721, + "avg_ms": 9.988666838576585e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 0.307413, + "avg_ms": 9.43274010432648e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "state_transition": { + "total_ms": 0.19633899999999993, + "avg_ms": 7.328816722657705e-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-05 + }, + "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-06 + }, + "state_transition": { + "total_ms": 0.1657129999999999, + "avg_ms": 6.858981788079466e-05 + }, + "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 new file mode 100644 index 00000000..0153b050 --- /dev/null +++ b/docs/benchmarks/gpu-culling-baseline.json @@ -0,0 +1,1001 @@ +{ + "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-06, + "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-06, + "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 new file mode 100644 index 00000000..b7dcb580 --- /dev/null +++ b/docs/decisions/917-compact-gpu-lod-tiles.md @@ -0,0 +1,165 @@ +# 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 index d641bdfd..e3b05718 100644 --- a/docs/lod-quality-controls.md +++ b/docs/lod-quality-controls.md @@ -2,14 +2,60 @@ The render distance preset is the supported user-facing control for distant LOD quality. Presets intentionally expose a small set of stable knobs: -- `lod_radii`: chunk radii for LOD0 through LOD3. +- `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. -- `vertical_span_budget`: enables rich column/span source data when greater than zero. Defaults keep this disabled to preserve current memory and generation cost. -- `mesh_path`: selects the stable heightfield path by default. `column_spans` and `qem` are available for controlled testing. +- `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`: controls the fade band for each LOD level. - `memory_budget_mb` and `max_uploads_per_frame`: bound cache pressure and per-frame GPU upload work. -Default presets preserve existing performance expectations by keeping `mesh_path = .heightfield` and `vertical_span_budget = 0`. +## 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/65 | 2 | 256 MB | 1,024 MB | 8 | +| High | 512 chunks | 33/65/65/97/97 | 3 | 384 MB | 1,536 MB | 8 | +| Ultra | 1,024 chunks | 33/65/65/129/129 | 4 | 512 MB | 3,072 MB | 12 | +| Extreme | 2,048 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. +- 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 @@ -17,7 +63,75 @@ Set `ZIGCRAFT_LOD_DIAG=1` to log LOD queue, render, and aggregate stats diagnost 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/shaders/spirv-sizes.json b/docs/shaders/spirv-sizes.json index b4918411..c73e73ab 100644 --- a/docs/shaders/spirv-sizes.json +++ b/docs/shaders/spirv-sizes.json @@ -12,6 +12,11 @@ "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": 4296, + "assets/shaders/vulkan/lod_compact_terrain.vert": 20656, + "assets/shaders/vulkan/lod_compact_water.frag": 5364, + "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/modules/engine-core/src/time.zig b/modules/engine-core/src/time.zig index 9225c909..4fab2720 100644 --- a/modules/engine-core/src/time.zig +++ b/modules/engine-core/src/time.zig @@ -8,6 +8,26 @@ pub fn timestampMs() i64 { return std.Io.Clock.real.now(std.Options.debug_io).toMilliseconds(); } +/// A lightweight monotonic elapsed-time timer backed by SDL's high-resolution +/// performance counter. Construct it only at an enabled instrumentation site. +pub const MonotonicTimer = struct { + start_ticks: u64, + + pub fn start() MonotonicTimer { + return .{ .start_ticks = c.SDL_GetPerformanceCounter() }; + } + + pub fn read(self: MonotonicTimer) u64 { + const frequency = c.SDL_GetPerformanceFrequency(); + if (frequency == 0) return 0; + const elapsed_ticks = c.SDL_GetPerformanceCounter() - self.start_ticks; + // Split quotient/remainder to preserve precision without overflowing + // for a process that has been running for a long time. + return (elapsed_ticks / frequency) * std.time.ns_per_s + + (elapsed_ticks % frequency) * std.time.ns_per_s / frequency; + } +}; + pub const Time = struct { /// Time since last frame in seconds delta_time: f32 = 0, diff --git a/modules/engine-graphics/src/cloud_system.zig b/modules/engine-graphics/src/cloud_system.zig index 6a4b4457..3f96e3c2 100644 --- a/modules/engine-graphics/src/cloud_system.zig +++ b/modules/engine-graphics/src/cloud_system.zig @@ -67,7 +67,10 @@ pub const CloudSystem = struct { pub fn render(self: *CloudSystem, ctx: rhi.RenderContext, camera_pos: Vec3) !void { if (!self.config.enabled or self.config.density <= 0.0) return; try self.updateMesh(camera_pos); - try self.uploadMesh(); + self.uploadMesh() catch |err| switch (err) { + error.OutOfMemory, error.PendingCopyOverflow => return, + else => return err, + }; if (self.vertex_count == 0) return; ctx.setTerrainPipelineBound(false); const smooth_offset = Vec3.init( @@ -84,7 +87,10 @@ pub const CloudSystem = struct { pub fn renderShadow(self: *CloudSystem, ctx: rhi.RenderContext, camera_pos: Vec3) !void { if (!self.config.enabled or self.config.density <= 0.0 or self.vertex_count == 0 or !self.gpu_valid) return; try self.updateMesh(camera_pos); - try self.uploadMesh(); + self.uploadMesh() catch |err| switch (err) { + error.OutOfMemory, error.PendingCopyOverflow => return, + else => return err, + }; const smooth_offset = Vec3.init( (self.origin_x - self.mesh_origin_x) - (camera_pos.x - self.mesh_camera_pos.x), -(camera_pos.y - self.mesh_camera_pos.y), diff --git a/modules/engine-graphics/src/render_graph.zig b/modules/engine-graphics/src/render_graph.zig index 3d202250..2d5be621 100644 --- a/modules/engine-graphics/src/render_graph.zig +++ b/modules/engine-graphics/src/render_graph.zig @@ -478,6 +478,19 @@ pub const OpaquePass = struct { } }; +/// 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); + } +}; + pub const EntityPass = struct { const VTABLE = IRenderPass.VTable{ .name = "EntityPass", diff --git a/modules/engine-graphics/src/render_system.zig b/modules/engine-graphics/src/render_system.zig index fc0479bb..1607c4a6 100644 --- a/modules/engine-graphics/src/render_system.zig +++ b/modules/engine-graphics/src/render_system.zig @@ -66,6 +66,7 @@ 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, @@ -220,6 +221,7 @@ 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 }, @@ -277,6 +279,7 @@ 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 9c1fc3ba..7cd9a696 100644 --- a/modules/engine-graphics/src/rhi_tests.zig +++ b/modules/engine-graphics/src/rhi_tests.zig @@ -64,6 +64,9 @@ const MockContext = struct { _ = draw_count; _ = stride; } + fn drawIndirectCount(_: *anyopaque, _: rhi.BufferHandle, _: rhi.BufferHandle, _: usize, _: rhi.BufferHandle, _: usize, _: u32, _: u32) bool { + return false; + } fn drawInstance(ptr: *anyopaque, handle: rhi.BufferHandle, count: u32, instance_index: u32) void { _ = ptr; _ = handle; @@ -193,6 +196,9 @@ 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; @@ -471,6 +477,8 @@ const MockContext = struct { const MOCK_QUERY_VTABLE = rhi.IDeviceQuery.VTable{ .getFrameIndex = undefined, .supportsIndirectFirstInstance = undefined, + .supportsIndirectCount = undefined, + .supportsCompactLODGpuCulling = undefined, .getMaxAnisotropy = undefined, .getMaxMSAASamples = undefined, .getFaultCount = undefined, @@ -568,6 +576,7 @@ const MockContext = struct { }, .culling_factory = .{ .createCullingSystem = MockContext.createCullingSystem, + .createLODCullingSystem = MockContext.createLODCullingSystem, }, .screenshot = .{ .captureFrame = undefined, @@ -581,7 +590,10 @@ const MockContext = struct { .draw = draw, .drawOffset = drawOffset, .drawIndexed = drawIndexed, + .drawCompactLOD = undefined, + .drawCompactLODIndirectCount = undefined, .drawIndirect = drawIndirect, + .drawIndirectCount = drawIndirectCount, .drawInstance = drawInstance, .setViewport = setViewport, }; @@ -590,6 +602,9 @@ const MockContext = struct { .setModelMatrix = undefined, .setInstanceBuffer = undefined, .setLODInstanceBuffer = undefined, + .setLODDescriptorStream = undefined, + .setLODCompactSampleBuffer = undefined, + .setLODCompactInstanceBuffer = undefined, .setTerrainPipelineBound = undefined, .setSelectionMode = undefined, .updateGlobalUniforms = undefined, diff --git a/modules/engine-graphics/src/rhi_vulkan.zig b/modules/engine-graphics/src/rhi_vulkan.zig index 1948229f..4e465261 100644 --- a/modules/engine-graphics/src/rhi_vulkan.zig +++ b/modules/engine-graphics/src/rhi_vulkan.zig @@ -22,6 +22,7 @@ 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({ @@ -406,6 +407,11 @@ 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(); @@ -482,6 +488,21 @@ fn setLODInstanceBuffer(ctx_ptr: *anyopaque, handle: rhi.BufferHandle) void { 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); @@ -531,6 +552,10 @@ fn bindTexture(ctx_ptr: *anyopaque, handle: rhi.TextureHandle, slot: u32) void { 6 => ctx.draw.dummy_normal_texture, 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 + // terrain undefined on RADV. + 11, 12, 13 => ctx.draw.dummy_texture_3d, 0, 1 => ctx.draw.dummy_texture, else => ctx.draw.dummy_texture, } else handle; @@ -586,6 +611,16 @@ fn supportsIndirectFirstInstance(ctx_ptr: *anyopaque) bool { return state_control.supportsIndirectFirstInstance(ctx); } +fn supportsIndirectCount(ctx_ptr: *anyopaque) bool { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + 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); @@ -681,6 +716,18 @@ 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(); @@ -688,6 +735,11 @@ fn drawIndirect(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, command_buffer: r draw_submission.drawIndirect(ctx, handle, command_buffer, offset, draw_count, stride); } +fn drawIndirectCount(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, command_buffer: rhi.BufferHandle, offset: usize, count_buffer: rhi.BufferHandle, count_offset: usize, max_draw_count: u32, stride: u32) bool { + const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); + return draw_submission.drawIndirectCount(ctx, handle, command_buffer, offset, count_buffer, count_offset, max_draw_count, stride); +} + fn drawInstance(ctx_ptr: *anyopaque, handle: rhi.BufferHandle, count: u32, instance_index: u32) void { const ctx: *VulkanContext = @ptrCast(@alignCast(ctx_ptr)); ctx.mutex.lock(); @@ -1061,6 +1113,9 @@ 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, @@ -1078,7 +1133,10 @@ const VULKAN_COMMAND_ENCODER_VTABLE = rhi.IGraphicsCommandEncoder.VTable{ .draw = draw, .drawOffset = drawOffset, .drawIndexed = drawIndexed, + .drawCompactLOD = drawCompactLOD, + .drawCompactLODIndirectCount = drawCompactLODIndirectCount, .drawIndirect = drawIndirect, + .drawIndirectCount = drawIndirectCount, .drawInstance = drawInstance, .setViewport = setViewport, }; @@ -1425,6 +1483,8 @@ const VULKAN_NATIVE_HANDLES_VTABLE = rhi.VulkanNativeHandles.VTable{ const VULKAN_DEVICE_QUERY_VTABLE = rhi.IDeviceQuery.VTable{ .getFrameIndex = getFrameIndex, .supportsIndirectFirstInstance = supportsIndirectFirstInstance, + .supportsIndirectCount = supportsIndirectCount, + .supportsCompactLODGpuCulling = supportsCompactLODGpuCulling, .getMaxAnisotropy = getMaxAnisotropy, .getMaxMSAASamples = getMaxMSAASamples, .getFaultCount = getFaultCount, @@ -1474,6 +1534,7 @@ 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 12b27961..54c48f80 100644 --- a/modules/engine-graphics/src/root.zig +++ b/modules/engine-graphics/src/root.zig @@ -18,6 +18,7 @@ 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; @@ -46,6 +47,7 @@ pub const rhi_pass_orchestration_tests = @import("vulkan/rhi_pass_orchestration_ pub const rhi_state_control_tests = @import("vulkan/rhi_state_control_tests.zig"); pub const shader_registry_tests = @import("vulkan/shader_registry_tests.zig"); pub const ssao_system_tests = @import("vulkan/ssao_system_tests.zig"); +pub const screenshot_tests = @import("vulkan/screenshot.zig"); pub const utils_tests = @import("vulkan/utils_tests.zig"); pub const vulkan_frame_tests = @import("vulkan/vulkan_frame_tests.zig"); diff --git a/modules/engine-graphics/src/vulkan/descriptor_bindings.zig b/modules/engine-graphics/src/vulkan/descriptor_bindings.zig index 57440889..17358b33 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_bindings.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_bindings.zig @@ -14,3 +14,5 @@ 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 74a918ae..54618524 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_manager.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_manager.zig @@ -10,6 +10,40 @@ 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, @@ -37,7 +71,10 @@ 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, @@ -46,6 +83,8 @@ 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, @@ -62,11 +101,14 @@ pub const DescriptorManager = struct { .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, @@ -92,6 +134,14 @@ 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); @@ -132,7 +182,9 @@ 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 }, - .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 100 }, + // 2 frames × 8 immutable LOD snapshots × 3 storage bindings, + // plus legacy, UI, and effect descriptors. + .{ .type = c.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, .descriptorCount = 160 }, }; var pool_info = std.mem.zeroes(c.VkDescriptorPoolCreateInfo); @@ -181,6 +233,10 @@ 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); @@ -209,6 +265,12 @@ pub const DescriptorManager = struct { 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{ @@ -226,6 +288,8 @@ 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{ .{ @@ -252,6 +316,8 @@ 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], @@ -276,8 +342,70 @@ pub const DescriptorManager = struct { .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. + 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{ + .sampler = dummy_2d.sampler, + .imageView = dummy_2d.view, + .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + var dummy_3d_info = c.VkDescriptorImageInfo{ + .sampler = dummy_3d.sampler, + .imageView = dummy_3d.view, + .imageLayout = c.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + }; + var texture_writes: [13]c.VkWriteDescriptorSet = undefined; + for ([_]u32{ 1, 3, 4, 6, 7, 8, 9, 10, 14, 15 }, 0..) |binding, write_index| { + texture_writes[write_index] = .{ + .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = self.descriptor_sets[i], + .dstBinding = binding, + .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = 1, + .pImageInfo = &dummy_2d_info, + }; + } + for ([_]u32{ 11, 12, 13 }, 0..) |binding, offset| { + texture_writes[10 + offset] = .{ + .sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .dstSet = self.descriptor_sets[i], + .dstBinding = binding, + .descriptorType = c.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorCount = 1, + .pImageInfo = &dummy_3d_info, + }; + } + 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, snapshot_writes.len, &snapshot_writes, 0, null); } return self; @@ -306,6 +434,16 @@ 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); @@ -328,6 +466,12 @@ 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_tests.zig b/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig index ea82c203..ba214b69 100644 --- a/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig +++ b/modules/engine-graphics/src/vulkan/descriptor_manager_tests.zig @@ -25,6 +25,7 @@ test "DescriptorManager default state has null handles" { .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), @@ -169,6 +170,44 @@ test "descriptor sets arrays match MAX_FRAMES_IN_FLIGHT" { 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" { // global_ubos and shadow_ubos should have MAX_FRAMES_IN_FLIGHT elements @@ -210,11 +249,11 @@ test "TextureHandle type is appropriate for handle values" { test "descriptor pool sizes are within Vulkan limits" { // The pool sizes defined in init() should be within reasonable limits - // From the source: 500 uniform buffers, 1000 samplers, 100 storage buffers + // From the source: 500 uniform buffers, 1000 samplers, 160 storage buffers const max_uniform_buffers = 500; const max_samplers = 1000; - const max_storage_buffers = 100; + const max_storage_buffers = 160; const max_sets = 1000; // These should be positive diff --git a/modules/engine-graphics/src/vulkan/lod_culling_system.zig b/modules/engine-graphics/src/vulkan/lod_culling_system.zig new file mode 100644 index 00000000..61f1feef --- /dev/null +++ b/modules/engine-graphics/src/vulkan/lod_culling_system.zig @@ -0,0 +1,582 @@ +//! 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 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.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); + } + + 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; + 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 = candidate.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 = candidate.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 883bdc8d..eac472f4 100644 --- a/modules/engine-graphics/src/vulkan/pipeline_manager.zig +++ b/modules/engine-graphics/src/vulkan/pipeline_manager.zig @@ -41,6 +41,8 @@ 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, @@ -220,6 +222,8 @@ 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); @@ -240,6 +244,8 @@ 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; @@ -337,6 +343,8 @@ 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 2bef9d91..5b68b989 100644 --- a/modules/engine-graphics/src/vulkan/pipeline_specialized.zig +++ b/modules/engine-graphics/src/vulkan/pipeline_specialized.zig @@ -143,6 +143,78 @@ 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/resource_manager.zig b/modules/engine-graphics/src/vulkan/resource_manager.zig index faf2da26..c0064857 100644 --- a/modules/engine-graphics/src/vulkan/resource_manager.zig +++ b/modules/engine-graphics/src/vulkan/resource_manager.zig @@ -89,8 +89,12 @@ pub const ResourceManager = struct { self.staging_ring = try StagingRing.init(vulkan_device, transfer_queue.DEFAULT_STAGING_CAPACITY); log.log.info("Staging ring initialized: {}MB", .{transfer_queue.DEFAULT_STAGING_CAPACITY / (1024 * 1024)}); - const tx_family = vulkan_device.transfer_family; - const is_dedicated = vulkan_device.has_dedicated_transfer_queue; + // Buffer resources use exclusive sharing and are consumed by graphics. + // Until queue-family release/acquire ownership transfers are modeled, + // record uploads on the graphics family. A semaphore alone does not + // transfer exclusive buffer ownership from a transfer-only queue. + const tx_family = vulkan_device.graphics_family; + const is_dedicated = false; self.transfer = try TransferQueue.init(vulkan_device, tx_family, is_dedicated); if (is_dedicated) { log.log.info("Transfer queue: DEDICATED (family {})", .{tx_family}); @@ -265,7 +269,7 @@ pub const ResourceManager = struct { .vertex => c.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, .index => c.VK_BUFFER_USAGE_INDEX_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, .uniform => c.VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT, - .indirect => c.VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT | c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, + .indirect => c.VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_SRC_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT | c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, .storage => c.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | c.VK_BUFFER_USAGE_TRANSFER_DST_BIT | c.VK_BUFFER_USAGE_TRANSFER_SRC_BIT | c.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, }; @@ -297,9 +301,11 @@ pub const ResourceManager = struct { pub fn updateBuffer(self: *ResourceManager, handle: rhi.BufferHandle, offset: usize, data: []const u8) rhi.RhiError!void { const buf = self.buffers.get(handle) orelse return; + const end = std.math.add(usize, offset, data.len) catch return error.InvalidState; + if (@as(u64, @intCast(end)) > buf.size) return error.InvalidState; const slice = self.staging_ring.allocate(data.len, self.current_frame_index) orelse { - log.log.err("Staging ring overflow in updateBuffer! Data dropped.", .{}); + log.log.err("Staging ring overflow in updateBuffer: request={} used={} head={} tail={} frame={} frame_used={any}", .{ data.len, self.staging_ring.used_total, self.staging_ring.head, self.staging_ring.tail, self.current_frame_index, self.staging_ring.frame_used }); return error.OutOfMemory; }; diff --git a/modules/engine-graphics/src/vulkan/rhi_context_factory.zig b/modules/engine-graphics/src/vulkan/rhi_context_factory.zig index 4cf38ccc..34496eb8 100644 --- a/modules/engine-graphics/src/vulkan/rhi_context_factory.zig +++ b/modules/engine-graphics/src/vulkan/rhi_context_factory.zig @@ -92,8 +92,11 @@ pub fn createRHI( 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.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; @@ -168,10 +171,12 @@ pub fn createRHI( 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.bound_lod_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; } diff --git a/modules/engine-graphics/src/vulkan/rhi_context_types.zig b/modules/engine-graphics/src/vulkan/rhi_context_types.zig index 398ad7ff..4ec379cb 100644 --- a/modules/engine-graphics/src/vulkan/rhi_context_types.zig +++ b/modules/engine-graphics/src/vulkan/rhi_context_types.zig @@ -167,10 +167,17 @@ const DrawState = struct { terrain_pipeline_bound: bool = false, descriptors_updated: bool = false, lod_mode: bool = false, + lod_descriptor_stream: rhi.LODDescriptorStream = .terrain_standard_direct, bound_instance_buffer: [MAX_FRAMES_IN_FLIGHT]rhi.BufferHandle = .{ 0, 0 }, - bound_lod_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 }, diff --git a/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig b/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig index d10267d7..49d76eeb 100644 --- a/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig +++ b/modules/engine-graphics/src/vulkan/rhi_draw_submission.zig @@ -1,10 +1,16 @@ 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, @@ -63,6 +69,85 @@ 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; +} + +/// Indexed indirect-count submission for compact far LOD. A single sentinel +/// push constant selects SSBO instance data; every command carries an output +/// slot in firstInstance, which GLSL exposes through gl_InstanceIndex. +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 { + if (!ctx.frames.frame_in_progress or !ctx.vulkan_device.draw_indirect_count or max_draw_count == 0) 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. + _ = count_handle; + _ = count_offset; + 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; @@ -120,10 +205,10 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. } const descriptor_set = if (ctx.draw.lod_mode) - &ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame] + lodDescriptorSet(ctx) else - &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); + 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) { const cascade_index = ctx.shadow_system.pass_index; @@ -181,6 +266,50 @@ pub fn drawIndirect(ctx: anytype, handle: rhi.BufferHandle, command_buffer: rhi. } } +/// Vulkan draw-indirect-count path for compute-compacted streams. It never +/// maps the count on the CPU, so the current frame remains fully GPU-driven. +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 { + if (!ctx.frames.frame_in_progress or !ctx.vulkan_device.draw_indirect_count) 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; + const commands = ctx.resources.buffers.get(command_buffer) orelse return false; + const cb = ctx.frames.command_buffers[ctx.frames.current_frame]; + const use_shadow = ctx.shadow_system.pass_active; + const use_g_pass = ctx.runtime.g_pass_active; + if (use_shadow) { + if (!ctx.shadow_system.pipeline_bound) { + if (ctx.shadow_system.shadow_pipeline == null) return false; + c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.shadow_system.shadow_pipeline); + ctx.shadow_system.pipeline_bound = true; + } + } else if (use_g_pass) { + if (ctx.pipeline_manager.g_pipeline == null) return false; + c.vkCmdBindPipeline(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.g_pipeline); + } else if (!ctx.draw.terrain_pipeline_bound) { + const pipeline = if (ctx.water_system.pass_active) + if (ctx.options.wireframe_enabled and ctx.water_system.reflection_wireframe_pipeline != null) ctx.water_system.reflection_wireframe_pipeline else ctx.water_system.reflection_terrain_pipeline + else if (ctx.options.wireframe_enabled and ctx.pipeline_manager.wireframe_pipeline != null) ctx.pipeline_manager.wireframe_pipeline else ctx.pipeline_manager.terrain_pipeline; + if (pipeline == null) return false; + 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]; + c.vkCmdBindDescriptorSets(cb, c.VK_PIPELINE_BIND_POINT_GRAPHICS, ctx.pipeline_manager.pipeline_layout, 0, 1, &descriptor_set, 0, null); + const uniforms = ModelUniforms{ .model = Mat4.identity, .color = .{ 1.0, 1.0, 1.0, 1.0 }, .mask_radius = -1.0 }; + 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. + _ = count_buffer; + _ = count_offset; + c.vkCmdDrawIndirect(cb, commands.buffer, @intCast(offset), max_draw_count, stride); + ctx.runtime.draw_call_count += 1; + return true; +} + pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance_index: u32) void { if (!ctx.frames.frame_in_progress) return; @@ -222,10 +351,10 @@ pub fn drawInstance(ctx: anytype, handle: rhi.BufferHandle, count: u32, instance } const descriptor_set = if (ctx.draw.lod_mode) - &ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame] + lodDescriptorSet(ctx) else - &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); + 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) { const cascade_index = ctx.shadow_system.pass_index; @@ -332,10 +461,10 @@ pub fn drawOffset(ctx: anytype, handle: rhi.BufferHandle, count: u32, mode: rhi. } const descriptor_set = if (ctx.draw.lod_mode) - &ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame] + lodDescriptorSet(ctx) else - &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); + 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) { diff --git a/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig b/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig index af808c09..55d2a938 100644 --- a/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig +++ b/modules/engine-graphics/src/vulkan/rhi_frame_orchestration.zig @@ -241,6 +241,9 @@ 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] = .{}; const command_buffer = ctx.frames.getCurrentCommandBuffer(); @@ -321,6 +324,7 @@ pub fn refreshTextureDescriptors(ctx: anytype) void { ctx.draw.bound_lpv_texture_g = cur_lpv_g; ctx.draw.bound_lpv_texture_b = cur_lpv_b; for (0..rhi.SHADOW_CASCADE_COUNT) |si| ctx.draw.bound_shadow_views[si] = ctx.shadow_system.shadow_image_views[si]; + ctx.draw.texture_state_revision +%= 1; } if (ctx.draw.descriptors_dirty[ctx.frames.current_frame]) { @@ -411,14 +415,6 @@ pub fn refreshTextureDescriptors(ctx: anytype) void { if (write_count > 0) { c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, write_count, &writes[0], 0, null); - - const lod_descriptor_set = ctx.descriptors.lod_descriptor_sets[ctx.frames.current_frame]; - if (lod_descriptor_set != null) { - for (0..write_count) |i| { - writes[i].dstSet = lod_descriptor_set; - } - c.vkUpdateDescriptorSets(ctx.vulkan_device.vk_device, write_count, &writes[0], 0, null); - } } ctx.draw.descriptors_dirty[ctx.frames.current_frame] = false; @@ -426,3 +422,52 @@ 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) void { + 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]; + std.debug.assert(source != null and destination != null); + if (source == null or destination == null) return; + const should_copy = ctx.draw.lod_snapshot_seals[frame].acquire(stream, ctx.draw.texture_state_revision) catch { + std.debug.assert(false); + return; + }; + if (!should_copy) return; + + // 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); +} diff --git a/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig b/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig index acb90e0b..422df59b 100644 --- a/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig +++ b/modules/engine-graphics/src/vulkan/rhi_init_deinit.zig @@ -67,8 +67,11 @@ pub fn initContext(ctx: anytype, allocator: std.mem.Allocator, render_device: ?* 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.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_render_state.zig b/modules/engine-graphics/src/vulkan/rhi_render_state.zig index d259d742..d78cdb57 100644 --- a/modules/engine-graphics/src/vulkan/rhi_render_state.zig +++ b/modules/engine-graphics/src/vulkan/rhi_render_state.zig @@ -4,6 +4,7 @@ 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; @@ -85,6 +86,29 @@ pub fn setLODInstanceBuffer(ctx: anytype, handle: rhi.BufferHandle) void { 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; + frame_orchestration.prepareLODDescriptorSnapshot(ctx, stream); + ctx.draw.lod_descriptor_stream = stream; + ctx.draw.lod_mode = true; +} + +pub fn setLODCompactSampleBuffer(ctx: anytype, handle: rhi.BufferHandle) void { + if (!ctx.frames.frame_in_progress) 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) return; + ctx.draw.pending_lod_compact_instance_buffer = handle; + ctx.draw.lod_mode = true; + applyPendingDescriptorUpdates(ctx, ctx.frames.current_frame); +} + pub fn setTerrainPipelineBound(ctx: anytype, bound: bool) void { ctx.draw.terrain_pipeline_bound = bound; } @@ -117,7 +141,10 @@ pub fn applyPendingDescriptorUpdates(ctx: anytype, frame_index: usize) void { } } - if (ctx.draw.pending_lod_instance_buffer != 0 and ctx.draw.bound_lod_instance_buffer[frame_index] != ctx.draw.pending_lod_instance_buffer) { + 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| { @@ -129,14 +156,43 @@ pub fn applyPendingDescriptorUpdates(ctx: anytype, frame_index: usize) void { var write = std.mem.zeroes(c.VkWriteDescriptorSet); write.sType = c.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - write.dstSet = ctx.descriptors.lod_descriptor_sets[frame_index]; + 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); - ctx.draw.bound_lod_instance_buffer[frame_index] = ctx.draw.pending_lod_instance_buffer; + 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 a39af076..aad3bc4d 100644 --- a/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig +++ b/modules/engine-graphics/src/vulkan/rhi_resource_setup.zig @@ -406,6 +406,10 @@ pub fn createSSAOResources(ctx: anytype) !void { 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 0b87f4cd..b3639ea4 100644 --- a/modules/engine-graphics/src/vulkan/rhi_state_control.zig +++ b/modules/engine-graphics/src/vulkan/rhi_state_control.zig @@ -57,6 +57,17 @@ pub fn supportsIndirectFirstInstance(ctx: anytype) bool { return ctx.vulkan_device.draw_indirect_first_instance; } +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.draw_indirect_count; +} + 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 172538a0..faca07e1 100644 --- a/modules/engine-graphics/src/vulkan/rhi_timing.zig +++ b/modules/engine-graphics/src/vulkan/rhi_timing.zig @@ -12,11 +12,16 @@ 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 = 12; + pub const COUNT = 17; }; pub const PASS_COUNT = GpuPass.COUNT; @@ -32,6 +37,11 @@ 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; @@ -101,6 +111,11 @@ 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; @@ -114,6 +129,11 @@ 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; @@ -130,7 +150,16 @@ 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/screenshot.zig b/modules/engine-graphics/src/vulkan/screenshot.zig index c54b3e6c..95111b85 100644 --- a/modules/engine-graphics/src/vulkan/screenshot.zig +++ b/modules/engine-graphics/src/vulkan/screenshot.zig @@ -11,6 +11,7 @@ pub const PendingCapture = struct { path: []const u8 = &.{}, width: u32 = 0, height: u32 = 0, + row_pitch: usize = 0, format: c.VkFormat = c.VK_FORMAT_UNDEFINED, source_layout: c.VkImageLayout = c.VK_IMAGE_LAYOUT_UNDEFINED, source_image: c.VkImage = null, @@ -35,6 +36,10 @@ pub fn requestCapture(ctx: anytype, path: []const u8) bool { log.log.err("screenshot: no swapchain images available", .{}); return false; } + if (detectScreenshotFormat(path) == null) { + log.log.err("screenshot: unsupported image path '{s}' (use .png, .jpg, .jpeg, .gif, or .webp)", .{path}); + return false; + } if (!ctx.swapchain.swapchain.screenshot_capture_supported) { log.log.err("screenshot: active surface does not support transfer-source swapchain images", .{}); return false; @@ -44,8 +49,13 @@ pub fn requestCapture(ctx: anytype, path: []const u8) bool { const image_format = ctx.swapchain.getImageFormat(); const width = extent.width; const height = extent.height; + if (width == 0 or height == 0) { + log.log.err("screenshot: invalid rendered output extent", .{}); + return false; + } - const image_size: u64 = @as(u64, width) * height * 4; + const row_pitch = tightRowPitch(width); + const image_size = row_pitch * @as(usize, height); const staging = Utils.createVulkanBuffer( device, @@ -67,6 +77,7 @@ pub fn requestCapture(ctx: anytype, path: []const u8) bool { .path = owned_path, .width = width, .height = height, + .row_pitch = row_pitch, .format = image_format, .frame_index = ctx.frames.current_frame, .path_owned = true, @@ -150,7 +161,7 @@ pub fn recordCapture(ctx: anytype) void { host_barrier.dstQueueFamilyIndex = c.VK_QUEUE_FAMILY_IGNORED; host_barrier.buffer = staging.buffer; host_barrier.offset = 0; - host_barrier.size = c.VK_WHOLE_SIZE; + host_barrier.size = @intCast(capture.row_pitch * @as(usize, capture.height)); c.vkCmdPipelineBarrier( cmd_buffer, c.VK_PIPELINE_STAGE_TRANSFER_BIT, @@ -165,14 +176,18 @@ pub fn recordCapture(ctx: anytype) void { ); barrier.srcAccessMask = c.VK_ACCESS_TRANSFER_READ_BIT; - barrier.dstAccessMask = c.VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + const skip_present = ctx.swapchain.skip_present; + barrier.dstAccessMask = if (skip_present) + c.VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | c.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT + else + 0; barrier.oldLayout = dst_layout; barrier.newLayout = capture.source_layout; c.vkCmdPipelineBarrier( cmd_buffer, c.VK_PIPELINE_STAGE_TRANSFER_BIT, - c.VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + if (skip_present) c.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT else c.VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, null, @@ -203,7 +218,7 @@ pub fn completeCapture(ctx: anytype) bool { return false; }; const bytes: [*]const u8 = @ptrCast(@alignCast(mapped_ptr)); - return writeImage(bytes, capture.width, capture.height, capture.path, capture.format); + return writeImage(bytes, capture.width, capture.height, capture.row_pitch, capture.path, capture.format); } pub fn discardCapture(ctx: anytype) void { @@ -223,14 +238,18 @@ const ScreenshotFormat = enum { webp, }; -fn writeImage(data: [*]const u8, width: u32, height: u32, path: []const u8, format: c.VkFormat) bool { +fn tightRowPitch(width: u32) usize { + return @as(usize, width) * 4; +} + +fn writeImage(data: [*]const u8, width: u32, height: u32, source_row_pitch: usize, path: []const u8, format: c.VkFormat) bool { const output_format = detectScreenshotFormat(path) orelse { log.log.err("screenshot: unsupported image path '{s}' (use .png, .jpg, .jpeg, .gif, or .webp)", .{path}); return false; }; return switch (output_format) { - .png => writePNG(data, width, height, path, format), + .png => writePNG(data, width, height, source_row_pitch, path, format), .jpeg, .gif, .webp => unsupportedEncoder(path, output_format), }; } @@ -257,7 +276,7 @@ fn hasExtension(path: []const u8, ext: []const u8) bool { return true; } -fn writePNG(data: [*]const u8, width: u32, height: u32, path: []const u8, format: c.VkFormat) bool { +fn writePNG(data: [*]const u8, width: u32, height: u32, source_row_pitch: usize, path: []const u8, format: c.VkFormat) bool { if (width > 16384) { log.log.err("screenshot: width {} exceeds max supported 16384", .{width}); return false; @@ -265,6 +284,10 @@ fn writePNG(data: [*]const u8, width: u32, height: u32, path: []const u8, format const allocator = std.heap.page_allocator; const row_bytes: usize = @as(usize, width) * 3; + if (source_row_pitch < tightRowPitch(width)) { + log.log.err("screenshot: source row pitch {} is smaller than {} RGBA pixels", .{ source_row_pitch, width }); + return false; + } const raw_size: usize = (@as(usize, height) * (row_bytes + 1)); const raw = allocator.alloc(u8, raw_size) catch { log.log.err("screenshot: failed to allocate PNG scanlines", .{}); @@ -280,7 +303,7 @@ fn writePNG(data: [*]const u8, width: u32, height: u32, path: []const u8, format raw[row_start] = 0; var x: u32 = 0; while (x < width) : (x += 1) { - const src_offset: usize = (@as(usize, y) * width + x) * 4; + const src_offset: usize = @as(usize, y) * source_row_pitch + @as(usize, x) * 4; const dst_offset: usize = row_start + 1 + @as(usize, x) * 3; const rgb = finalDisplayRgb(data[src_offset..][0..4], is_bgra); raw[dst_offset] = if (needs_srgb_encode) linearByteToSrgb(rgb[0]) else rgb[0]; diff --git a/modules/engine-graphics/src/vulkan/shader_registry.zig b/modules/engine-graphics/src/vulkan/shader_registry.zig index ec64c495..66a8fba8 100644 --- a/modules/engine-graphics/src/vulkan/shader_registry.zig +++ b/modules/engine-graphics/src/vulkan/shader_registry.zig @@ -43,3 +43,8 @@ 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/transfer_queue.zig b/modules/engine-graphics/src/vulkan/transfer_queue.zig index e9186322..060f31e9 100644 --- a/modules/engine-graphics/src/vulkan/transfer_queue.zig +++ b/modules/engine-graphics/src/vulkan/transfer_queue.zig @@ -28,7 +28,7 @@ pub const StagingRing = struct { capacity: u64 = 0, head: u64 = 0, tail: u64 = 0, - frame_base: [rhi.MAX_FRAMES_IN_FLIGHT]u64, + used_total: u64 = 0, frame_used: [rhi.MAX_FRAMES_IN_FLIGHT]u64, pub fn init(device: *const VulkanDevice, capacity: u64) !StagingRing { @@ -48,10 +48,9 @@ pub const StagingRing = struct { .capacity = capacity, .head = 0, .tail = 0, - .frame_base = undefined, + .used_total = 0, .frame_used = undefined, }; - @memset(&ring.frame_base, 0); @memset(&ring.frame_used, 0); return ring; } @@ -68,23 +67,23 @@ pub const StagingRing = struct { self.capacity = 0; self.head = 0; self.tail = 0; - @memset(&self.frame_base, 0); + self.used_total = 0; @memset(&self.frame_used, 0); } pub fn beginFrame(self: *StagingRing, frame_index: usize) void { - self.frame_base[frame_index] = self.head; self.frame_used[frame_index] = 0; } pub fn reclaimFrame(self: *StagingRing, frame_index: usize) void { - self.tail = self.frame_base[frame_index] + self.frame_used[frame_index]; - if (self.tail >= self.capacity) self.tail -= self.capacity; + const reclaimed = @min(self.frame_used[frame_index], self.used_total); + self.tail = (self.tail + reclaimed) % self.capacity; + self.used_total -= reclaimed; + self.frame_used[frame_index] = 0; } pub fn allocated(self: *StagingRing) u64 { - if (self.head >= self.tail) return self.head - self.tail; - return self.capacity - self.tail + self.head; + return self.used_total; } pub fn available(self: *StagingRing) u64 { @@ -95,10 +94,12 @@ pub const StagingRing = struct { if (size == 0) return null; const aligned_head = std.mem.alignForward(u64, self.head, ALIGNMENT); + const alignment_padding = aligned_head - self.head; var padded_to_end: u64 = 0; const try_offset = blk: { if (aligned_head + size <= self.capacity) { + if (alignment_padding + size > self.available()) return null; break :blk aligned_head; } padded_to_end = self.capacity - self.head; @@ -117,7 +118,9 @@ pub const StagingRing = struct { const new_head = try_offset + size; self.head = if (new_head >= self.capacity) 0 else new_head; - self.frame_used[frame_index] += size + padded_to_end; + const consumed = size + padded_to_end + if (padded_to_end == 0) alignment_padding else 0; + self.frame_used[frame_index] += consumed; + self.used_total += consumed; return slice; } @@ -352,3 +355,40 @@ pub const TransferQueue = struct { try self.submitAndWait(vk_device, queue_mutex); } }; + +test "staging ring distinguishes full from empty" { + var memory: [1024]u8 = undefined; + var ring = StagingRing{ + .mapped = &memory, + .capacity = memory.len, + .frame_used = [_]u64{0} ** rhi.MAX_FRAMES_IN_FLIGHT, + }; + ring.beginFrame(0); + try std.testing.expect(ring.allocate(512, 0) != null); + try std.testing.expect(ring.allocate(512, 0) != null); + try std.testing.expectEqual(@as(u64, memory.len), ring.allocated()); + try std.testing.expect(ring.allocate(1, 0) == null); + ring.reclaimFrame(0); + try std.testing.expectEqual(@as(u64, 0), ring.allocated()); +} + +test "staging ring reclaims wrapped frame regions" { + var memory: [1024]u8 = undefined; + var ring = StagingRing{ + .mapped = &memory, + .capacity = memory.len, + .frame_used = [_]u64{0} ** rhi.MAX_FRAMES_IN_FLIGHT, + }; + ring.beginFrame(0); + try std.testing.expect(ring.allocate(400, 0) != null); + ring.beginFrame(1); + try std.testing.expect(ring.allocate(400, 1) != null); + ring.reclaimFrame(0); + ring.beginFrame(0); + try std.testing.expect(ring.allocate(400, 0) != null); + try std.testing.expectEqual(@as(u64, 1024), ring.allocated()); + ring.reclaimFrame(1); + ring.reclaimFrame(0); + try std.testing.expectEqual(@as(u64, 0), ring.allocated()); + try std.testing.expectEqual(ring.head, ring.tail); +} diff --git a/modules/engine-graphics/src/vulkan/utils.zig b/modules/engine-graphics/src/vulkan/utils.zig index 86c276d5..7a88d4db 100644 --- a/modules/engine-graphics/src/vulkan/utils.zig +++ b/modules/engine-graphics/src/vulkan/utils.zig @@ -8,6 +8,7 @@ pub const VulkanBuffer = struct { buffer: c.VkBuffer = null, memory: c.VkDeviceMemory = null, size: c.VkDeviceSize = 0, + usage: c.VkBufferUsageFlags = 0, is_host_visible: bool = false, mapped_ptr: ?*anyopaque = null, }; @@ -74,7 +75,10 @@ pub fn createVulkanBuffer(device: *const VulkanDevice, size: usize, usage: c.VkB return .{ .buffer = buffer, .memory = memory, - .size = mem_reqs.size, + // Descriptor ranges and copy bounds are expressed in the VkBuffer's + // requested size, not the (possibly alignment-rounded) allocation. + .size = @intCast(size), + .usage = usage, .is_host_visible = is_host_visible, .mapped_ptr = mapped_ptr, }; diff --git a/modules/engine-graphics/src/vulkan_device.zig b/modules/engine-graphics/src/vulkan_device.zig index 324ad6df..32f8f337 100644 --- a/modules/engine-graphics/src/vulkan_device.zig +++ b/modules/engine-graphics/src/vulkan_device.zig @@ -77,6 +77,9 @@ pub const VulkanDevice = struct { max_msaa_samples: u8 = 1, multi_draw_indirect: bool = false, draw_indirect_first_instance: bool = false, + draw_indirect_count: bool = false, + vkCmdDrawIndirectCountKHR: ?*const fn (c.VkCommandBuffer, c.VkBuffer, c.VkDeviceSize, c.VkBuffer, c.VkDeviceSize, u32, u32) callconv(.c) void = null, + vkCmdDrawIndexedIndirectCountKHR: ?*const fn (c.VkCommandBuffer, c.VkBuffer, c.VkDeviceSize, c.VkBuffer, c.VkDeviceSize, u32, u32) callconv(.c) void = null, timestamp_period: f32 = 1.0, pub fn init(allocator: std.mem.Allocator, window: *c.SDL_Window) !VulkanDevice { @@ -293,16 +296,20 @@ pub const VulkanDevice = struct { const robustness2_name: [*:0]const u8 = @ptrCast(c.VK_EXT_ROBUSTNESS_2_EXTENSION_NAME); const device_fault_name: [*:0]const u8 = @ptrCast(c.VK_EXT_DEVICE_FAULT_EXTENSION_NAME); + const indirect_count_name: [*:0]const u8 = @ptrCast(c.VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME); const robustness2_name_slice = std.mem.span(robustness2_name); const device_fault_name_slice = std.mem.span(device_fault_name); + const indirect_count_name_slice = std.mem.span(indirect_count_name); var supports_robustness2 = false; var supports_device_fault = false; + var supports_indirect_count = false; for (ext_props) |prop| { const name: [*:0]const u8 = @ptrCast(&prop.extensionName); const name_slice = std.mem.span(name); if (std.mem.eql(u8, name_slice, robustness2_name_slice)) supports_robustness2 = true; if (std.mem.eql(u8, name_slice, device_fault_name_slice)) supports_device_fault = true; + if (std.mem.eql(u8, name_slice, indirect_count_name_slice)) supports_indirect_count = true; } if (supports_robustness2) log.log.info("VK_EXT_robustness2 supported", .{}); @@ -334,7 +341,7 @@ pub const VulkanDevice = struct { robustness2_features.pNext = if (allow_device_fault) @ptrCast(&fault_features) else null; } - var enabled_extensions: [3][*c]const u8 = undefined; + var enabled_extensions: [4][*c]const u8 = undefined; var enabled_extension_count: u32 = 0; enabled_extensions[enabled_extension_count] = c.VK_KHR_SWAPCHAIN_EXTENSION_NAME; enabled_extension_count += 1; @@ -346,6 +353,10 @@ pub const VulkanDevice = struct { enabled_extensions[enabled_extension_count] = c.VK_EXT_DEVICE_FAULT_EXTENSION_NAME; enabled_extension_count += 1; } + if (supports_indirect_count) { + enabled_extensions[enabled_extension_count] = c.VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME; + enabled_extension_count += 1; + } var device_create_info = std.mem.zeroes(c.VkDeviceCreateInfo); device_create_info.sType = c.VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; @@ -368,6 +379,7 @@ pub const VulkanDevice = struct { device_create_info.pNext = null; enabled_extensions[0] = c.VK_KHR_SWAPCHAIN_EXTENSION_NAME; enabled_extension_count = 1; + supports_indirect_count = false; device_create_info.enabledExtensionCount = enabled_extension_count; device_create_info.ppEnabledExtensionNames = &enabled_extensions; queue_create_count = 1; @@ -387,6 +399,17 @@ pub const VulkanDevice = struct { self.supports_device_fault = false; } } + if (supports_indirect_count and self.vk_device != null) { + const proc = c.vkGetDeviceProcAddr(self.vk_device, "vkCmdDrawIndirectCountKHR"); + if (proc) |function| { + self.vkCmdDrawIndirectCountKHR = @ptrCast(function); + const indexed_proc = c.vkGetDeviceProcAddr(self.vk_device, "vkCmdDrawIndexedIndirectCountKHR"); + if (indexed_proc) |indexed_function| { + self.vkCmdDrawIndexedIndirectCountKHR = @ptrCast(indexed_function); + self.draw_indirect_count = true; + } + } + } return self; } diff --git a/modules/engine-graphics/src/vulkan_swapchain.zig b/modules/engine-graphics/src/vulkan_swapchain.zig index 705e9d31..d87a006e 100644 --- a/modules/engine-graphics/src/vulkan_swapchain.zig +++ b/modules/engine-graphics/src/vulkan_swapchain.zig @@ -101,6 +101,13 @@ pub const VulkanSwapchain = struct { if (self.headless_memory != null) c.vkFreeMemory(vk, self.headless_memory, null); self.headless_image = null; self.headless_memory = null; + + // These are non-owning image handles. Keeping them after destroying a + // headless image leaves index zero pointing at freed Vulkan memory on + // the next recreate, while the freshly-created framebuffer points at + // the newly appended image. In particular, readback must never select + // an image from the previous swapchain generation. + self.clearImageTrackingForRecreate(); } pub fn recreate(self: *VulkanSwapchain, msaa_samples: u8) !void { @@ -277,6 +284,10 @@ pub const VulkanSwapchain = struct { } } + fn clearImageTrackingForRecreate(self: *VulkanSwapchain) void { + self.images.clearRetainingCapacity(); + } + fn createDepthBuffer(self: *VulkanSwapchain, msaa_samples: u8) !void { const depth_format = c.VK_FORMAT_D32_SFLOAT; var depth_image_info = std.mem.zeroes(c.VkImageCreateInfo); @@ -540,6 +551,18 @@ fn presentModeName(mode: c.VkPresentModeKHR) []const u8 { }; } +test "VulkanSwapchain recreation discards prior image handles" { + var swapchain: VulkanSwapchain = undefined; + swapchain.images = .empty; + defer swapchain.images.deinit(std.testing.allocator); + + try swapchain.images.append(std.testing.allocator, null); + try swapchain.images.append(std.testing.allocator, null); + swapchain.clearImageTrackingForRecreate(); + + try std.testing.expectEqual(@as(usize, 0), swapchain.images.items.len); +} + fn checkVk(result: c.VkResult) !void { if (result != c.VK_SUCCESS) return error.BackendError; } diff --git a/modules/engine-rhi/src/culling.zig b/modules/engine-rhi/src/culling.zig index c595ab4d..46a2daed 100644 --- a/modules/engine-rhi/src/culling.zig +++ b/modules/engine-rhi/src/culling.zig @@ -1,4 +1,5 @@ const Mat4 = @import("engine-math").Mat4; +const rhi_types = @import("rhi_types.zig"); pub const ChunkCullData = extern struct { min_point: [4]f32, @@ -55,3 +56,109 @@ 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/rhi.zig b/modules/engine-rhi/src/rhi.zig index d381e915..4c1990f7 100644 --- a/modules/engine-rhi/src/rhi.zig +++ b/modules/engine-rhi/src/rhi.zig @@ -86,6 +86,9 @@ 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; @@ -99,6 +102,7 @@ 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, @@ -429,11 +433,28 @@ 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. The compact instance + /// SSBO is indexed by each command's firstInstance; no CPU count readback + /// or per-draw push constants are used. + pub fn drawCompactLODIndirectCount(self: RenderContext, index_buffer: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32) bool { + 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 { self.encoder.drawIndirect(handle, command_buffer, offset, draw_count, stride); } + /// Issues GPU-generated indirect commands, with the command count read from + /// `count_buffer`. Returns false when the active backend lacks the feature. + 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 { + 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. /// Instance buffers and model data must be populated for the active frame. Must be called from the render thread that owns the backend context. pub fn drawInstance(self: RenderContext, handle: BufferHandle, count: u32, instance_index: u32) void { @@ -462,6 +483,17 @@ pub const RenderContext = struct { 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 { @@ -808,7 +840,10 @@ 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, setViewport: *const fn (ptr: *anyopaque, width: u32, height: u32) void, }; @@ -843,11 +878,20 @@ 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 { self.vtable.drawIndirect(self.ptr, handle, command_buffer, offset, draw_count, stride); } + pub fn drawIndirectCount(self: IGraphicsCommandEncoder, handle: BufferHandle, command_buffer: BufferHandle, offset: usize, count_buffer: BufferHandle, count_offset: usize, max_draw_count: u32, stride: u32) bool { + return self.vtable.drawIndirectCount(self.ptr, handle, command_buffer, offset, count_buffer, count_offset, max_draw_count, stride); + } /// Issues an instanced draw using currently bound per-instance state. /// Instance buffers and model data must be populated for the active frame. Must be called from the render thread that owns the backend context. pub fn drawInstance(self: IGraphicsCommandEncoder, handle: BufferHandle, count: u32, instance_index: u32) void { @@ -868,6 +912,9 @@ pub const IRenderStateContext = struct { setModelMatrix: *const fn (ptr: *anyopaque, model: Mat4, color: Vec3, mask_radius: f32) 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, @@ -889,6 +936,18 @@ pub const IRenderStateContext = struct { 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 { @@ -1307,6 +1366,10 @@ pub const IDeviceQuery = struct { pub const VTable = 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, @@ -1327,6 +1390,15 @@ pub const IDeviceQuery = struct { pub fn supportsIndirectFirstInstance(self: IDeviceQuery) bool { return self.vtable.supportsIndirectFirstInstance(self.ptr); } + /// Reports whether GPU-generated indirect command counts are supported. + 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 { @@ -1562,6 +1634,7 @@ 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. @@ -1569,6 +1642,9 @@ 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 { @@ -1833,6 +1909,10 @@ 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 e9a869d9..9d7139e8 100644 --- a/modules/engine-rhi/src/rhi_contract_tests.zig +++ b/modules/engine-rhi/src/rhi_contract_tests.zig @@ -500,6 +500,10 @@ test "RHI contract declares 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")); } @@ -659,6 +663,10 @@ test "RHI contract declares IDeviceQuery.getFrameIndex" { 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 bfcb3494..8c815a60 100644 --- a/modules/engine-rhi/src/rhi_types.zig +++ b/modules/engine-rhi/src/rhi_types.zig @@ -243,6 +243,173 @@ 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, @@ -390,6 +557,19 @@ 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 e86bd523..c14392dc 100644 --- a/modules/engine-rhi/src/root.zig +++ b/modules/engine-rhi/src/root.zig @@ -39,7 +39,12 @@ 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; @@ -94,6 +99,11 @@ 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 7ea1edce..ec779b77 100644 --- a/modules/engine-rhi/src/world_contracts.zig +++ b/modules/engine-rhi/src/world_contracts.zig @@ -25,11 +25,16 @@ 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, }; + 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, render_lod: bool) void { self.vtable.render(self.ptr, view_proj, camera_pos, render_lod); } diff --git a/modules/engine-ui/src/root.zig b/modules/engine-ui/src/root.zig index 619270d5..2ea73419 100644 --- a/modules/engine-ui/src/root.zig +++ b/modules/engine-ui/src/root.zig @@ -26,6 +26,9 @@ 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; diff --git a/modules/engine-ui/src/timing_overlay.zig b/modules/engine-ui/src/timing_overlay.zig index 7b77b6c6..b016adbe 100644 --- a/modules/engine-ui/src/timing_overlay.zig +++ b/modules/engine-ui/src/timing_overlay.zig @@ -7,6 +7,8 @@ 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, @@ -23,11 +25,11 @@ pub const TimingOverlay = struct { comptime std.debug.assert(rhi.SHADOW_CASCADE_COUNT >= 3); - var num_lines: f32 = 2 + 1 + 13 + 1 + 6 + 1 + 4 + 1; + var num_lines: f32 = 2 + 1 + 15 + 1 + 6 + 1 + 4 + 1; if (data.world) |ws| { num_lines += 1 + 6; if (ws.lod != null) { - num_lines += 1 + 6; + num_lines += 1 + 8; } } const padding: f32 = 25; @@ -61,6 +63,9 @@ 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); @@ -84,6 +89,8 @@ pub const TimingOverlay = struct { 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)); } } @@ -134,6 +141,24 @@ 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); @@ -166,10 +191,108 @@ 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 2a899100..d0b4b9fa 100644 --- a/modules/game-core/src/benchmark.zig +++ b/modules/game-core/src/benchmark.zig @@ -5,6 +5,10 @@ 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, @@ -12,6 +16,39 @@ pub const Waypoint = struct { 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 Scenario = enum { + stationary, + traversal, + rapid_turn, + teleport_eviction, + + pub fn parse(value: []const u8) !Scenario { + if (std.mem.eql(u8, value, "stationary")) return .stationary; + if (std.mem.eql(u8, value, "traversal")) return .traversal; + if (std.mem.eql(u8, value, "rapid-turn")) return .rapid_turn; + if (std.mem.eql(u8, value, "teleport-eviction")) return .teleport_eviction; + return error.InvalidBenchmarkScenario; + } + + pub fn name(self: Scenario) []const u8 { + return switch (self) { + .stationary => "stationary", + .traversal => "traversal", + .rapid_turn => "rapid-turn", + .teleport_eviction => "teleport-eviction", + }; + } +}; + +const Pose = struct { + pos: Vec3, + look: Vec3, +}; + pub const BENCH_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 }, @@ -21,16 +58,97 @@ pub const BENCH_PATH = [_]Waypoint{ .{ .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 }, + .{ .pos = Vec3.init(-1_536, 120, 1_024), .look = Vec3.init(0, -0.1, -1), .duration = 4.0 }, + .{ .pos = Vec3.init(-1_024, 160, -1_536), .look = Vec3.init(1, -0.3, 0.2), .duration = 4.0 }, + .{ .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 { @@ -55,49 +173,374 @@ 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, +}; + +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 BenchmarkResults = struct { + schema_version: u32 = BENCHMARK_SCHEMA_VERSION, + artifact_type: []const u8 = "benchmark-result", preset: []const u8, + scenario: []const u8, + world_seed: u64, + build: BuildMetadata, + provenance: BenchmarkProvenance = .{}, render_distance: i32, + horizon_distance: i32, gpu_memory_mb_avg: f64, gpu_memory_mb_max: f64, frames: u32, duration_s: f32, fps: Summary, + frame_ms: Summary, max_frame_ms: f64, cpu_ms_avg: f64, gpu_ms: GpuSummary, draw_calls_avg: f64, 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 = .{}, +}; + +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, +}; +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 BenchmarkRunner = struct { allocator: std.mem.Allocator, 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, + 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, render_distance: i32, duration_s: f32, output_path: []const u8) !BenchmarkRunner { + 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 { 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, + }, + .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)))); @@ -110,7 +553,7 @@ pub const BenchmarkRunner = struct { } pub fn applyPose(self: *const BenchmarkRunner, player: *Player) void { - const pose = poseAtTime(@max(self.elapsed_s - self.warmup_s, 0.0)); + const pose = poseAtTime(self.scenario, self.scenario_elapsed_s); player.fly_mode = true; player.can_fly = true; player.noclip = true; @@ -124,6 +567,42 @@ 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 { 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; + return; + } if (self.elapsed_s < self.warmup_s) return; const shadow_avg = averageArray(&gpu.shadow_pass_ms); @@ -136,17 +615,24 @@ pub const BenchmarkRunner = struct { .fps = frame_fps, .gpu_shadow_ms = shadow_avg, .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, .gpu_memory_mb = gpu_memory_mb, + .lod = lod, }); self.sampled_s += dt; + self.scenario_elapsed_s += dt; } pub fn isComplete(self: *const BenchmarkRunner) bool { - return self.sampled_s >= self.duration_s or wallElapsedSeconds(self) >= self.duration_s + self.warmup_s + 30.0; + return self.sampled_s >= self.duration_s or self.warmup_timed_out or wallElapsedSeconds(self) >= self.duration_s + self.warmup_s + 90.0; } pub fn writeResults(self: *const BenchmarkRunner) !void { @@ -168,10 +654,31 @@ pub const BenchmarkRunner = struct { 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); 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 draw_sum: f64 = 0; var vertices_sum: f64 = 0; @@ -179,22 +686,159 @@ pub const BenchmarkRunner = struct { 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, + }; - for (self.samples.items) |sample| { + for (self.samples.items, 0..) |sample, index| { cpu_sum += sample.cpu_ms; - max_frame_ms = @max(max_frame_ms, 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; 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); } 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; @@ -202,22 +846,111 @@ pub const BenchmarkRunner = struct { return .{ .preset = self.preset, + .scenario = self.scenario.name(), + .world_seed = self.world_seed, + .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, .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), }, .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 }, }; } @@ -229,6 +962,20 @@ pub const BenchmarkRunner = struct { 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 }); } @@ -238,11 +985,61 @@ 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 }; @@ -271,7 +1068,33 @@ fn thresholdsForResults(results: BenchmarkResults) SloThresholds { return thresholds; } -fn enforceSlo(results: BenchmarkResults) !void { +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; @@ -298,23 +1121,95 @@ fn enforceSlo(results: BenchmarkResults) !void { 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, .total_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); @@ -323,10 +1218,738 @@ test "benchmark draw-call SLO scales for render-distance override" { 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); +} + +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)); +} + +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)); +} + +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 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 }; +} + +/// 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 .{}; + }; + 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; + } + } + return .{ .name = name, .ms = ms }; +} + 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 }; @@ -366,6 +1989,14 @@ fn percentile(sorted: []const f32, p: f64) f64 { 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 averageArray(values: []const f32) f32 { if (values.len == 0) return 0; @@ -374,18 +2005,35 @@ fn averageArray(values: []const f32) f32 { return sum / @as(f32, @floatFromInt(values.len)); } -fn poseAtTime(time_s: f32) struct { pos: Vec3, look: Vec3 } { - const total = pathDuration(); +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 poseAtWaypoints(path: []const Waypoint, time_s: f32, interpolate: bool) Pose { + const total = pathDuration(path); if (total <= 0.0) { - return .{ .pos = BENCH_PATH[0].pos, .look = BENCH_PATH[0].look.normalize() }; + 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 (BENCH_PATH, 0..) |waypoint, i| { - const next = BENCH_PATH[(i + 1) % BENCH_PATH.len]; - if (t <= waypoint.duration or i == BENCH_PATH.len - 1) { + 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); @@ -397,12 +2045,12 @@ fn poseAtTime(time_s: f32) struct { pos: Vec3, look: Vec3 } { t -= waypoint.duration; } - return .{ .pos = BENCH_PATH[0].pos, .look = BENCH_PATH[0].look.normalize() }; + return .{ .pos = path[0].pos, .look = path[0].look.normalize() }; } -fn pathDuration() f32 { +fn pathDuration(path: []const Waypoint) f32 { var total: f32 = 0; - for (BENCH_PATH) |waypoint| total += waypoint.duration; + for (path) |waypoint| total += waypoint.duration; return total; } diff --git a/modules/game-core/src/root.zig b/modules/game-core/src/root.zig index 991aa772..ac6835f9 100644 --- a/modules/game-core/src/root.zig +++ b/modules/game-core/src/root.zig @@ -19,8 +19,10 @@ pub const session_hud = @import("ui/session_hud.zig"); pub const BLOCK_TEXTURE_DEFINITIONS = block_texture_definitions.BLOCK_TEXTURE_DEFINITIONS; 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 7e68b776..78cf2842 100644 --- a/modules/game-core/src/session.zig +++ b/modules/game-core/src/session.zig @@ -39,14 +39,32 @@ 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; @@ -91,10 +109,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, generator_index: usize, render_distance_preset: RenderDistancePreset, 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(); @@ -139,7 +172,7 @@ pub const GameSession = struct { } } - const lod_config = if (strict_safe_mode) + var lod_config = if (strict_safe_mode) LODConfig{ .chunk_render_radius = chunk_render_radius, .radii = preset_radii, @@ -163,6 +196,11 @@ pub const GameSession = struct { .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; @@ -215,6 +253,11 @@ 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.* = .{ @@ -340,9 +383,94 @@ 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); } @@ -360,8 +488,212 @@ 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-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, @@ -369,6 +701,96 @@ 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, "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 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(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); @@ -379,6 +801,9 @@ 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-ui/src/screens/world.zig b/modules/game-ui/src/screens/world.zig index b4fc8a1b..52fe472e 100644 --- a/modules/game-ui/src/screens/world.zig +++ b/modules/game-ui/src/screens/world.zig @@ -84,6 +84,11 @@ pub const WorldScreen = struct { } pub fn initMenuPreview(allocator: std.mem.Allocator, context: EngineContext, seed: u64, generator_index: usize) !*WorldScreen { + // The rotating menu preview is intentionally bounded to full-detail + // chunks. It does not need the distant hierarchy, and continuously + // streaming compact tiles underneath a retained RmlUi overlay can + // trigger a RADV command-stream rejection on RDNA1. Normal worlds keep + // the user's LOD setting and the complete compact pipeline. return initWithDistance( allocator, context, @@ -91,7 +96,7 @@ pub const WorldScreen = struct { generator_index, context.settings.render_distance, context.settings.horizon_distance, - context.settings.lod_enabled, + false, true, ); } @@ -676,10 +681,102 @@ pub const WorldScreen = struct { 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 .{ @@ -690,6 +787,7 @@ 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, }; } diff --git a/modules/world-core/src/lod_data.zig b/modules/world-core/src/lod_data.zig index a1921e1d..87b719e5 100644 --- a/modules/world-core/src/lod_data.zig +++ b/modules/world-core/src/lod_data.zig @@ -151,8 +151,44 @@ pub const LODSimplifiedData = struct { 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 { - const grid_size = getGridSize(lod_level); + 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); @@ -203,7 +239,18 @@ pub const LODSimplifiedData = struct { } pub fn initWithVerticalSpans(allocator: std.mem.Allocator, lod_level: LODLevel) !LODSimplifiedData { - var data = try init(allocator, lod_level); + 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; @@ -468,6 +515,37 @@ pub const LODSimplifiedData = struct { 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" { @@ -483,12 +561,45 @@ test "LODSimplifiedData initializes rich column defaults" { try std.testing.expectEqual(@as(f32, 0.0), data.vegetation[0].tree_coverage); } -test "LODSimplifiedData far levels use high-detail grids" { +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, 129), LODSimplifiedData.getGridSize(.lod4)); + 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" { diff --git a/modules/world-lod/src/lod_cache.zig b/modules/world-lod/src/lod_cache.zig index 7030ac82..c3c96305 100644 --- a/modules/world-lod/src/lod_cache.zig +++ b/modules/world-lod/src/lod_cache.zig @@ -14,7 +14,8 @@ const BlockType = world_core.BlockType; const BiomeId = world_core.BiomeId; pub const MAGIC: u32 = 0x5A4C4F44; // "ZLOD" -pub const CACHE_VERSION: u8 = 10; +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; @@ -36,6 +37,8 @@ pub const CacheError = error{ InvalidBiome, InvalidBlock, InvalidSpanCount, + InvalidProvenance, + MissingProvenance, ChecksumMismatch, }; @@ -70,15 +73,39 @@ fn payloadSize(count: usize) usize { 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; + var total = HEADER_SIZE + payloadSize(count) + SPAN_FLAGS_WIRE_SIZE + count; if (data.hasVerticalSpans()) { - total += count; // provenance bytes 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); } @@ -277,13 +304,13 @@ pub fn serialize(data: *const LODSimplifiedData, key: Key, allocator: std.mem.Al 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.?; - for (data.provenance) |provenance| { - buf[off] = @intFromEnum(provenance); - off += 1; - } @memcpy(buf[off..][0..count], counts); off += count; for (spans) |span| { @@ -306,7 +333,7 @@ pub fn deserialize(bytes: []const u8, key: Key, allocator: std.mem.Allocator) !L off += 4; const version = bytes[off]; - if (version != CACHE_VERSION and version != CACHE_VERSION_V1) return CacheError.UnsupportedVersion; + 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; @@ -330,7 +357,7 @@ pub fn deserialize(bytes: []const u8, key: Key, allocator: std.mem.Allocator) !L 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 (width != LODSimplifiedData.getGridSize(key.lod)) return CacheError.InvalidWidth; + 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); @@ -338,15 +365,27 @@ pub fn deserialize(bytes: []const u8, key: Key, allocator: std.mem.Allocator) !L var has_spans = false; var expected = v1_expected; - if (version == CACHE_VERSION) { + 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 (has_spans) expected += count + count + count * world_core.MAX_LOD_VERTICAL_SPANS * SPAN_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; - var data = if (has_spans) try LODSimplifiedData.initWithVerticalSpans(allocator, key.lod) else try LODSimplifiedData.init(allocator, key.lod); + // 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| { @@ -382,21 +421,23 @@ pub fn deserialize(bytes: []const u8, key: Key, allocator: std.mem.Allocator) !L off += 18; } - if (version == CACHE_VERSION) { + 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 (has_spans) { + 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.InvalidSpanCount, + else => return CacheError.InvalidProvenance, }; off += 1; } + } + if (has_spans) { const counts = data.vertical_span_counts.?; @memcpy(counts, bytes[off..][0..count]); off += count; @@ -461,6 +502,24 @@ test "LOD cache round-trip preserves source data" { 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(); @@ -498,18 +557,18 @@ test "LOD cache round-trip preserves vertical spans" { 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(data.vertical_spans.?.len, decoded.vertical_spans.?.len); - try testing.expectEqualSlices(u8, std.mem.sliceAsBytes(data.vertical_spans.?), std.mem.sliceAsBytes(decoded.vertical_spans.?)); + try testing.expectEqual(lower, decoded.getVerticalSpan(2, 3, 0).?); + try testing.expectEqual(upper, decoded.getVerticalSpan(2, 3, 1).?); } -test "LOD cache round-trip preserves column provenance" { - var data = try LODSimplifiedData.initWithVerticalSpans(testing.allocator, .lod1); +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 = .lod1 }; + 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); @@ -521,7 +580,7 @@ test "LOD cache round-trip preserves column provenance" { try testing.expectEqual(ColumnProvenance.worldgen, decoded.getColumnProvenance(0, 0)); } -test "LOD cache accepts v1 payload as span-less data" { +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); @@ -537,13 +596,27 @@ test "LOD cache accepts v1 payload as span-less data" { std.mem.writeInt(u32, v1_bytes[6..][0..4], 0, .little); std.mem.writeInt(u32, v1_bytes[6..][0..4], computeCrc(v1_bytes), .little); - var decoded = try deserialize(v1_bytes, key, testing.allocator); - defer decoded.deinit(); + try testing.expectError(CacheError.MissingProvenance, deserialize(v1_bytes, key, testing.allocator)); +} - try testing.expect(!decoded.hasVerticalSpans()); - const idx = 1 + data.width; - try testing.expectEqual(data.heightmap[idx], decoded.heightmap[idx]); - try testing.expectEqual(data.biomes[idx], decoded.biomes[idx]); +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" { @@ -558,6 +631,22 @@ test "LOD cache rejects checksum mismatch" { 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(); @@ -585,4 +674,9 @@ test "LOD cache checksum covers key header fields" { 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 new file mode 100644 index 00000000..3e11a420 --- /dev/null +++ b/modules/world-lod/src/lod_cache_io.zig @@ -0,0 +1,296 @@ +//! 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 index 0befea04..6b335687 100644 --- a/modules/world-lod/src/lod_chunk.zig +++ b/modules/world-lod/src/lod_chunk.zig @@ -161,6 +161,11 @@ pub const LODChunk = struct { /// 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) @@ -192,6 +197,22 @@ pub const LODChunk = struct { /// 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 { @@ -202,6 +223,7 @@ pub const LODChunk = struct { .state = .missing, .job_token = 0, .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, @@ -210,6 +232,12 @@ pub const LODChunk = struct { .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, }; } @@ -243,6 +271,18 @@ pub const LODChunk = struct { 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 { @@ -341,6 +381,15 @@ pub const LODChunk = struct { 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. @@ -437,6 +486,7 @@ pub const ILODConfig = struct { 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, getVerticalSpanBudget: *const fn (ptr: *anyopaque) u8, getMeshPath: *const fn (ptr: *anyopaque) LODMeshPath, getFogStartPercent: *const fn (ptr: *anyopaque, lod: LODLevel) f32, @@ -519,6 +569,11 @@ pub const ILODConfig = struct { 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 the maximum number of vertical spans retained per LOD column. /// Implementations clamp this to the storage capacity of `LODSimplifiedData`. @@ -591,6 +646,11 @@ pub const LODConfig = struct { 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 }, + vertical_span_budget: u8 = 4, mesh_path: LODMeshPath = .column_spans, @@ -712,6 +772,7 @@ pub const LODConfig = struct { .getQEMTarget = getQEMTargetWrapper, .getQEMMinInputTriangles = getQEMMinInputTrianglesWrapper, .getHorizontalDetail = getHorizontalDetailWrapper, + .getSampleDensity = getSampleDensityWrapper, .getVerticalSpanBudget = getVerticalSpanBudgetWrapper, .getMeshPath = getMeshPathWrapper, .getFogStartPercent = getFogStartPercentWrapper, @@ -779,6 +840,10 @@ pub const LODConfig = struct { 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 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))); @@ -850,6 +915,16 @@ test "LODConfig distance calculation" { 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 }, diff --git a/modules/world-lod/src/lod_compact_pool.zig b/modules/world-lod/src/lod_compact_pool.zig new file mode 100644 index 00000000..2b8fe546 --- /dev/null +++ b/modules/world-lod/src/lod_compact_pool.zig @@ -0,0 +1,290 @@ +//! 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_geometry.zig b/modules/world-lod/src/lod_geometry.zig index d6c5a905..4c43c92f 100644 --- a/modules/world-lod/src/lod_geometry.zig +++ b/modules/world-lod/src/lod_geometry.zig @@ -205,6 +205,9 @@ 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. @@ -1041,11 +1044,17 @@ pub fn isLeafBlock(block: BlockType) bool { /// 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)]; - return representativeVegetation(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 uses the strongest sample while average height is averaged across non-empty tree samples. +/// 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); @@ -1060,9 +1069,11 @@ pub fn representativeVegetation(data: *const LODSimplifiedData, gx: u32, gz: u32 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; @@ -1071,6 +1082,7 @@ pub fn representativeVegetation(data: *const LODSimplifiedData, gx: u32, gz: u32 } 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; } diff --git a/modules/world-lod/src/lod_ingest.zig b/modules/world-lod/src/lod_ingest.zig index 897ac892..be8e0d44 100644 --- a/modules/world-lod/src/lod_ingest.zig +++ b/modules/world-lod/src/lod_ingest.zig @@ -575,23 +575,23 @@ test "writeIngestedColumn respects provenance authority" { // 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); + 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); + 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); + 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); + 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)); diff --git a/modules/world-lod/src/lod_manager.zig b/modules/world-lod/src/lod_manager.zig index d5cc6b4a..c4e48a59 100644 --- a/modules/world-lod/src/lod_manager.zig +++ b/modules/world-lod/src/lod_manager.zig @@ -26,6 +26,7 @@ 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; @@ -72,15 +73,15 @@ 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 GenerationCandidate = lod_manager_context.GenerationCandidate; -const MeshCandidate = lod_manager_context.MeshCandidate; -const UploadCandidate = lod_manager_context.UploadCandidate; +const LifecycleQueue = lod_manager_context.LifecycleQueue; pub const ChunkResolver = lod_manager_context.ChunkResolver; const MAX_LOD_REGIONS = lod_manager_context.MAX_LOD_REGIONS; @@ -94,6 +95,8 @@ const LODTransition = struct { 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 = .{}, }; @@ -126,6 +129,9 @@ pub const LODMeshDisposalQueue = struct { 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, }; @@ -166,8 +172,10 @@ pub const LODManager = struct { // 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, @@ -198,6 +206,11 @@ pub const LODManager = struct { // 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, @@ -207,12 +220,20 @@ pub const LODManager = struct { // update() once the regions appear. ingestion_queue: LODIngestionQueue, - // Reused per-update scratch buffers for candidate sorting. These used to - // allocate/free on every LOD tick; retaining capacity removes steady-state - // allocator churn at high render distances. - generation_candidates_scratch: std.ArrayListUnmanaged(GenerationCandidate) = .empty, - mesh_candidates_scratch: std.ArrayListUnmanaged(MeshCandidate) = .empty, - upload_candidates_scratch: std.ArrayListUnmanaged(UploadCandidate) = .empty, + // 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; @@ -228,7 +249,7 @@ pub const LODManager = struct { /// 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 { + pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []const u8) !Self { return lod_manager_cache_ops.initCacheTestManager(allocator, cache_dir_path); } @@ -256,6 +277,13 @@ pub const LODManager = struct { 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 { @@ -292,6 +320,17 @@ pub const LODManager = struct { 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, layer: LODRenderLayer) void { + return lod_manager_core.renderFrame(self, frame_serial, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, 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) void { + return lod_manager_core.prepareFrame(self, frame_serial, view_proj, camera_pos, chunk_checker, checker_ctx, max_distance_chunks); + } + /// 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 { @@ -304,6 +343,23 @@ pub const LODManager = struct { return lod_manager_cache_ops.flushDirtyStores(self); } + /// Explicitly waits for accepted cache I/O and applies its completions. + /// This is for shutdown/tests; frame updates never block on cache I/O. + 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 { @@ -382,6 +438,12 @@ pub const LODManager = struct { 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 { @@ -391,18 +453,23 @@ pub const LODManager = struct { /// 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 expires or applies. + /// 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); } @@ -418,14 +485,14 @@ pub const LODManager = struct { return lod_manager_ingestion_ops.recordPendingLocked(self, cx, cz, provenance, mask); } - /// Refreshes an existing deferred ingestion request with a new mask and time-to-live. - /// Used when chunk edits arrive faster than regions can be regenerated. + /// 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); } - /// Decrements pending-ingestion TTL counters while the ingestion mutex is held. - /// Expired requests are dropped to avoid unbounded retry queues. + /// 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); } @@ -460,6 +527,17 @@ pub const LODManager = struct { 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 { @@ -490,12 +568,29 @@ pub const LODManager = struct { 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 { diff --git a/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig b/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig new file mode 100644 index 00000000..4c66fa66 --- /dev/null +++ b/modules/world-lod/src/lod_manager_benchmark_fixture_ops.zig @@ -0,0 +1,131 @@ +//! 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 index e6672605..e2af6f30 100644 --- a/modules/world-lod/src/lod_manager_cache_ops.zig +++ b/modules/world-lod/src/lod_manager_cache_ops.zig @@ -1,70 +1,18 @@ const std = @import("std"); -const Self = @import("lod_manager.zig").LODManager; const fs = @import("fs"); +const Self = @import("lod_manager.zig").LODManager; +const LODRegionKey = @import("lod_chunk.zig").LODRegionKey; +const LODSimplifiedData = @import("lod_chunk.zig").LODSimplifiedData; 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 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); @@ -73,115 +21,177 @@ pub fn enableCache(self: *Self, save_dir_path: []const u8) !void { .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) { - // Seed mismatch => the entire store is foreign; discard all. - log.log.warn("LOD store seed mismatch; discarding foreign LOD source store", .{}); - try lod_store.deleteStore(self.allocator, save_dir_path); - } else if (stored_header.lod_data_version != live_header.lod_data_version) { - log.log.warn("LOD store data version changed; discarding stale LOD source store", .{}); - try lod_store.deleteStore(self.allocator, save_dir_path); - } else if (stored_header.generator_identity_hash != live_header.generator_identity_hash or + 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) { - // Generator changed but seed is the same: worldgen-sampled LOD - // is stale, but chunk-derived columns reflect real saved chunks - // and remain valid. Cached payloads are keyed by generator - // identity/version, so the old data is naturally ignored on - // read (cache miss) and regenerated with the new generator; - // `setGeneratedColumn` is provenance-aware, so regeneration - // preserves any chunk_derived/edited columns. We still delete - // the orphaned old-keyed store for disk hygiene. - log.log.warn("LOD store generator mismatch; regenerating stale worldgen LOD (chunk-derived data re-ingested from saved chunks)", .{}); + 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); - } + 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 { - const save_dir_path = self.cacheDirPathSnapshot() orelse return; - defer self.allocator.free(save_dir_path); + _ = queueDirtyStores(self, 1); +} - var write_key: ?LODRegionKey = null; - var write_bytes: ?[]u8 = null; +pub fn flushCacheIO(self: *Self) void { + self.cache_io.waitUntilIdle(); + drainCacheCompletions(self); +} - { - self.mutex.lock(); - defer self.mutex.unlock(); +/// 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 { + var attempts: usize = 0; + while (attempts < 2048) : (attempts += 1) { + const queued = queueDirtyStores(self, cache_io.MAX_PENDING_TASKS); + if (queued == 0) break; + self.cache_io.waitUntilIdle(); + drainCacheCompletions(self); + } + self.flushCacheIO(); +} - const active = lod_chunk.activeLODCount(self.config); - var i: usize = 1; - while (i < active) : (i += 1) { - var it = self.regions[i].iterator(); - while (it.next()) |entry| { - const lcp = entry.value_ptr.*; - if (!lcp.store_dirty) continue; - switch (lcp.data) { - .simplified => |*data| { - const key = LODRegionKey{ .rx = lcp.region_x, .rz = lcp.region_z, .lod = lcp.lodLevel() }; - const cache_key = self.cacheKey(key); - const bytes = lod_cache.serialize(data, cache_key, self.allocator) catch |err| { - log.log.warn("Failed to serialize LOD{} cache ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - return; - }; - lcp.store_dirty = false; - write_key = key; - write_bytes = bytes; - }, - else => {}, +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; + }, + } } - break; // one region per frame keeps frame cost bounded } - if (write_bytes != null) break; + 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; +} - const key = write_key orelse return; - const bytes = write_bytes orelse return; - defer self.allocator.free(bytes); - - const cache_key = self.cacheKey(key); - self.writeStorePayload(save_dir_path, cache_key, bytes) catch |err| { - log.log.warn("Failed to write LOD{} store ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - self.mutex.lock(); - if (self.regions[@intFromEnum(key.lod)].get(key)) |chunk| { - chunk.store_dirty = true; - } - self.mutex.unlock(); - }; +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, - }; + 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 { - const filename = try std.fmt.allocPrint( - self.allocator, - "lod_{}_{}_{}_{}_{}_{}.dat", - .{ key.seed, key.generator_identity_hash, key.generator_version, key.rx, key.rz, @intFromEnum(key.lod) }, - ); - defer self.allocator.free(filename); - return std.fs.path.join(self.allocator, &.{ save_dir_path, "lod_cache", filename }); + 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 { @@ -195,9 +205,8 @@ pub fn logLegacyCacheNotice(self: *Self) void { pub fn cacheDirPathSnapshot(self: *Self) ?[]u8 { self.mutex.lockShared(); defer self.mutex.unlockShared(); - - const cache_dir_path = self.cache_store.cache_dir_path orelse return null; - return self.allocator.dupe(u8, cache_dir_path) catch |err| { + 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; }; @@ -209,6 +218,8 @@ pub fn cacheEnabled(self: *Self) bool { 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(); @@ -216,12 +227,9 @@ pub fn readStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_c } pub fn writeStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod_cache.Key, bytes: []const u8) !void { - self.mutex.lockShared(); - const store_size_cap_mb = self.config.getLODStoreSizeCapMB(); - self.mutex.unlockShared(); - 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); } @@ -234,27 +242,21 @@ pub fn deleteStorePayload(self: *Self, save_dir_path: []const u8, cache_key: lod 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 |delete_err| { - if (delete_err != error.FileNotFound) { - log.log.warn("Failed to delete corrupt LOD store container '{s}': {}", .{ path, delete_err }); - } + 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 save_dir_path = self.cacheDirPathSnapshot() orelse return null; - defer self.allocator.free(save_dir_path); - + const path = self.cacheDirPathSnapshot() orelse return null; + defer self.allocator.free(path); const cache_key = self.cacheKey(key); - - if (self.readStorePayload(save_dir_path, cache_key) catch |err| switch (err) { + if (self.readStorePayload(path, cache_key) catch |err| switch (err) { lod_store.StoreError.CorruptContainer => { - const path = lod_store.containerPath(self.allocator, save_dir_path, cache_key) catch null; - if (path) |container_path| { - defer self.allocator.free(container_path); - log.log.warn("Discarding corrupt LOD store container '{s}'", .{container_path}); - self.deleteStoreContainer(container_path); - } + 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 => { @@ -265,33 +267,25 @@ pub fn loadCachedSourceData(self: *Self, key: LODRegionKey) ?LODSimplifiedData { 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(save_dir_path, cache_key); + self.deleteStorePayload(path, cache_key); return null; }; } - - const path = self.legacyCacheFilePath(save_dir_path, cache_key) catch |err| { - log.log.warn("LOD legacy cache path allocation failed: {}", .{err}); - return null; - }; - defer self.allocator.free(path); - - const bytes = fs.cwd().readFileAlloc(path, self.allocator, 16 * 1024 * 1024) catch |err| switch (err) { + 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}': {}", .{ path, err }); + 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}': {}", .{ path, err }); - fs.cwd().deleteFile(path) catch |delete_err| { - if (delete_err != error.FileNotFound) { - log.log.warn("Failed to delete corrupt legacy LOD cache '{s}': {}", .{ path, delete_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; }; @@ -309,26 +303,26 @@ pub fn recordCacheMiss(self: *Self) void { 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 save_dir_path = self.cacheDirPathSnapshot() orelse return; - defer self.allocator.free(save_dir_path); - - const cache_key = self.cacheKey(key); - - const bytes = lod_cache.serialize(data, cache_key, self.allocator) catch |err| { - log.log.warn("Failed to serialize LOD{} cache ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); + 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; }; - defer self.allocator.free(bytes); - - self.writeStorePayload(save_dir_path, cache_key, bytes) catch |err| { - log.log.warn("Failed to write LOD{} store ({}, {}): {}", .{ @intFromEnum(key.lod), key.rx, key.rz, err }); - }; + 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 { +pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []const u8) !Self { _ = cache_dir_path; - return .{ + var manager = Self{ .allocator = allocator, .config = undefined, .regions = undefined, @@ -339,18 +333,13 @@ pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []cons .player_cx = std.atomic.Value(i32).init(0), .player_cz = std.atomic.Value(i32).init(0), .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, - }, + .generator = .{ .ptr = undefined, .generate_heightmap_only = undefined, .maybe_recenter_cache = undefined, .seed = 42, .identity_hash = 99, .version = 7 }, .atlas = undefined, .paused = false, .memory_governor = .{}, @@ -358,7 +347,20 @@ pub fn initCacheTestManager(allocator: std.mem.Allocator, cache_dir_path: []cons .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 index 89c33ff7..3d29a0e9 100644 --- a/modules/world-lod/src/lod_manager_context.zig +++ b/modules/world-lod/src/lod_manager_context.zig @@ -1,5 +1,6 @@ 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; @@ -38,7 +39,10 @@ pub const PendingIngestion = struct { cz: i32, provenance: LODColumnProvenance, pending_levels: u8, - ttl: u16, + /// 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 { @@ -67,6 +71,93 @@ pub const UploadCandidate = struct { 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, @@ -82,6 +173,10 @@ pub const ChunkResolver = struct { }; 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; @@ -98,9 +193,14 @@ 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 PENDING_INGESTION_TTL: u16 = 240; 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; @@ -109,9 +209,9 @@ pub fn lodUploadBudgetBytes() 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, uploads: u32) bool { +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 or uploads == 0) return false; + if (pending_bytes == 0) return false; if (uploaded_bytes >= budget_bytes) return true; return pending_bytes > budget_bytes - uploaded_bytes; } @@ -123,6 +223,64 @@ pub fn isUploadPressureError(err: anyerror) bool { }; } +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 index 082f0a1e..268b6891 100644 --- a/modules/world-lod/src/lod_manager_core_ops.zig +++ b/modules/world-lod/src/lod_manager_core_ops.zig @@ -1,4 +1,5 @@ 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"); @@ -65,6 +66,7 @@ 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); @@ -132,8 +134,10 @@ pub fn init(allocator: std.mem.Allocator, config: ILODConfig, gpu_bridge: LODGPU .player_cx = std.atomic.Value(i32).init(0), .player_cz = std.atomic.Value(i32).init(0), .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, @@ -144,10 +148,14 @@ pub fn init(allocator: std.mem.Allocator, config: ILODConfig, gpu_bridge: LODGPU .mesh_disposal = .{}, .renderer = render_iface, .cleanup_covered_regions = true, - .cache_store = .{}, + .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 lod_worker_count = std.math.clamp(cpu_count / 2, MIN_LOD_WORKERS, MAX_LOD_WORKERS); @@ -172,6 +180,17 @@ pub fn deinit(self: *Self) void { // 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(); @@ -183,6 +202,17 @@ pub fn deinit(self: *Self) void { 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]); @@ -191,6 +221,7 @@ pub fn deinit(self: *Self) void { // 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.*); } @@ -216,9 +247,9 @@ pub fn deinit(self: *Self) void { } self.ingestion_queue.deinit(self.allocator); - self.generation_candidates_scratch.deinit(self.allocator); - self.mesh_candidates_scratch.deinit(self.allocator); - self.upload_candidates_scratch.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. @@ -228,11 +259,27 @@ pub fn deinit(self: *Self) void { /// 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; - // Deferred deletion handling. LOD meshes do not carry frame fences yet, - // so destruction still waits for idle, but the bounded sweep prevents - // unreachable GPU/CPU resources from accumulating through long sessions. + // 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); @@ -243,7 +290,14 @@ pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checke 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 @@ -280,6 +334,7 @@ pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checke // Queue a small horizon seed first so something appears quickly, then // let LOD0/LOD1/LOD2 refinements replace the coarse fallback. + 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); @@ -287,24 +342,29 @@ pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checke 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; @@ -327,9 +387,11 @@ pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checke } // 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. @@ -341,7 +403,15 @@ pub fn update(self: *Self, player_pos: Vec3, player_velocity: Vec3, chunk_checke /// Get current statistics pub fn getStats(self: *Self) LODStats { - return self.stats; + 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 @@ -350,6 +420,72 @@ pub fn pause(self: *Self) void { 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 @@ -391,10 +527,40 @@ pub fn isInRange(self: *const Self, chunk_x: i32, chunk_z: i32) bool { /// 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(); - 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); + 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, 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, 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) 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, &self.stats, if (self.profiling.enabled) &self.profiling else null); } pub fn pointDistanceSquared(x0: i32, z0: i32, x1: i32, z1: i32) i64 { diff --git a/modules/world-lod/src/lod_manager_eviction_ops.zig b/modules/world-lod/src/lod_manager_eviction_ops.zig index 969c32fc..5791bf1b 100644 --- a/modules/world-lod/src/lod_manager_eviction_ops.zig +++ b/modules/world-lod/src/lod_manager_eviction_ops.zig @@ -16,7 +16,6 @@ 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; @@ -50,6 +49,7 @@ 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; @@ -83,7 +83,11 @@ pub fn unloadDistantForLevel(self: *Self, lod: LODLevel, max_radius: i32) !void 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(); @@ -107,6 +111,9 @@ pub fn unloadDistantForLevel(self: *Self, lod: LODLevel, max_radius: i32) !void 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); @@ -125,24 +132,40 @@ pub fn unloadDistantForLevel(self: *Self, lod: LODLevel, max_radius: i32) !void } 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; - // LODMesh does not carry a per-frame fence today, so destruction still - // requires GPU idle. Bound each sweep so a memory-pressure eviction burst - // cannot turn into an unbounded main-thread stall. - self.gpu_bridge.waitIdle(); + // 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; @@ -155,7 +178,12 @@ pub fn regionMemoryBytes(chunk: *const LODChunk, mesh: ?*LODMesh) usize { .simplified => |*s| total += s.totalMemoryBytes(), else => {}, } - if (mesh) |m| total += m.capacity * @sizeOf(Vertex); + 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; } @@ -167,7 +195,10 @@ pub fn enforceMemoryBudget(self: *Self) !void { // 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) { @@ -175,6 +206,7 @@ pub fn enforceMemoryBudget(self: *Self) !void { 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", .{}); @@ -188,7 +220,11 @@ pub fn enforceMemoryBudget(self: *Self) !void { 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(); @@ -263,45 +299,117 @@ pub fn enforceMemoryBudget(self: *Self) !void { /// Update statistics pub fn updateStats(self: *Self) void { self.stats.reset(); - var mem_usage: usize = 0; - + 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; + + 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| { - mem_usage += s.totalMemoryBytes(); + source_data_cpu_bytes += s.totalMemoryBytes(); }, else => {}, } } - // Add mesh memory + // 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] += mesh.vertexCount(); - mem_usage += mesh.capacity * @sizeOf(Vertex); + 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()); } - self.stats.addMemory(mem_usage); + // 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); + const admission_reservation_bytes = std.math.mul(usize, resident_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.memory_governor.used_bytes = mem_usage; + 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 { @@ -333,7 +441,11 @@ pub fn updateStats(self: *Self) void { /// 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); @@ -366,6 +478,9 @@ pub fn unloadLODWhereChunksLoaded(self: *Self, checker: ChunkChecker, ctx: *anyo 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); } diff --git a/modules/world-lod/src/lod_manager_generation_ops.zig b/modules/world-lod/src/lod_manager_generation_ops.zig index 2b8c00a2..ec7f237f 100644 --- a/modules/world-lod/src/lod_manager_generation_ops.zig +++ b/modules/world-lod/src/lod_manager_generation_ops.zig @@ -25,6 +25,7 @@ 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; @@ -47,12 +48,15 @@ const ChunkResolver = manager_ctx.ChunkResolver; const PendingIngestion = manager_ctx.PendingIngestion; const PlayerChunkPos = manager_ctx.PlayerChunkPos; const GenerationCandidate = manager_ctx.GenerationCandidate; -const MeshCandidate = manager_ctx.MeshCandidate; -const UploadCandidate = manager_ctx.UploadCandidate; +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; @@ -73,16 +77,7 @@ pub fn queueLODRegions(self: *Self, lod: LODLevel, velocity: Vec3, chunk_checker 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; - var pending_regions: usize = 0; - for (0..active_lod_count) |i| { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - switch (entry.value_ptr.*.getState()) { - .missing, .renderable => {}, - else => pending_regions += 1, - } - } - } + const memory_budget_bytes = @as(usize, self.config.getMemoryBudgetMB()) * 1024 * 1024; self.mutex.unlock(); const Coverage = struct { @@ -109,222 +104,275 @@ pub fn queueLODRegions(self: *Self, lod: LODLevel, velocity: Vec3, chunk_checker // Route every region through the bounded admission path, whether or // not persistent LOD caching is enabled. .defer_generation_dispatch = true, - .pending_regions = &pending_regions, + .pending_regions = &self.pending_region_count, + .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 Candidate = GenerationCandidate; - var candidates = &self.generation_candidates_scratch; - candidates.clearRetainingCapacity(); + 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); const player = self.loadPlayerChunkPos(); - const cache_enabled = self.cacheEnabled(); - var active_lod_count: usize = 0; + 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 active_lod_count = lod_chunk.activeLODCount(self.config); + const scale: i32 = @intCast(key.lod.chunksPerSide()); + const candidate = GenerationCandidate{ + .key = key, + .chunk = chunk, + .encoded_priority = lod_scheduler.encodePriority(key.lod, key.rx * scale + @divFloor(scale, 2) - player.cx, key.rz * scale + @divFloor(scale, 2) - player.cz, Vec3.zero, active_lod_count), + .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 { + if (token.stage != .generation) return null; + const lod_idx = @intFromEnum(token.key.lod); + const player = self.loadPlayerChunkPos(); self.mutex.lock(); - active_lod_count = lod_chunk.activeLODCount(self.config); - const radii = self.config.getRadii(); - var i: usize = 0; - while (i < active_lod_count) : (i += 1) { - const lod: LODLevel = @enumFromInt(@as(u3, @intCast(i))); - const scale: i32 = @intCast(lod.chunksPerSide()); - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const chunk = entry.value_ptr.*; - if (chunk.getState() != .queued_for_generation) continue; - const center_cx = chunk.region_x * scale + @divFloor(scale, 2); - const center_cz = chunk.region_z * scale + @divFloor(scale, 2); - const encoded_priority = lod_scheduler.encodePriority(lod, center_cx - player.cx, center_cz - player.cz, velocity, active_lod_count); - candidates.append(self.allocator, .{ - .key = entry.key_ptr.*, - .chunk = chunk, - .encoded_priority = encoded_priority, - .level = @intCast(i), - .coord_scale = scale, - .job_token = chunk.job_token, - .lod_radius = radii[i], - .want_spans = self.config.getVerticalSpanBudget() > 0 and self.effectiveMeshPath(lod) == .column_spans, - }) catch |err| { - self.mutex.unlock(); - return err; - }; - } + 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 active = lod_chunk.activeLODCount(self.config); + const scale: i32 = @intCast(token.key.lod.chunksPerSide()); + return .{ + .key = token.key, + .chunk = chunk, + .encoded_priority = lod_scheduler.encodePriority(token.key.lod, token.key.rx * scale + @divFloor(scale, 2) - player.cx, token.key.rz * scale + @divFloor(scale, 2) - player.cz, velocity, active), + .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(); - std.mem.sort(Candidate, candidates.items, {}, struct { - fn lt(_: void, a: Candidate, b: Candidate) bool { - return a.encoded_priority < b.encoded_priority; - } - }.lt); + 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, + .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); +} - var cache_reads: usize = 0; - for (candidates.items) |candidate| { - var cached_data: ?LODSimplifiedData = null; - var attempted_cache = false; - if (cache_enabled and cache_reads < MAX_CACHE_LOADS_PER_UPDATE) { - cache_reads += 1; - attempted_cache = true; - cached_data = self.loadCachedSourceData(candidate.key); - if (cached_data) |*cached| { - if (candidate.want_spans and !cached.hasVerticalSpans()) { - cached.deinit(); - cached_data = null; - } - } +/// 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 (cached_data) |data| { - self.recordCacheHit(); - self.mutex.lock(); - if (candidate.chunk.getState() == .queued_for_generation and candidate.chunk.job_token == candidate.job_token) { - candidate.chunk.data = .{ .simplified = data }; - candidate.chunk.updateHeightBoundsFromData(); - candidate.chunk.setState(.generated); - } else { - var stale_data = data; - stale_data.deinit(); + 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 (attempted_cache) self.recordCacheMiss(); - - self.mutex.lock(); - if (candidate.chunk.getState() != .queued_for_generation or candidate.chunk.job_token != candidate.job_token) { + if (token.stage != .mesh or chunk.getState() != .generated) { self.mutex.unlock(); continue; } - candidate.chunk.setState(.generating); - self.mutex.unlock(); - + const scale: i32 = @intCast(token.key.lod.chunksPerSide()); + chunk.setState(.meshing); + chunk.resetCancellation(); self.job_dispatcher.queues[LODLevel.count - 1].push(.{ - .type = .chunk_generation, - .dist_sq = candidate.encoded_priority, + .type = .chunk_meshing, + .dist_sq = token.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, - .lod_radius = candidate.lod_radius, - .use_vertical_spans = candidate.want_spans, + .x = chunk.region_x, + .z = chunk.region_z, + .job_token = token.job_token, + .lod_level = @intCast(lod_idx), + .coord_scale = scale, + .lod_radius = self.config.getRadii()[lod_idx], }, }, }) catch |err| { - self.mutex.lock(); - if (candidate.chunk.getState() == .generating and candidate.chunk.job_token == candidate.job_token) { - candidate.chunk.setState(.queued_for_generation); + 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); } -/// Process state transitions (generated -> meshing -> ready) -pub fn processStateTransitions(self: *Self, velocity: Vec3) !void { - // Collect generated/mesh-ready chunks, then sort by ascending distance - // before enqueueing. The regions HashMap iterates in arbitrary - // (hash-bucket) order, so without sorting the meshing/upload order is - // effectively random — far chunks can be processed before near ones. - var mesh_candidates = &self.mesh_candidates_scratch; - mesh_candidates.clearRetainingCapacity(); - - var upload_candidates = &self.upload_candidates_scratch; - upload_candidates.clearRetainingCapacity(); - - const player = self.loadPlayerChunkPos(); - var active_lod_count: usize = 0; - +/// 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(); - active_lod_count = lod_chunk.activeLODCount(self.config); - const radii = self.config.getRadii(); - for (0..active_lod_count) |i| { - const lod = @as(LODLevel, @enumFromInt(@as(u3, @intCast(i)))); - const scale = @as(i32, @intCast(lod.chunksPerSide())); - const level: u3 = @intCast(i); - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { + defer self.mutex.unlock(); + for (&self.regions, 0..) |*regions, lod_idx| { + var it = regions.iterator(); + while (it.next()) |entry| { const chunk = entry.value_ptr.*; - if (chunk.getState() == .generated) { - const center_cx = chunk.region_x * scale + @divFloor(scale, 2); - const center_cz = chunk.region_z * scale + @divFloor(scale, 2); - const encoded_priority = lod_scheduler.encodePriority(lod, center_cx - player.cx, center_cz - player.cz, velocity, active_lod_count); - // Append before flipping state so an allocation failure - // leaves the chunk in .generated (re-tried next tick) - // instead of stuck in .meshing with no queued job. - mesh_candidates.append(self.allocator, .{ .chunk = chunk, .encoded_priority = encoded_priority, .level = level, .coord_scale = scale, .job_token = chunk.job_token, .lod_radius = radii[i] }) catch |err| { - self.mutex.unlock(); - return err; - }; - chunk.setState(.meshing); - } else if (chunk.getState() == .mesh_ready) { - const center_cx = chunk.region_x * scale + @divFloor(scale, 2); - const center_cz = chunk.region_z * scale + @divFloor(scale, 2); - const encoded_priority = lod_scheduler.encodePriority(lod, center_cx - player.cx, center_cz - player.cz, velocity, active_lod_count); - upload_candidates.append(self.allocator, .{ .chunk = chunk, .encoded_priority = encoded_priority, .level = level }) catch |err| { - self.mutex.unlock(); - return err; - }; - chunk.setState(.uploading); + 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 = @as(i32, @intCast(lod_idx)) << 28, + .stage = stage, + }; + if (stage == .generation) { + _ = self.generation_tokens.push(self.allocator, token) catch false; + } else { + _ = self.transition_tokens.push(self.allocator, token) catch false; } } } - self.mutex.unlock(); +} - // Meshing jobs share one queue; sort by encoded priority so fine/near - // sections are built before coarse fallback. - std.mem.sort(MeshCandidate, mesh_candidates.items, {}, struct { - fn lt(_: void, a: MeshCandidate, b: MeshCandidate) bool { - return a.encoded_priority < b.encoded_priority; - } - }.lt); - for (mesh_candidates.items) |mc| { - self.job_dispatcher.queues[LODLevel.count - 1].push(.{ - .type = .chunk_meshing, - .dist_sq = mc.encoded_priority, - .data = .{ - .chunk = .{ - .x = mc.chunk.region_x, - .z = mc.chunk.region_z, - .job_token = mc.job_token, - .lod_level = mc.level, - .coord_scale = mc.coord_scale, - .lod_radius = mc.lod_radius, - }, - }, - }) catch |err| { - self.mutex.lock(); - if (mc.chunk.getState() == .meshing and mc.chunk.job_token == mc.job_token) { - mc.chunk.setState(.generated); - } - self.mutex.unlock(); - return err; - }; +pub fn enqueueTransition(self: *Self, key: LODRegionKey, chunk: *const LODChunk, stage: LifecycleStage) void { + const player = self.loadPlayerChunkPos(); + const scale: i32 = @intCast(key.lod.chunksPerSide()); + const priority = lod_scheduler.encodePriority(key.lod, key.rx * scale + @divFloor(scale, 2) - player.cx, key.rz * scale + @divFloor(scale, 2) - player.cz, Vec3.zero, lod_chunk.activeLODCount(self.config)); + const token = LifecycleToken{ + .key = key, + .job_token = chunk.job_token, + .source_revision = chunk.source_revision, + .priority = 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; } +} - // Uploads go to per-level FIFO queues. Sort by the same encoded priority - // used by generation/meshing: small horizon seed first, then detailed - // bands so fallback terrain is visible but short-lived. - std.mem.sort(UploadCandidate, upload_candidates.items, {}, struct { - fn lt(_: void, a: UploadCandidate, b: UploadCandidate) bool { - return a.encoded_priority < b.encoded_priority; - } - }.lt); - for (upload_candidates.items) |uc| { - self.upload_queues[uc.level].push(uc.chunk) catch |err| { - self.mutex.lock(); - if (uc.chunk.getState() == .uploading) { - uc.chunk.setState(.mesh_ready); - } - self.mutex.unlock(); - return err; - }; +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); } } @@ -350,6 +398,13 @@ pub fn getOrCreateMesh(self: *Self, key: LODRegionKey) !*LODMesh { /// 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, @@ -357,15 +412,34 @@ pub fn buildMeshForChunk(self: *Self, chunk: *LODChunk) !void { }; const mesh = try self.getOrCreateMesh(key); - - // Access chunk.data under shared lock - the data is read-only during meshing - // and the chunk is pinned, so we just need to ensure visibility - self.mutex.lockShared(); - defer self.mutex.unlockShared(); + 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), @@ -391,6 +465,45 @@ pub fn buildMeshForChunk(self: *Self, chunk: *LODChunk) !void { } } +/// 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; + 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 @@ -424,6 +537,13 @@ pub fn processLODJob(ctx: *anyopaque, job: Job) void { 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; @@ -436,18 +556,13 @@ pub fn processLODJob(ctx: *anyopaque, job: Job) void { 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; } - // Skip if token mismatch - if (chunk.job_token != job.data.chunk.job_token) { - self.mutex.unlock(); - return; - } - // Check state and capture job type before releasing lock const current_state = chunk.getState(); const job_type = job.type; @@ -474,43 +589,73 @@ pub fn processLODJob(ctx: *anyopaque, job: Job) void { // 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.initWithVerticalSpans(self.allocator, lod_level) catch { + 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) chunk.setState(new_state); + 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.init(self.allocator, lod_level) catch { + 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) chunk.setState(new_state); + 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 stop flag so teardown/pause can interrupt the - // multi-second coarse-LOD heightmap loop instead of - // forcing the worker-join to block until it finishes. - self.generator.generateHeightmapOnly(&data, chunk.region_x, chunk.region_z, lod_level, &self.job_dispatcher.stop_flag); + // 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)) { + 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) chunk.setState(new_state); + 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; @@ -520,13 +665,15 @@ pub fn processLODJob(ctx: *anyopaque, job: Job) void { self.mutex.lock(); chunk.data = .{ .simplified = data }; chunk.updateHeightBoundsFromData(); - chunk.store_dirty = true; + 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| { @@ -548,6 +695,12 @@ pub fn processLODJob(ctx: *anyopaque, job: Job) void { 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 index 7f8d25af..b1fe7d41 100644 --- a/modules/world-lod/src/lod_manager_ingestion_ops.zig +++ b/modules/world-lod/src/lod_manager_ingestion_ops.zig @@ -57,7 +57,6 @@ 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; @@ -133,7 +132,8 @@ pub fn applyIngestionToRegions(self: *Self, cx: i32, cz: i32, chunk: *const Chun // 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.getState() == .generating or + if (lod_chunk_ptr.isPinned() or + lod_chunk_ptr.getState() == .generating or lod_chunk_ptr.getState() == .meshing or lod_chunk_ptr.getState() == .uploading) { @@ -145,8 +145,7 @@ pub fn applyIngestionToRegions(self: *Self, cx: i32, cz: i32, chunk: *const Chun 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.dirty = true; - lod_chunk_ptr.store_dirty = true; + lod_chunk_ptr.markSourceDirty(); lod_chunk_ptr.updateHeightBoundsFromData(); // Force a remesh of already-rendered regions so the new // chunk-derived data becomes visible. @@ -163,66 +162,64 @@ pub fn applyIngestionToRegions(self: *Self, cx: i32, cz: i32, chunk: *const Chun /// 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) void { for (self.ingestion_queue.pending_ingestions.items) |*entry| { if (entry.cx == cx and entry.cz == cz) { entry.pending_levels |= mask; - entry.ttl = PENDING_INGESTION_TTL; + entry.ttl = 0; if (provenance.canOverwrite(entry.provenance)) entry.provenance = provenance; return; } } - if (self.ingestion_queue.pending_ingestions.items.len >= MAX_PENDING_INGESTIONS) { - _ = self.ingestion_queue.pending_ingestions.orderedRemove(0); - } + 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 = PENDING_INGESTION_TTL, + .ttl = 0, }) catch |err| { log.log.warn("Failed to defer LOD ingestion for chunk ({}, {}): {}", .{ cx, cz, err }); }; } -/// Re-record a pending entry from outside the lock (decay applied by -/// caller). Coalesces with any existing entry for the coordinate. +/// 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; - entry.ttl = @max(entry.ttl, ttl); + _ = ttl; + entry.ttl = 0; if (provenance.canOverwrite(entry.provenance)) entry.provenance = provenance; return; } } - if (self.ingestion_queue.pending_ingestions.items.len >= MAX_PENDING_INGESTIONS) { - _ = self.ingestion_queue.pending_ingestions.orderedRemove(0); - } + 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 = ttl, + .ttl = 0, }) catch |err| { log.log.warn("Failed to requeue deferred LOD ingestion for chunk ({}, {}): {}", .{ cx, cz, err }); }; } -/// Decay TTL of every pending entry, dropping expired ones. Assumes -/// `ingestion_mutex` held. +/// 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| { - const ttl = if (entry.ttl > 0) entry.ttl - 1 else 0; - if (ttl > 0 and entry.pending_levels != 0) { - var e = entry; - e.ttl = ttl; - self.ingestion_queue.pending_ingestions.items[write] = e; + if (entry.pending_levels != 0) { + self.ingestion_queue.pending_ingestions.items[write] = entry; write += 1; } } @@ -231,7 +228,7 @@ pub fn decayPendingLocked(self: *Self) void { /// Replay deferred ingestions: snapshot all pending under the ingestion /// lock, resolve each chunk, and re-apply. Unresolved or still-in-flight -/// levels are re-recorded with a decayed TTL. +/// levels remain queued until they apply or manager teardown. pub fn drainPendingIngestions(self: *Self) void { var snapshot = std.ArrayListUnmanaged(PendingIngestion).empty; { @@ -249,28 +246,43 @@ pub fn drainPendingIngestions(self: *Self) void { const resolver = self.ingestion_queue.chunk_resolver; const limit = @min(snapshot.items.len, self.ingestion_queue.drain_per_frame); - // Process the head of the snapshot; defer the tail with a decayed TTL. + // 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) { - const ttl = if (entry.ttl > 0) entry.ttl - 1 else 0; - if (ttl > 0) self.rerecordPending(entry.cx, entry.cz, entry.provenance, entry.pending_levels, ttl); + 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 = self.applyIngestionToRegions(entry.cx, entry.cz, c, entry.provenance); if (remaining != 0) { - const ttl = if (entry.ttl > 0) entry.ttl - 1 else 0; - if (ttl > 0) self.rerecordPending(entry.cx, entry.cz, entry.provenance, remaining, ttl); + self.rerecordPending(entry.cx, entry.cz, entry.provenance, remaining, 0); } } else { - const ttl = if (entry.ttl > 0) entry.ttl - 1 else 0; - if (ttl > 0) self.rerecordPending(entry.cx, entry.cz, entry.provenance, entry.pending_levels, ttl); + 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; } /// Flush debounced player edits: re-ingest edited chunks with the `edited` diff --git a/modules/world-lod/src/lod_manager_internal_tests.zig b/modules/world-lod/src/lod_manager_internal_tests.zig index fe447284..2bb84b46 100644 --- a/modules/world-lod/src/lod_manager_internal_tests.zig +++ b/modules/world-lod/src/lod_manager_internal_tests.zig @@ -12,9 +12,12 @@ 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; @@ -25,6 +28,9 @@ 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" { @@ -35,7 +41,8 @@ test "LODManager cache helpers save and reload source data" { var path_buf: [fs.max_path_bytes]u8 = undefined; const save_dir_path = try dir.realpath(".", &path_buf); - var manager = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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 }; @@ -49,6 +56,7 @@ test "LODManager cache helpers save and reload source data" { }, 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); @@ -71,7 +79,8 @@ test "LODManager cache helpers delete corrupt cache files" { var path_buf: [fs.max_path_bytes]u8 = undefined; const save_dir_path = try dir.realpath(".", &path_buf); - var manager = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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 }; @@ -105,7 +114,8 @@ test "LODManager enableCache deletes stale generator-keyed store and writes live 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 = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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); @@ -134,7 +144,8 @@ test "LODManager enableCache deletes stale data-version store" { 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 = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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); @@ -144,7 +155,7 @@ test "LODManager enableCache deletes stale data-version store" { try testing.expectEqual(lod_cache.CACHE_VERSION, header.lod_data_version); } -test "LODManager queued generation reloads source store on main thread" { +test "LODManager queued generation applies source-store completion asynchronously" { var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -163,15 +174,18 @@ test "LODManager queued generation reloads source store on main thread" { .foundation = .stone, }, 0xFF445566, .empty, .daylight, .empty); - var writer = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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 = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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); @@ -199,6 +213,9 @@ test "LODManager queued generation reloads source store on main thread" { 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); @@ -206,9 +223,16 @@ test "LODManager queued generation reloads source store on main thread" { 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); @@ -231,7 +255,8 @@ test "LODManager queued generation dispatches beyond cache read budget" { const save_dir_path = try dir.realpath(".", &path_buf); var config = LODConfig{}; - var manager = LODManager.initCacheTestManager(testing.allocator, save_dir_path); + 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); @@ -259,6 +284,9 @@ test "LODManager queued generation dispatches beyond cache read budget" { 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; @@ -269,21 +297,30 @@ test "LODManager queued generation dispatches beyond cache read budget" { 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 = LODManager.initCacheTestManager(allocator, ""); + 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 = .{ @@ -302,6 +339,7 @@ fn initEvictionTestManager(allocator: std.mem.Allocator, config: *LODConfig) !LO } 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| { @@ -312,6 +350,7 @@ fn deinitEvictionTestManager(manager: *LODManager) void { 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); } @@ -319,12 +358,409 @@ fn deinitEvictionTestManager(manager: *LODManager) void { } 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 LOD3 and full-density horizon 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()); + } } fn putTestRegion(manager: *LODManager, key: LODRegionKey, state: LODState) !*LODChunk { @@ -353,16 +789,49 @@ fn putTestPendingMesh(manager: *LODManager, key: LODRegionKey, vertex_count: usi 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), }; } @@ -381,7 +850,17 @@ const UploadMock = struct { } fn destroy(_: *LODMesh, _: *anyopaque) void {} - fn waitIdle(_: *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" { @@ -409,6 +888,90 @@ test "LODManager upload budget defers remaining queued meshes" { try testing.expectEqual(@as(usize, 1), manager.upload_queues[1].count()); } +test "LODManager upload budget defers an oversized first mesh" { + 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 key = LODRegionKey{ .rx = 0, .rz = 0, .lod = .lod1 }; + const chunk = try putTestRegion(&manager, key, .uploading); + _ = try putTestPendingMesh(&manager, key, 1); + try manager.upload_queues[1].push(chunk); + + manager.processUploadsWithBudget(@sizeOf(Vertex) - 1); + + try testing.expectEqual(@as(u32, 0), mock.calls); + try testing.expectEqual(LODState.uploading, chunk.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()); +} + +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); @@ -435,6 +998,71 @@ test "LODManager staging pressure failure stops upload sweep" { 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); @@ -539,3 +1167,152 @@ test "LODManager memory budget eviction skips unsafe regions and evicts farthest 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 index 98cbc5fa..aff3543d 100644 --- a/modules/world-lod/src/lod_manager_tests.zig +++ b/modules/world-lod/src/lod_manager_tests.zig @@ -6,18 +6,20 @@ 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("world-lod").lod_manager; +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 MAX_LOD_REGIONS = lod_manager.MAX_LOD_REGIONS; -const lod_chunk = @import("world-lod").lod_chunk; +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("world-lod").LODMesh; -const lod_gpu = @import("world-lod").lod_upload_queue; +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; @@ -30,16 +32,29 @@ 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) void {} + 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; @@ -70,7 +85,7 @@ test "LODManager initialization" { const mock_gen = Generator{ .ptr = &mock_gen_impl, .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator" }, + .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, }; var config = LODConfig{ @@ -96,7 +111,14 @@ test "LODManager initialization" { 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) void {} + 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, 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 {} @@ -117,12 +139,16 @@ test "LODManager initialization" { .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, }; - var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, mock_gen.toLODGenerator(), &mock_atlas); + 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); + try std.testing.expect(mock_state.prepare_saw_stats); + try std.testing.expect(mock_state.prepare_saw_profiling); mgr.deinit(); @@ -136,7 +162,7 @@ test "LODManager end-to-end covered cleanup" { const allocator = std.testing.allocator; const MockGenerator = struct { - fn generate(_: *anyopaque, _: *Chunk, _: ?*const bool) void {} + 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; @@ -167,7 +193,7 @@ test "LODManager end-to-end covered cleanup" { const mock_gen = Generator{ .ptr = &mock_gen_impl, .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator" }, + .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, }; var config = LODConfig{ @@ -190,7 +216,7 @@ test "LODManager end-to-end covered cleanup" { 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) void {} + 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 {} @@ -211,7 +237,7 @@ test "LODManager end-to-end covered cleanup" { .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, }; - var mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, mock_gen.toLODGenerator(), &mock_atlas); + 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(); @@ -266,10 +292,12 @@ test "LODStats aggregation" { 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" { @@ -277,6 +305,17 @@ test "LODManager constants" { 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) // --------------------------------------------------------------------------- @@ -285,7 +324,7 @@ test "LODManager constants" { /// `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) void {} + 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; @@ -316,7 +355,7 @@ fn buildIngestionManager(allocator: std.mem.Allocator, config: *LODConfig) !*LOD const mock_gen = Generator{ .ptr = &mock_gen_impl, .vtable = &MockGenerator.vtable, - .info = .{ .name = "Mock", .description = "Mock Generator" }, + .info = .{ .name = "Mock", .description = "Mock Generator", .version = 1 }, }; var noop_ctx: u8 = 0; @@ -335,7 +374,7 @@ fn buildIngestionManager(allocator: std.mem.Allocator, config: *LODConfig) !*LOD 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) void {} + 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 {} @@ -356,7 +395,7 @@ fn buildIngestionManager(allocator: std.mem.Allocator, config: *LODConfig) !*LOD .tile_mappings = [_]TextureAtlas.BlockTiles{TextureAtlas.BlockTiles.uniform(0)} ** MAX_BLOCK_TYPES, }; - const mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, mock_gen.toLODGenerator(), &mock_atlas); + const mgr = try LODManager.init(allocator, config.interface(), mock_bridge, mock_render, lodGeneratorFromGenerator(mock_gen), &mock_atlas); mgr.cleanup_covered_regions = false; return mgr; } @@ -402,6 +441,29 @@ test "ingestChunk upgrades worldgen region data and schedules a remesh" { 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 provenance authority: edited beats chunk_derived, worldgen cannot overwrite" { const allocator = std.testing.allocator; var config = LODConfig{ .radii = .{ 2, 4, 8, 16, 32 } }; @@ -466,7 +528,7 @@ test "markChunkEdited coalesces and re-ingests via the resolver on update" { mgr.markChunkEdited(0, 0); // update() drains the edit queue on its cooldown (starts expired). - try mgr.update(Vec3.zero, Vec3.zero, null, null); + 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)); @@ -484,10 +546,10 @@ test "enforceMemoryBudget shrinks finer radii under sustained pressure and spare // Force sustained over-budget state with no evictable regions, so the // hysteresis grow path must fire. - mgr.memory_used_bytes = 50_000_000; - try mgr.update(Vec3.zero, Vec3.zero, null, null); + 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.radius_shrink_chunks[1] > 0); - try std.testing.expectEqual(@as(i32, 0), mgr.radius_shrink_chunks[LODLevel.count - 1]); + 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 index aa328c73..37451a20 100644 --- a/modules/world-lod/src/lod_manager_upload_ops.zig +++ b/modules/world-lod/src/lod_manager_upload_ops.zig @@ -63,6 +63,99 @@ 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 { @@ -75,7 +168,7 @@ pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { chunk: *LODChunk, mesh: *LODMesh, lod_idx: usize, - pending_bytes: usize, + staging_bytes: usize, }; self.mutex.lockShared(); @@ -86,26 +179,34 @@ pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { var uploaded_bytes: usize = 0; while (uploads < max_uploads) { + const prep_timer = self.profiling.begin(); var task: ?UploadTask = null; var completed_without_upload = false; var made_progress = false; - var stop_processing = 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 uploads < max_uploads and task == null and !completed_without_upload and !stop_processing) : (order_idx += 1) { + 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); - if (self.upload_queues[i].pop()) |chunk| { + 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 pending_bytes = mesh.pendingUploadBytes(); - if (wouldExceedUploadBudget(uploaded_bytes, pending_bytes, upload_budget_bytes, uploads)) { + const staging_bytes = self.gpu_bridge.uploadCost(mesh).total(); + if (wouldExceedUploadBudget(uploaded_bytes, staging_bytes, upload_budget_bytes)) { + self.profiling.addStagingPressure(); self.requeueUpload(i, chunk); - stop_processing = true; - break; + deferred_for_budget = true; + continue; } chunk.pin(); @@ -114,27 +215,65 @@ pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { .chunk = chunk, .mesh = mesh, .lod_idx = i, - .pending_bytes = pending_bytes, + .staging_bytes = staging_bytes, }; } else { self.markRegionRenderable(key, chunk); uploads += 1; completed_without_upload = true; } - } + }; } self.mutex.unlock(); - if (stop_processing or !made_progress) break; - if (completed_without_upload) continue; + 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 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(); @@ -145,8 +284,21 @@ pub fn processUploadsWithBudget(self: *Self, upload_budget_bytes: usize) void { self.mutex.unlock(); continue; }; + self.profiling.end(.upload_submission, submission_timer); - uploaded_bytes += upload_task.pending_bytes; + uploaded_bytes += upload_task.staging_bytes; + 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; @@ -185,11 +337,14 @@ 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)); + self.enqueueFade(key, chunk); + if (self.pending_region_count > 0) self.pending_region_count -= 1; if (self.regionContributesGeometry(key, chunk)) { self.adjustParentReadyChildren(key, 1); } @@ -198,13 +353,15 @@ pub fn markRegionRenderable(self: *Self, key: LODRegionKey, chunk: *LODChunk) vo pub fn decayTransitionFrames(self: *Self) void { self.mutex.lock(); defer self.mutex.unlock(); - const active = lod_chunk.activeLODCount(self.config); - var i: usize = 1; - while (i < active) : (i += 1) { - var iter = self.regions[i].iterator(); - while (iter.next()) |entry| { - const chunk = entry.value_ptr.*; - chunk.tickTransition(); + 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; } } } @@ -218,8 +375,15 @@ pub fn noteRegionRemoved(self: *Self, key: LODRegionKey, chunk: *const LODChunk) 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) { + chunk.job_token +%= 1; chunk.setState(.generated); + self.enqueueTransition(key, chunk, .mesh); } } diff --git a/modules/world-lod/src/lod_mesh.zig b/modules/world-lod/src/lod_mesh.zig index 0b163b2c..2c70c13c 100644 --- a/modules/world-lod/src/lod_mesh.zig +++ b/modules/world-lod/src/lod_mesh.zig @@ -28,6 +28,8 @@ const log = @import("engine-core").log; 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; @@ -83,6 +85,11 @@ pub fn getCellSize(lod: LODLevel) u32 { /// 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, @@ -99,6 +106,24 @@ pub const LODMesh = struct { 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 @@ -117,6 +142,25 @@ pub const LODMesh = struct { 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 @@ -144,6 +188,115 @@ pub const LODMesh = struct { 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; @@ -166,7 +319,12 @@ pub const LODMesh = struct { } pub fn drawRange(self: *const LODMesh, layer: LODRenderLayer) ?DrawRange { - if (!self.ready or self.buffer_handle == 0) return null; + 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 } @@ -231,6 +389,37 @@ pub const LODMesh = struct { 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(); @@ -253,9 +442,106 @@ pub const LODMesh = struct { 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(); @@ -269,10 +555,31 @@ pub const LODMesh = struct { 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; @@ -331,29 +638,27 @@ pub const LODMesh = struct { } } - // Store pending vertices - 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; + 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); - - 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; - } + self.pending_vertices = pending; } /// Build mesh from rich LOD column/span data, falling back to the stable heightfield path @@ -661,7 +966,6 @@ pub const LODMesh = struct { if (pending.len == 0) { if (self.buffer_handle != 0 and !self.pooled) { - resources.waitIdle(); resources.destroyBuffer(self.buffer_handle); } self.buffer_handle = 0; @@ -685,7 +989,7 @@ pub const LODMesh = struct { var new_handle: BufferHandle = 0; // Create or resize buffer. Keep the old buffer renderable until the - // replacement upload succeeds, then destroy it after an idle wait. + // 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; @@ -701,7 +1005,6 @@ pub const LODMesh = struct { self.vertex_offset = 0; self.pooled = false; if (old_handle != 0 and !self.pooled) { - resources.waitIdle(); resources.destroyBuffer(old_handle); } } diff --git a/modules/world-lod/src/lod_mesh_resources.zig b/modules/world-lod/src/lod_mesh_resources.zig index 410b0f2b..ca4c6b4c 100644 --- a/modules/world-lod/src/lod_mesh_resources.zig +++ b/modules/world-lod/src/lod_mesh_resources.zig @@ -1,5 +1,6 @@ //! 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; @@ -142,6 +143,18 @@ pub const LODMeshResources = struct { }; }; +/// 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. diff --git a/modules/world-lod/src/lod_mesh_tests.zig b/modules/world-lod/src/lod_mesh_tests.zig index 6e74b254..c60b4003 100644 --- a/modules/world-lod/src/lod_mesh_tests.zig +++ b/modules/world-lod/src/lod_mesh_tests.zig @@ -19,6 +19,9 @@ 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; @@ -69,6 +72,83 @@ test "LODMesh initialization" { 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)); @@ -999,7 +1079,7 @@ test "LOD boundary height retains its authoritative source sample" { try std.testing.expectEqual(@as(f32, 100.0), quantizedCellSurfaceHeight(&data, 0, 1)); } -test "representativeVegetationForLOD preserves sparse coarse coverage" { +test "representativeVegetationForLOD aggregates sparse coarse coverage" { const allocator = std.testing.allocator; var data = try LODSimplifiedData.init(allocator, .lod2); defer data.deinit(); @@ -1014,10 +1094,21 @@ test "representativeVegetationForLOD preserves sparse coarse coverage" { }; const vegetation = representativeVegetationForLOD(&data, 0, 0, .lod2); - try std.testing.expect(vegetation.tree_coverage >= 0.1); + 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; diff --git a/modules/world-lod/src/lod_renderer.zig b/modules/world-lod/src/lod_renderer.zig index 85f2b438..377343dd 100644 --- a/modules/world-lod/src/lod_renderer.zig +++ b/modules/world-lod/src/lod_renderer.zig @@ -37,7 +37,9 @@ 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; @@ -46,10 +48,12 @@ 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; @@ -58,9 +62,91 @@ 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; +const COMPACT_GRID_WIDTHS = [_]u32{ 5, 9, 17, 33, 65, 129 }; + +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, @@ -74,6 +160,25 @@ const RenderDiag = struct { 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: @@ -93,14 +198,39 @@ pub fn LODRenderer(comptime RHI: type) type { // MDI Resources (Moved from LODManager) instance_data: std.ArrayListUnmanaged(rhi_types.InstanceData), draw_list: std.ArrayListUnmanaged(*LODMesh), + projection_regions: std.ArrayListUnmanaged(VisibleRegion), + 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, frame_index: usize, + frame_serial: u64, enable_mdi: bool, gpu_culling_requested: bool, - gpu_culling_fallback_logged: 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, /// 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. @@ -129,24 +259,59 @@ pub fn LODRenderer(comptime RHI: type) type { 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); + 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 indices = try compactGridIndices(allocator, width, include_skirts); + defer allocator.free(indices); + compact_index_buffers[idx][layer_idx][width_idx] = try resources.createBuffer(std.mem.sliceAsBytes(indices).len, .index); + try resources.uploadBuffer(compact_index_buffers[idx][layer_idx][width_idx], std.mem.sliceAsBytes(indices)); + }; + } + 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, + .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, .frame_index = 0, - .enable_mdi = engine_core.envFlag("ZIGCRAFT_ENABLE_LOD_MDI", false), - .gpu_culling_requested = engine_core.envFlag("ZIGCRAFT_LOD_GPU_CULLING", false), - .gpu_culling_fallback_logged = false, + .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 default; set ZIGCRAFT_ENABLE_LOD_MDI=1 to test indirect LOD batches", .{}); + log.log.info("LOD MDI disabled by ZIGCRAFT_DISABLE_LOD_MDI", .{}); } return renderer; @@ -169,13 +334,171 @@ pub fn LODRenderer(comptime RHI: type) type { 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, + layer: LODRenderLayer, + stats: ?*LODStats, + profiling: ?*LODProfilingCollector, + ) void { + self.frame_serial = 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, 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, 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, + 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, null, 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 (!@hasDecl(@TypeOf(query), "supportsIndirectCount")) { + self.projection_frame = null; + return; + } + if (!query.supportsIndirectFirstInstance() or !query.supportsIndirectCount()) { + 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, @@ -189,19 +512,16 @@ pub fn LODRenderer(comptime RHI: type) type { 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(); - if (self.gpu_culling_requested and !self.gpu_culling_fallback_logged) { - log.log.warn("ZIGCRAFT_LOD_GPU_CULLING requested, but LOD GPU culling is using CPU fallback until RHI exposes indirect-command compaction", .{}); - self.gpu_culling_fallback_logged = true; - } - // 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); @@ -228,16 +548,33 @@ pub fn LODRenderer(comptime RHI: type) type { 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) catch |err| { + 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; - if (self.enable_mdi and layer == .terrain and self.renderIndirectBatches(render_ctx, query)) 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); @@ -266,7 +603,6 @@ pub fn LODRenderer(comptime RHI: type) type { var total_commands: usize = 0; for (self.draw_commands) |commands| total_commands += commands.items.len; if (total_commands == 0) return false; - if (total_commands != self.draw_list.items.len) 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; @@ -311,6 +647,471 @@ pub fn LODRenderer(comptime RHI: type) type { return true; } + 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, + stats: ?*LODStats, + profiling: ?*LODProfilingCollector, + ) !void { + 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 = config.getChunkRenderRadius(); + 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; + } + + var mask_radius = config.calculateMaskRadius(); + 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; + } + if (cov.missing_chunk_in_radius and !cov.has_chunk_coverage_in_radius) mask_radius = LOD_UNMASKED_SENTINEL; + } + } + + 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 = LOD_UNMASKED_SENTINEL, + .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, LOD_UNMASKED_SENTINEL); + 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") or !@hasDecl(@TypeOf(query), "supportsIndirectCount")) return false; + if (!query.supportsIndirectFirstInstance() or !query.supportsIndirectCount() 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 { + 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, @@ -327,6 +1128,7 @@ pub fn LODRenderer(comptime RHI: type) type { max_distance_chunks: ?i32, layer: LODRenderLayer, stats: ?*LODStats, + profiling: ?*LODProfilingCollector, ) !void { const meshes = &all_meshes[@intFromEnum(lod)]; const regions = &all_regions[@intFromEnum(lod)]; @@ -347,21 +1149,29 @@ pub fn LODRenderer(comptime RHI: type) type { 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; } @@ -371,6 +1181,15 @@ pub fn LODRenderer(comptime RHI: type) type { 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; } } @@ -382,10 +1201,16 @@ pub fn LODRenderer(comptime RHI: type) type { 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) { @@ -405,13 +1230,6 @@ pub fn LODRenderer(comptime RHI: type) type { lod_rendered += 1; - if (use_frustum and !disable_frustum) { - if (!isRegionInFrustum(frustum, bounds, camera_pos)) { - diag.frustum_culled += 1; - continue; - } - } - 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 @@ -434,6 +1252,7 @@ pub fn LODRenderer(comptime RHI: type) type { }); } diag.drawn += 1; + if (profiling) |profile| profile.addVisible(); if (stats) |s| { const lod_idx = @intFromEnum(lod); if (layer == .fluid) { @@ -446,6 +1265,7 @@ pub fn LODRenderer(comptime RHI: type) type { } } else { diag.missing_region += 1; + if (profiling) |profile| profile.addRejected(); } } @@ -578,33 +1398,96 @@ pub fn LODRenderer(comptime RHI: type) type { fn onUpload(mesh: *LODMesh, ctx: *anyopaque) rhi_types.RhiError!void { const renderer: *Self = @ptrCast(@alignCast(ctx)); const resources = LODMeshResources.fromProvider(RHI, &renderer.rhi); - if (!renderer.enable_mdi) { - return mesh.upload(resources); - } + 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())].destroyMesh(mesh); + 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.pool_gpu_capacity_bytes += compact.capacity_bytes; + result.pool_gpu_allocated_bytes += compact.allocated_bytes; + result.pool_gpu_slack_bytes += compact.free_bytes; + 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 { @@ -621,9 +1504,37 @@ pub fn LODRenderer(comptime RHI: type) type { 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, + 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); + renderer.renderFrame(frame_serial, meshes, regions, config, view_proj, camera_pos, chunk_checker, checker_ctx, use_frustum, max_distance_chunks, 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, 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, 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)); @@ -632,6 +1543,9 @@ pub fn LODRenderer(comptime RHI: type) type { }; 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, }; @@ -666,6 +1580,51 @@ fn supports_lod_indirect(comptime RenderCtx: type, comptime Query: type, comptim @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[3][0] + m[0][0], m[3][1] + m[0][1], m[3][2] + m[0][2], m[3][3] + m[0][3] }, + .{ m[3][0] - m[0][0], m[3][1] - m[0][1], m[3][2] - m[0][2], m[3][3] - m[0][3] }, + .{ m[3][0] - m[1][0], m[3][1] - m[1][1], m[3][2] - m[1][2], m[3][3] - m[1][3] }, + .{ m[3][0] + m[1][0], m[3][1] + m[1][1], m[3][2] + m[1][2], m[3][3] + m[1][3] }, + .{ m[3][0] + m[2][0], m[3][1] + m[2][1], m[3][2] + m[2][2], m[3][3] + m[2][3] }, + .{ m[3][0] - m[2][0], m[3][1] - m[2][1], m[3][2] - m[2][2], m[3][3] - m[2][3] }, + }; + 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); @@ -687,6 +1646,25 @@ fn isRegionInFrustum(frustum: Frustum, bounds: LODChunk.WorldBounds, camera_pos: } // 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 "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 "LODRenderer init/deinit lifecycle" { const allocator = std.testing.allocator; @@ -768,7 +1746,9 @@ test "LODRenderer batches pooled meshes into per-LOD indirect draws" { pub fn setInstanceBuffer(_: @This(), _: anytype) void {} pub fn setSelectionMode(_: @This(), _: bool) void {} pub fn draw(_: @This(), _: u32, _: u32, _: anytype) void {} - pub fn drawOffset(_: @This(), _: u32, _: u32, _: anytype, _: usize) 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; @@ -781,6 +1761,7 @@ test "LODRenderer batches pooled meshes into per-LOD indirect draws" { 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; @@ -824,10 +1805,31 @@ test "LODRenderer batches pooled meshes into per-LOD indirect draws" { try regions[2].put(.{ .rx = 8, .rz = 0, .lod = .lod2 }, &chunk_lod2); var mock_config = LODConfig{ .radii = .{ 16, 128, 256, 512, 1024 } }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null); + var profiling = LODProfilingCollector.init(true); + renderer.renderFrame(99, &meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .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, .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, .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" { @@ -903,6 +1905,9 @@ test "LODRenderer render draw path" { 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); @@ -945,7 +1950,7 @@ test "LODRenderer render draw path" { 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); + 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); @@ -955,7 +1960,7 @@ test "LODRenderer render draw path" { 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); + 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]); @@ -1030,7 +2035,7 @@ test "LODRenderer keeps coarse LOD visible while finer bands stream" { .radii = .{ 16, 32, 64, 100, 256 }, }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null); + 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); @@ -1104,7 +2109,7 @@ test "LODRenderer disables mask when chunks are missing inside chunk render radi } }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.missingInRadius, &checker_ctx, false, null, .terrain, null); + 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); @@ -1184,7 +2189,7 @@ test "LODRenderer chunk mask uses chunk render radius instead of LOD0 radius" { } }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.missing, &checker_ctx, false, null, .terrain, null); + 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); @@ -1258,7 +2263,7 @@ test "LODRenderer keeps mask when only outside-radius chunks are uncovered" { } }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.loaded, &checker_ctx, false, null, .terrain, null); + 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); @@ -1332,7 +2337,7 @@ test "LODRenderer keeps mask for partially covered chunk regions" { } }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.partiallyLoaded, &checker_ctx, false, null, .terrain, null); + renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, Checker.partiallyLoaded, &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); @@ -1418,7 +2423,7 @@ test "LODRenderer skips coarse LOD when finer coverage is ready" { .radii = .{ 16, 32, 64, 100, 256 }, }; - renderer.render(&meshes, ®ions, mock_config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .terrain, null); + 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); } @@ -1483,7 +2488,7 @@ test "LODRenderer always renders ready LOD0 regions" { 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); + 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]); @@ -1568,7 +2573,7 @@ test "LODRenderer keeps coarse LOD when a finer child is missing" { 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); + 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); @@ -1653,12 +2658,313 @@ test "LODRenderer resolves finer coverage across negative region boundaries" { 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); + 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, .terrain, null, &profiling); + renderer.renderFrame(1, &meshes, ®ions, config.interface(), Mat4.identity, Vec3.zero, null, null, false, null, .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, .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 balances compact indirect timing and counts only emitted streams" { + 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 true; + } + 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; @@ -1751,7 +3057,7 @@ test "LODRenderer createGPUBridge and toInterface round-trip" { 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); + 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); diff --git a/modules/world-lod/src/lod_scheduler.zig b/modules/world-lod/src/lod_scheduler.zig index 8b8f39f4..537582ab 100644 --- a/modules/world-lod/src/lod_scheduler.zig +++ b/modules/world-lod/src/lod_scheduler.zig @@ -14,6 +14,8 @@ 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; pub const CoverageFn = *const fn (ptr: *anyopaque, bounds: LODChunk.WorldBounds, checker: ChunkChecker, ctx: *anyopaque) bool; @@ -40,7 +42,15 @@ pub const SchedulerContext = struct { // 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"); @@ -59,6 +69,7 @@ const LOD1_QUEUE_CANDIDATE_LIMIT: usize = 64; const HORIZON_QUEUE_CANDIDATE_LIMIT: usize = 24; const REFINEMENT_QUEUE_CANDIDATE_LIMIT: usize = 48; 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; pub fn priorityRank(lod: LODLevel, active_lod_count: usize) usize { const lod_idx: usize = @intFromEnum(lod); @@ -134,10 +145,10 @@ pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chu const queue = ctx.gen_queues[LODLevel.count - 1]; const lod_idx: u3 = @intFromEnum(lod); - // Collect candidates and queue them nearest-first. Coarse regions must form - // contiguous fallback coverage; scattered horizon samples appear as - // isolated floating terrain islands. + // 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 }; + const max_candidates = maxQueueCandidatesForLOD(lod, active_lod_count); var candidates = std.ArrayListUnmanaged(Candidate).empty; defer candidates.deinit(ctx.allocator); @@ -166,24 +177,25 @@ pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chu const center_cx = key.rx * scale + @divFloor(scale, 2); const center_cz = key.rz * scale + @divFloor(scale, 2); const encoded_priority = encodePriority(lod, center_cx - ctx.player_cx, center_cz - ctx.player_cz, velocity, active_lod_count); - try candidates.append(ctx.allocator, .{ .key = key, .encoded_priority = encoded_priority }); + const candidate = Candidate{ .key = key, .encoded_priority = encoded_priority }; + var insert_at: usize = 0; + while (insert_at < candidates.items.len and candidates.items[insert_at].encoded_priority <= encoded_priority) : (insert_at += 1) {} + if (insert_at < max_candidates) { + try candidates.insert(ctx.allocator, insert_at, candidate); + if (candidates.items.len > max_candidates) _ = candidates.pop(); + } diag.candidates += 1; } } - const max_candidates = maxQueueCandidatesForLOD(lod, active_lod_count); - std.mem.sort(Candidate, candidates.items, {}, struct { - fn lessThan(_: void, a: Candidate, b: Candidate) bool { - return a.encoded_priority < b.encoded_priority; - } - }.lessThan); - 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| { if (queued_count >= max_candidates) break; if (ctx.pending_regions) |pending| { @@ -192,7 +204,15 @@ pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chu const existing = storage.get(cand.key); if (existing != null) diag.existing += 1; - const needs_queue = if (existing) |chunk| chunk.getState() == .missing else true; + if (existing == null and resident_regions >= ctx.resident_region_limit) 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.*) 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: { @@ -200,6 +220,10 @@ pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chu 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; }; @@ -207,7 +231,17 @@ pub fn queueLODRegions(ctx: SchedulerContext, lod: LODLevel, velocity: Vec3, chu ctx.next_job_token.* += 1; 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, @@ -273,7 +307,7 @@ test "LOD scheduling seeds horizon before detailed refinements" { try std.testing.expectEqual(@as(usize, 3), priorityLevelIndex(4, LODLevel.count)); } -test "LOD scheduling queues LOD0 generation jobs" { +test "LOD scheduling caps resident regions and logical admission memory" { const allocator = std.testing.allocator; var regions: [LODLevel.count]RegionMap = undefined; @@ -306,6 +340,9 @@ test "LOD scheduling queues LOD0 generation jobs" { var mutex: sync.RwLock = .{}; var next_job_token: u32 = 1; var radius_reduction = [_]i32{0} ** 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 { @@ -328,10 +365,17 @@ test "LOD scheduling queues LOD0 generation jobs" { .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.expect(queue.count() > 0); + 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); diff --git a/modules/world-lod/src/lod_stats.zig b/modules/world-lod/src/lod_stats.zig index a8593710..3f7efa2b 100644 --- a/modules/world-lod/src/lod_stats.zig +++ b/modules/world-lod/src/lod_stats.zig @@ -1,11 +1,542 @@ //! 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, @@ -14,6 +545,25 @@ pub const LODStats = struct { 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, @@ -29,10 +579,31 @@ pub const LODStats = struct { 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; @@ -47,6 +618,7 @@ pub const LODStats = struct { } 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; @@ -54,6 +626,23 @@ pub const LODStats = struct { 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; @@ -62,10 +651,25 @@ pub const LODStats = struct { 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 { @@ -81,8 +685,8 @@ pub const LODStats = struct { } pub fn addMemory(self: *LODStats, bytes: usize) void { - const mb = bytes / (1024 * 1024); - self.memory_used_mb += @intCast(mb); + self.memory_used_bytes += @intCast(bytes); + self.memory_used_mb = @intCast(self.memory_used_bytes / (1024 * 1024)); } pub fn cacheHitRate(self: *const LODStats) f32 { @@ -92,8 +696,6 @@ pub const LODStats = struct { } }; -const std = @import("std"); - test "LODStats reports cache hit rate" { var stats = LODStats{}; try std.testing.expectEqual(@as(f32, 0.0), stats.cacheHitRate()); @@ -102,3 +704,100 @@ test "LODStats reports cache hit rate" { 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 index 48b83738..34f8c365 100644 --- a/modules/world-lod/src/lod_store.zig +++ b/modules/world-lod/src/lod_store.zig @@ -18,6 +18,7 @@ pub const StoreHeader = struct { pub const StoreError = error{ CorruptContainer, + StoreSizeLimit, }; pub fn headersMatch(a: StoreHeader, b: StoreHeader) bool { @@ -129,6 +130,147 @@ 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); @@ -152,37 +294,87 @@ pub fn writePayload(allocator: std.mem.Allocator, save_dir_path: []const u8, key 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 = 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| { + 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; }; - tmp_file.close(); + region.close(); } - var region = (if (use_existing) - RegionFile.open(allocator, tmp_path) - else - RegionFile.create(allocator, tmp_path)) catch |err| { + 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; }; - - region.writeChunk(localCoord(key.rx), localCoord(key.rz), bytes) catch |err| { - region.close(); + 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 {}; - return err; - }; - region.close(); + // 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 { @@ -196,6 +388,16 @@ pub fn deletePayload(allocator: std.mem.Allocator, save_dir_path: []const u8, ke 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(); @@ -274,6 +476,83 @@ test "LOD store write replaces corrupt containers" { 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(); @@ -311,7 +590,7 @@ test "LOD store writes metadata header atomically" { 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\": 10") != null); + try testing.expect(std.mem.indexOf(u8, bytes, "\"lod_data_version\": 11") != null); } test "LOD store reads and deletes metadata store" { diff --git a/modules/world-lod/src/lod_tile.zig b/modules/world-lod/src/lod_tile.zig new file mode 100644 index 00000000..2ff9b2a7 --- /dev/null +++ b/modules/world-lod/src/lod_tile.zig @@ -0,0 +1,839 @@ +//! 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_upload_queue.zig b/modules/world-lod/src/lod_upload_queue.zig index 09510fef..220d8851 100644 --- a/modules/world-lod/src/lod_upload_queue.zig +++ b/modules/world-lod/src/lod_upload_queue.zig @@ -12,11 +12,14 @@ 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. @@ -27,8 +30,16 @@ pub const LODGPUBridge = struct { 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); @@ -64,6 +75,33 @@ pub const LODGPUBridge = struct { 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. @@ -78,6 +116,19 @@ pub const LODRenderLayer = enum { 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 { @@ -95,7 +146,42 @@ pub const LODRenderInterface = struct { 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, + 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, + 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. @@ -114,11 +200,44 @@ pub const LODRenderInterface = struct { 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, + 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); + 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, 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, 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, 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_vertex_pool.zig b/modules/world-lod/src/lod_vertex_pool.zig index e63147cc..39e34399 100644 --- a/modules/world-lod/src/lod_vertex_pool.zig +++ b/modules/world-lod/src/lod_vertex_pool.zig @@ -12,6 +12,7 @@ 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; @@ -27,6 +28,13 @@ const AllocationRecord = struct { 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. @@ -38,6 +46,10 @@ pub const LODVertexPool = struct { 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 = .{}, @@ -62,6 +74,7 @@ pub const LODVertexPool = struct { } self.free_blocks.deinit(self.allocator); self.allocations.deinit(self.allocator); + self.retired_ranges.deinit(self.allocator); self.capacity_bytes = 0; } @@ -87,7 +100,7 @@ pub const LODVertexPool = struct { }; if (pending.len == 0) { - self.freeMeshUnlocked(mesh); + if (self.findRecordIndexUnlocked(mesh)) |idx| self.retireRecordUnlocked(idx, true, mesh); mesh.markEmptyUploadedUnlocked(); self.allocator.free(pending); mesh.pending_vertices = null; @@ -135,7 +148,7 @@ pub const LODVertexPool = struct { const new_size = record.size; if (using_new_record) { if (old_record_index) |idx| { - self.freeRecordUnlocked(idx, false, mesh); + self.retireRecordUnlocked(idx, false, mesh); } } @@ -167,6 +180,45 @@ pub const LODVertexPool = struct { 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(); @@ -179,6 +231,35 @@ pub const LODVertexPool = struct { 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(); @@ -250,7 +331,6 @@ pub const LODVertexPool = struct { } if (old_handle != 0) { - resources.waitIdle(); resources.destroyBuffer(old_handle); } if (old_shadow) |shadow| self.allocator.free(shadow); @@ -298,6 +378,21 @@ pub const LODVertexPool = struct { _ = 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; @@ -327,6 +422,7 @@ pub const LODVertexPool = struct { 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; @@ -338,21 +434,51 @@ pub const LODVertexPool = struct { } }.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| { - if (record.offset != cursor) { - try lod_mesh.updateBufferChunked(resources, self.buffer_handle, cursor, self.shadow[record.offset .. record.offset + record.size]); - std.mem.copyForwards(u8, self.shadow[cursor .. cursor + record.size], self.shadow[record.offset .. record.offset + record.size]); - record.offset = cursor; - setMeshPoolLocation(record.mesh, self.buffer_handle, cursor, locked_mesh); - } + 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 { @@ -365,6 +491,7 @@ pub const LODVertexPool = struct { 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; } @@ -536,7 +663,30 @@ test "LODVertexPool grows and preserves pooled handles" { 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, 1), resources.wait_idle); + 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); } @@ -576,14 +726,55 @@ test "LODVertexPool compacts fragmented ranges" { 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 }; @@ -630,3 +821,29 @@ test "LODVertexPool upload failure preserves existing renderable allocation" { 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 index be92c203..fe8a8e63 100644 --- a/modules/world-lod/src/root.zig +++ b/modules/world-lod/src/root.zig @@ -13,6 +13,7 @@ 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"); @@ -23,6 +24,8 @@ 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; @@ -41,5 +44,11 @@ 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 new file mode 100644 index 00000000..0605156d --- /dev/null +++ b/modules/world-lod/src/tests.zig @@ -0,0 +1,12 @@ +//! 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"); +} diff --git a/modules/world-lod/src/world_lod.zig b/modules/world-lod/src/world_lod.zig index 960f11a3..7ad25928 100644 --- a/modules/world-lod/src/world_lod.zig +++ b/modules/world-lod/src/world_lod.zig @@ -102,6 +102,12 @@ pub fn WorldLOD(comptime RHI: type) type { 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, diff --git a/modules/world-persistence/src/region_file.zig b/modules/world-persistence/src/region_file.zig index 5b09795c..3642c41e 100644 --- a/modules/world-persistence/src/region_file.zig +++ b/modules/world-persistence/src/region_file.zig @@ -125,6 +125,7 @@ pub const RegionFile = struct { const total_len: u32 = @intCast(compressed.len + 1); const sectors_needed = (total_len + 4 + SECTOR_SIZE - 1) / SECTOR_SIZE; + if (sectors_needed > std.math.maxInt(u8)) return RegionError.FileTooShort; const idx = @as(u32, local_z) * 32 + @as(u32, local_x); const old_entry = self.header[idx]; diff --git a/modules/world-runtime/src/world.zig b/modules/world-runtime/src/world.zig index ef1700d3..f8c5c9f7 100644 --- a/modules/world-runtime/src/world.zig +++ b/modules/world-runtime/src/world.zig @@ -905,6 +905,13 @@ pub const World = struct { } } + /// 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(); + } + /// Returns a resident chunk or creates storage for it. /// May allocate chunk data and enqueue follow-up generation or meshing work. Propagates errors from streaming, persistence, meshing, or mutation subsystems. pub fn getOrCreateChunk(self: *World, chunk_x: i32, chunk_z: i32) !*ChunkData { @@ -985,6 +992,14 @@ pub const World = struct { 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| { + lod.manager.prepareFrame(self.renderer.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkRenderable, @ptrCast(&self.storage), null); + } + } + /// 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 { @@ -1164,11 +1179,17 @@ 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); diff --git a/modules/world-runtime/src/world_facade_tests.zig b/modules/world-runtime/src/world_facade_tests.zig index e4940fcc..9fc68d37 100644 --- a/modules/world-runtime/src/world_facade_tests.zig +++ b/modules/world-runtime/src/world_facade_tests.zig @@ -85,6 +85,8 @@ const MockWorld = struct { 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; diff --git a/modules/world-runtime/src/world_renderer.zig b/modules/world-runtime/src/world_renderer.zig index 3b9e84fd..39d8f505 100644 --- a/modules/world-runtime/src/world_renderer.zig +++ b/modules/world-runtime/src/world_renderer.zig @@ -13,6 +13,7 @@ const rhi_mod = @import("engine-rhi").rhi; 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"); @@ -56,6 +57,11 @@ pub fn chooseGpuBlockCapacity(vram_mb: usize) usize { return @min(MAX_MDI_CHUNKS, max_by_budget); } +fn gpuBlockCapacityForBudgetMb(budget_mb: usize) usize { + const max_by_budget = (budget_mb * MB) / GPU_BLOCK_SLOT_SIZE; + return @min(MAX_MDI_CHUNKS, max_by_budget); +} + pub const RenderStats = struct { chunks_total: u32 = 0, chunks_rendered: u32 = 0, @@ -103,6 +109,7 @@ pub const WorldRenderer = struct { rm: ResourceManager, render_ctx: RenderContext, query: IDeviceQuery, + timing: IDeviceTiming, culling_screen_size: rhi_mod.RenderResolution, vertex_allocator: *GlobalVertexAllocator, @@ -153,8 +160,9 @@ pub const WorldRenderer = struct { const vram_bytes = query.getDeviceLocalVramBytes(); const vram_mb = vram_bytes / (1024 * 1024); - const vertex_capacity_mb = chooseVertexCapacityMb(vram_mb, strict_safe_mode); - const gpu_block_capacity = chooseGpuBlockCapacity(vram_mb); + const vertex_capacity_mb = runtime_env.envInt("ZIGCRAFT_VERTEX_CAPACITY_MB", chooseVertexCapacityMb(vram_mb, strict_safe_mode)); + const gpu_block_budget_mb = runtime_env.envInt("ZIGCRAFT_GPU_BLOCK_BUDGET_MB", 0); + const gpu_block_capacity = if (gpu_block_budget_mb > 0) gpuBlockCapacityForBudgetMb(gpu_block_budget_mb) else chooseGpuBlockCapacity(vram_mb); log.log.info("VRAM budget: {}MB | vertex_allocator: {}MB | gpu_block_buffer: {} slots", .{ vram_mb, vertex_capacity_mb, gpu_block_capacity }); @@ -221,6 +229,7 @@ pub const WorldRenderer = struct { .rm = rm, .render_ctx = render_ctx, .query = query, + .timing = rhi.timing(), .culling_screen_size = culling_screen_size, .vertex_allocator = vertex_allocator, .visible_chunks = .empty, @@ -321,10 +330,14 @@ pub const WorldRenderer = struct { if (render_lod) { if (lod_manager) |lod_mgr| { if (layer != .fluid) { - lod_mgr.render(view_proj, camera_pos, ChunkStorage.isChunkRenderable, @ptrCast(self.storage), true, null, LODRenderLayer.terrain); + self.timing.beginPassTiming("LODTerrainPass"); + lod_mgr.renderFrame(self.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkRenderable, @ptrCast(self.storage), true, null, LODRenderLayer.terrain); + self.timing.endPassTiming("LODTerrainPass"); } if (layer != .terrain and parseEnabledEnv(getenv("ZIGCRAFT_LOD_WATER"), true)) { - lod_mgr.render(view_proj, camera_pos, ChunkStorage.isChunkRenderable, @ptrCast(self.storage), true, null, LODRenderLayer.fluid); + self.timing.beginPassTiming("LODWaterPass"); + lod_mgr.renderFrame(self.frame_serial, view_proj, camera_pos, ChunkStorage.isChunkRenderable, @ptrCast(self.storage), true, null, LODRenderLayer.fluid); + self.timing.endPassTiming("LODWaterPass"); } } } diff --git a/phase5_benchmark_gate_tests.zig b/phase5_benchmark_gate_tests.zig new file mode 100644 index 00000000..0ba2bb5e --- /dev/null +++ b/phase5_benchmark_gate_tests.zig @@ -0,0 +1,179 @@ +//! 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 new file mode 100644 index 00000000..462d8cef --- /dev/null +++ b/scripts/benchmark_baseline.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +"""Validate, pair, assemble, and compare reproducible benchmark evidence. + +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 +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]: + with path.open(encoding="utf-8") as handle: + value = json.load(handle) + if not isinstance(value, dict): + raise ValueError(f"{path}: JSON root must be an object") + return value + + +def require(value: dict[str, Any], path: str, source: str) -> Any: + try: + return get_path(value, path) + except (KeyError, TypeError) as exc: + raise ValueError(f"{source}: missing {path}") from exc + + +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 validate_result(value: dict[str, Any], source: str = "result") -> 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: + 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 + + +def assemble(inputs: 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): + 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, {})] + 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, + } + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(artifact, 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 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") + + +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() + + +if __name__ == "__main__": + try: main() + except (ValueError, OSError, 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 new file mode 100644 index 00000000..3deaee39 --- /dev/null +++ b/scripts/check_phase5_benchmark_config.sh @@ -0,0 +1,33 @@ +#!/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 new file mode 100644 index 00000000..d236350a --- /dev/null +++ b/scripts/check_phase5_visual_smoke.py @@ -0,0 +1,541 @@ +#!/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 377630ea..ccae8449 100755 --- a/scripts/compare_benchmarks.sh +++ b/scripts/compare_benchmarks.sh @@ -4,6 +4,7 @@ set -euo pipefail warn_threshold=5 fail_threshold=10 preset="" +scenario="" if [[ $# -lt 2 ]]; then printf 'Usage: %s baseline.json new.json\n' "$0" >&2 @@ -20,6 +21,10 @@ while [[ $# -gt 0 ]]; do preset="$2" shift 2 ;; + --scenario) + scenario="$2" + shift 2 + ;; --warn) warn_threshold="$2" shift 2 @@ -39,16 +44,15 @@ read_json() { jq -r "$1" "$2" } +[[ -n "$preset" && -n "$scenario" ]] || { printf 'Schema-v2 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" baseline_source="$baseline" -baseline_generated=$(read_json '.generated' "$baseline") -if [[ "$baseline_generated" != "true" ]]; then - printf 'Baseline placeholder detected; refusing to skip benchmark regression gating.\n' >&2 - exit 1 -fi if [[ -n "$preset" ]]; then baseline_source=$(mktemp) - jq -c --arg preset "$preset" '.presets[$preset]' "$baseline" > "$baseline_source" + jq -c --arg preset "$preset" --arg scenario "$scenario" '.results[$preset][$scenario]' "$baseline" > "$baseline_source" if [[ "$(read_json 'type' "$baseline_source")" == "null" ]]; then printf 'Baseline preset not found: %s\n' "$preset" >&2 exit 2 @@ -75,8 +79,8 @@ baseline_fps_avg=$(read_json '.fps.avg' "$baseline_source") new_fps_avg=$(read_json '.fps.avg' "$new") baseline_fps_p1=$(read_json '.fps.p1' "$baseline_source") new_fps_p1=$(read_json '.fps.p1' "$new") -baseline_gpu=$(read_json '.gpu_ms.total_avg' "$baseline_source") -new_gpu=$(read_json '.gpu_ms.total_avg' "$new") +baseline_gpu=$(read_json '.gpu_ms.total.avg' "$baseline_source") +new_gpu=$(read_json '.gpu_ms.total.avg' "$new") baseline_draws=$(read_json '.draw_calls_avg' "$baseline_source") new_draws=$(read_json '.draw_calls_avg' "$new") diff --git a/scripts/run_benchmark.sh b/scripts/run_benchmark.sh index bf51137f..a450b0aa 100755 --- a/scripts/run_benchmark.sh +++ b/scripts/run_benchmark.sh @@ -3,8 +3,22 @@ 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 @@ -16,6 +30,20 @@ while [[ $# -gt 0 ]]; do presets="$2" 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 ;; + --overwrite) overwrite=true; shift ;; --output-dir) output_dir="$2" shift 2 @@ -31,17 +59,66 @@ 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; } +[[ "$benchmark_lod_memory_budget_mb" =~ ^[0-9]+$ ]] && (( benchmark_lod_memory_budget_mb <= 4096 )) || { printf 'Benchmark LOD memory budget must be an integer from 0 to 4096 MiB: %s\n' "$benchmark_lod_memory_budget_mb" >&2; exit 2; } +[[ "$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 - output_file="$output_dir/$preset.json" + for scenario in "${scenario_list[@]}"; do + output_file="$output_dir/$compact_mode/$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-preset="$preset" -Dbenchmark-duration="$duration" -Dbenchmark-output="$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" + ) if [[ -n "${IN_NIX_SHELL:-}" ]]; then - ZIGCRAFT_SAFE_MODE=1 timeout --preserve-status "${per_preset_timeout}s" "${benchmark_cmd[@]}" + env "${benchmark_env[@]}" timeout --preserve-status "${per_preset_timeout}s" "${benchmark_cmd[@]}" else - ZIGCRAFT_SAFE_MODE=1 timeout --preserve-status "${per_preset_timeout}s" nix develop --command "${benchmark_cmd[@]}" + env "${benchmark_env[@]}" timeout --preserve-status "${per_preset_timeout}s" nix develop --command "${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" + done +done done diff --git a/scripts/run_phase5_visual_smoke.sh b/scripts/run_phase5_visual_smoke.sh new file mode 100644 index 00000000..59e3ab31 --- /dev/null +++ b/scripts/run_phase5_visual_smoke.sh @@ -0,0 +1,110 @@ +#!/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 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. Give real world + # generation wall-clock time to settle instead of racing a fast GPU's frame + # counter and failing the readiness assertion at frame 900. + if [[ "$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 "$capture_timeout" nix develop --command 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/scripts/validate_benchmark_artifact.sh b/scripts/validate_benchmark_artifact.sh new file mode 100644 index 00000000..ad865f89 --- /dev/null +++ b/scripts/validate_benchmark_artifact.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +[[ $# -eq 2 ]] || exit 2 +kind=$1 file=$2 +case "$kind" in + --baseline) exec python3 "$(dirname "$0")/benchmark_baseline.py" validate "$file" ;; + --result) exec python3 "$(dirname "$0")/benchmark_baseline.py" validate-result "$file" ;; + *) exit 2 ;; +esac diff --git a/src/game/app.zig b/src/game/app.zig index 69207620..b5542dcc 100644 --- a/src/game/app.zig +++ b/src/game/app.zig @@ -15,6 +15,7 @@ const InputMapper = @import("game-core").InputMapper; const RenderSystem = @import("engine-graphics").RenderSystem; const AudioSystemManager = @import("audio_system_manager.zig").AudioSystemManager; const BenchmarkRunner = @import("game-core").BenchmarkRunner; +const BENCHMARK_WORLD_SEED = @import("game-core").BENCHMARK_WORLD_SEED; const json_presets = @import("game-core").settings.json_presets; const SettingsManager = @import("game-core").SettingsManager; @@ -81,9 +82,13 @@ 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, }; } @@ -124,15 +129,19 @@ pub const App = struct { errdefer settings_manager.deinit(); if (build_options.benchmark) { - applyBenchmarkPreset(settings_manager.ptr(), build_options.benchmark_preset); - if (build_options.benchmark_render_distance > 0) { - settings_manager.settings.render_distance = build_options.benchmark_render_distance; - settings_manager.settings.horizon_distance = build_options.benchmark_render_distance; - } + applyBenchmarkSettings(settings_manager.ptr(), build_options.benchmark_preset, build_options.benchmark_render_distance, build_options.benchmark_horizon_distance); } - if (build_options.auto_preset.len > 0) { + // 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. + if (build_options.auto_preset.len > 0 and !build_options.benchmark) { _ = applyNamedPreset(settings_manager.ptr(), build_options.auto_preset, "AUTO PRESET"); } + if (build_options.screenshot_path.len > 0) { + // Captures are regression artifacts, not a replay of persisted + // developer visualization toggles. + settings_manager.settings.wireframe_enabled = false; + } if (build_options.shadow_test_scene) { applyShadowTestPreset(settings_manager.ptr()); } @@ -196,9 +205,25 @@ pub const App = struct { var benchmark_runner: ?*BenchmarkRunner = null; if (build_options.benchmark) { + // Benchmarks include GPU pass breakdowns, including LOD passes. + render_system.getRHI().timing().setTimingEnabled(true); const runner = try allocator.create(BenchmarkRunner); const benchmark_duration_s: f32 = @as(f32, @floatFromInt(build_options.benchmark_duration)); - runner.* = try BenchmarkRunner.init(allocator, build_options.benchmark_preset, settings_manager.settings.render_distance, benchmark_duration_s, build_options.benchmark_output); + runner.* = try BenchmarkRunner.init( + allocator, + 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; } @@ -232,7 +257,7 @@ pub const App = struct { // temporary manager value used during initialization. app.input.setRawEventProcessor(.{ .context = &app.ui_manager, .process = UISystemManager.processRawEvent }); - if (build_options.smoke_test or build_options.screenshot_path.len > 0 or build_options.benchmark) { + if (build_options.smoke_test or build_options.screenshot_path.len > 0) { app.render_system.getRHI().timing().setTimingEnabled(true); } @@ -251,7 +276,7 @@ pub const App = struct { } } else if (build_options.benchmark) { log.log.info("BENCHMARK MODE: Deferring world launch until swapchain settles", .{}); - app.pending_world_launch = .{ .seed = 12345, .generator_index = 0 }; + app.pending_world_launch = .{ .seed = BENCHMARK_WORLD_SEED, .generator_index = 0 }; if (runtime_env.strictSafeModeAutoEnabled()) app.direct_launch_resize_guard_frames = 240; } else if (resolveAutoWorldGenerator()) |generator_index| { log.log.info("AUTO WORLD MODE: Deferring '{s}' world launch until swapchain settles", .{build_options.auto_world}); @@ -474,7 +499,13 @@ pub const App = struct { self.pending_world_launch == null and stats.chunks_rendered > 0 and stats.gen_queue == 0 and stats.mesh_queue == 0 and stats.upload_queue == 0 else false; - if (!requires_world_ready or world_ready) { + // The world statistics can settle one frame before the render graph + // has emitted its first scene draw. Require a rendered frame as + // 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) { self.screenshot_settle_frames += 1; } else { self.screenshot_settle_frames = 0; @@ -489,34 +520,48 @@ pub const App = struct { } else |_| {} } + const capture_ready = self.smoke_test_frames >= target_frames and self.screenshot_settle_frames >= 30; const screenshot_delay_ready = build_options.screenshot_path.len > 0 and build_options.screenshot_delay_seconds > 0; - const should_finish = if (screenshot_delay_ready) blk: { - const target_loaded = self.pending_world_launch == null and self.screen_manager.stack.items.len > 0; - if (!target_loaded) break :blk false; - + const should_finish = if (screenshot_delay_ready and capture_ready) blk: { + // A delay is additional settling time, not a replacement for + // readiness. The old branch captured after wall time alone, + // often while the auto world was still displaying its black + // loading frame in unthrottled headless runs. const start = self.screenshot_delay_start orelse start: { self.screenshot_delay_start = self.time.elapsed; break :start self.time.elapsed; }; const delay_s: f32 = @floatFromInt(build_options.screenshot_delay_seconds); break :blk self.time.elapsed - start >= delay_s; - } else self.smoke_test_frames >= target_frames and self.screenshot_settle_frames >= 30; + } else capture_ready and !screenshot_delay_ready; - if (build_options.screenshot_path.len > 0 and requires_world_ready and self.smoke_test_frames >= target_frames + 1800 and self.screenshot_settle_frames < 30) { + // Headless frames are intentionally unthrottled. A frame-count + // timeout can therefore expire before generation workers receive + // meaningful CPU time; use a wall-time bound instead. + if (build_options.screenshot_path.len > 0 and requires_world_ready and self.time.elapsed >= 90.0 and !capture_ready) { return error.ScreenshotWorldNotReady; } 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", .{}); + return error.ScreenshotCaptureFailed; } } finish_screenshot_run = true; } } + // Screenshot requests must reach the RHI while this frame's command + // buffer is still open. Vulkan records the readback after its final + // output pass and before submission/presentation. self.render_system.endFrame(); self.revealMenuWindowWhenReady(); @@ -612,6 +657,26 @@ 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 { + 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" { + var settings = Settings{}; + applyBenchmarkSettings(&settings, "high", 12, 4096); + 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 { const bytes = stats.total_buffer_memory + stats.total_texture_memory; return @as(f32, @floatFromInt(bytes)) / (1024.0 * 1024.0); diff --git a/src/game/player_tests.zig b/src/game/player_tests.zig index 77183901..43b5af55 100644 --- a/src/game/player_tests.zig +++ b/src/game/player_tests.zig @@ -304,8 +304,9 @@ test "Player block mutation errors are handled" { return undefined; } fn graphicsRenderView(ptr: *anyopaque) @import("engine-rhi").IWorldRenderView { - return .{ .ptr = ptr, .vtable = &.{ .render = render, .renderOpaque = render, .renderFluid = render } }; + 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 }; } diff --git a/src/integration_test.zig b/src/integration_test.zig index 8d95c405..041dc10b 100644 --- a/src/integration_test.zig +++ b/src/integration_test.zig @@ -8,6 +8,7 @@ const std = @import("std"); const testing = std.testing; +const fs = @import("fs"); const App = @import("game/app.zig").App; const build_options = @import("build_options"); @@ -17,10 +18,114 @@ 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; 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. +fn initStorageOnlyPersistenceWorld(allocator: std.mem.Allocator) world_runtime.World { + return .{ + .storage = world_meshing.ChunkStorage.init(allocator), + .streamer = undefined, + .renderer = undefined, + .allocator = allocator, + .generator = undefined, + .render_distance = 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, + }; +} + +fn deinitStorageOnlyPersistenceWorld(world: *world_runtime.World) void { + if (world.save_manager) |save_manager| { + save_manager.deinit(); + world.save_manager = null; + } + world.storage.deinitWithoutRHI(); +} + const UploadScreen = struct { context: EngineContext, buffer: rhi.BufferHandle, @@ -120,3 +225,112 @@ 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" { + const allocator = testing.allocator; + 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_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. + 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); + edited_data.chunk.generated = true; + var y: u32 = 0; + while (y <= 96) : (y += 1) { + edited_data.chunk.setBlock(0, y, 0, if (y == 96) .grass else .stone); + } + source_world.saveAllModifiedChunks(); + try testing.expect(!edited_data.chunk.modified); + try testing.expectEqual(@as(usize, 0), source_world.takeSaveFailureWarningCount()); + deinitStorageOnlyPersistenceWorld(&source_world); + + var reloaded_world = initStorageOnlyPersistenceWorld(allocator); + defer deinitStorageOnlyPersistenceWorld(&reloaded_world); + reloaded_world.save_manager = try SaveManager.init(allocator, save_path, "edit-persistence", 923, "integration"); + var reloaded_chunk = world_core.Chunk.init(0, 0); + 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/integration_test_robustness.zig b/src/integration_test_robustness.zig index 5ac1aeea..fa287669 100644 --- a/src/integration_test_robustness.zig +++ b/src/integration_test_robustness.zig @@ -3,12 +3,10 @@ const testing = std.testing; const fs = @import("fs"); const c = @import("c").c; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { std.debug.print("Running integration tests...\n", .{}); - var gpa: std.heap.DebugAllocator(.{}) = .init; - defer _ = gpa.deinit(); - const allocator = gpa.allocator(); + const allocator = init.gpa; // Find the robust-demo executable // Typically in zig-out/bin/robust-demo or similar @@ -18,7 +16,7 @@ pub fn main() !void { std.debug.print("Found robust-demo at: {s}\n", .{robust_demo_path}); // Run the demo - const run_result = try std.process.run(allocator, std.Options.debug_io, .{ + const run_result = try std.process.run(allocator, init.io, .{ .argv = &[_][]const u8{robust_demo_path}, .stdout_limit = .limited(4096), .stderr_limit = .limited(4096), @@ -27,14 +25,15 @@ pub fn main() !void { defer allocator.free(run_result.stderr); const stdout = run_result.stdout; + const stderr = run_result.stderr; const result = run_result.term; // Check exit code switch (result) { - .Exited => |code| { + .exited => |code| { if (code != 0) { std.debug.print("robust-demo failed with exit code {d}\n", .{code}); - std.debug.print("Output:\n{s}\n", .{stdout}); + std.debug.print("stdout:\n{s}\nstderr:\n{s}\n", .{ stdout, stderr }); return error.DemoFailed; } }, @@ -46,9 +45,9 @@ pub fn main() !void { // Verify expected output const expected_msg = "[SUCCESS] Command completed successfully. Robustness2 prevented device loss."; - if (std.mem.indexOf(u8, stdout, expected_msg) == null) { + if (std.mem.indexOf(u8, stdout, expected_msg) == null and std.mem.indexOf(u8, stderr, expected_msg) == null) { std.debug.print("robust-demo did not output expected success message.\n", .{}); - std.debug.print("Output:\n{s}\n", .{stdout}); + std.debug.print("stdout:\n{s}\nstderr:\n{s}\n", .{ stdout, stderr }); return error.VerificationFailed; } diff --git a/src/interface_mock_tests.zig b/src/interface_mock_tests.zig index 42e05aec..7976d0d2 100644 --- a/src/interface_mock_tests.zig +++ b/src/interface_mock_tests.zig @@ -293,6 +293,8 @@ const MockWorld = struct { 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); } @@ -471,7 +473,7 @@ const MockWorld = struct { } fn graphicsRenderView(ptr: *anyopaque) GraphicsWorldRenderView { - return .{ .ptr = ptr, .vtable = &.{ .render = render, .renderOpaque = renderOpaque, .renderFluid = renderFluid } }; + return .{ .ptr = ptr, .vtable = &.{ .prepareLODCulling = prepareLODCulling, .render = render, .renderOpaque = renderOpaque, .renderFluid = renderFluid } }; } fn getGpuMeshDispatch(ptr: *anyopaque) GpuMeshDispatch { diff --git a/src/tests.zig b/src/tests.zig index d5e63811..93f762d1 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -39,6 +39,7 @@ test { _ = @import("engine-graphics").descriptor_manager_tests; _ = @import("engine-graphics").descriptor_manager_error_tests; _ = @import("engine-graphics").shader_registry_tests; + _ = @import("engine-graphics").screenshot_tests; _ = @import("engine-graphics").frame_manager_tests; _ = @import("engine-graphics").final_composition; _ = @import("engine-graphics").render_pass_manager_tests; @@ -48,7 +49,9 @@ 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; _ = @import("engine-shadows").shadow_cascade_tests; _ = @import("engine-graphics").shadow_tests; @@ -69,10 +72,6 @@ test { _ = @import("world-worldgen").terrain_modifier_tests; _ = @import("world-worldgen").terrain_shape_generator_tests; _ = @import("world-worldgen").terrain_report; - _ = @import("world-lod").lod_manager_tests; - _ = @import("world-lod").lod_manager_internal_tests; - _ = @import("world-lod").lod_seam; - _ = @import("world-lod").lod_renderer; _ = @import("engine-atmosphere").atmosphere_tests; _ = @import("game-core").settings_tests; _ = @import("game-core").input_settings;