From 1276fd11e3ea8479d06aa6070c61e2fc3b532f07 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 19 Oct 2026 17:30:49 +0200 Subject: [PATCH] wip(assembler): round L1 -- Linux baseline on abyss: Linux world identical to Windows Headless Linux benchmark/hash tool (Clang 19 / GCC 14) sharing the viewer's plan builder and streaming pipeline (moved verbatim into StreamBenchmarkPipeline.h / WorldAssemblyPrototype.h). Owner Round17 view: density/mesh/payload/topology hashes equal the Windows S13 reference at W1/W8/W12 (Clang, 3 runs each) and W8 (GCC), with -ffp-contract=off. No output change. Baseline (i9-12900H, CPUs 0-11, powersave/balance_performance): whole view W8 3.562 s median, W12 3.512 s, W1 16.555 s; peak RSS < 605 MiB. Review: the default-stack crash is 17.3 MiB of static TLS per thread (glibc puts it in the thread stack; HeightfieldColumnCache 6.8 MB), not recursion -- open, after S14. MSVC compile of the viewer refactor not verified (no MSVC here). Co-Authored-By: Claude Opus 5.5 --- CODEMAP.md | 10 +- Standalone/BuildLinux.sh | 118 +++ Standalone/Compat/CoreMinimal.h | 1 + Standalone/Compat/StandaloneDataAssetGlue.cpp | 5 + Standalone/Core/src/CoreNamespacePreamble.h | 1 + .../WorldAssemblerM3/Linux1/MeasureProcess.py | 34 + .../WorldAssemblerM3/Linux1/REPORT.md | 153 ++++ Standalone/Source/StandaloneViewer.cpp | 765 +----------------- Standalone/Source/StreamBenchmarkHeadless.cpp | 186 +++++ Standalone/Source/StreamBenchmarkPipeline.h | 368 +++++++++ Standalone/Source/WorldAssemblyPrototype.h | 427 ++++++++++ 11 files changed, 1308 insertions(+), 760 deletions(-) create mode 100755 Standalone/BuildLinux.sh create mode 100755 Standalone/Research/WorldAssemblerM3/Linux1/MeasureProcess.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux1/REPORT.md create mode 100644 Standalone/Source/StreamBenchmarkHeadless.cpp create mode 100644 Standalone/Source/StreamBenchmarkPipeline.h create mode 100644 Standalone/Source/WorldAssemblyPrototype.h diff --git a/CODEMAP.md b/CODEMAP.md index 6c0939b..bcc0087 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -874,8 +874,14 @@ round-2 matched capture source and results are under [`Standalone/Research/WorldAssemblerM3/Round2/`](Standalone/Research/WorldAssemblerM3/Round2/). `RefineLod8NativeCap` in [`Standalone/Core/src/VoxelForgeCore.cpp`](Standalone/Core/src/VoxelForgeCore.cpp) replaces coarse LOD8 ceiling surfaces with the planned 2D cap field while retaining ceiling skirts. -The standalone SurfaceWorld plan composer and capture/audit entry point live in -[`Standalone/Source/StandaloneViewer.cpp`](Standalone/Source/StandaloneViewer.cpp). +The standalone SurfaceWorld plan composer (`AttachRound1WorldAssemblyPrototype`) lives in +[`Standalone/Source/WorldAssemblyPrototype.h`](Standalone/Source/WorldAssemblyPrototype.h), and the +streaming benchmark/hash pipeline (`RunPipelinedStreamingBenchmark`, `HashTileRows`) in +[`Standalone/Source/StreamBenchmarkPipeline.h`](Standalone/Source/StreamBenchmarkPipeline.h); both are +shared by the D3D11 viewer ([`StandaloneViewer.cpp`](Standalone/Source/StandaloneViewer.cpp), which +keeps the capture/audit entry points) and the Linux headless tool +([`StreamBenchmarkHeadless.cpp`](Standalone/Source/StreamBenchmarkHeadless.cpp), built by +`Standalone/BuildLinux.sh`; see `Standalone/Research/WorldAssemblerM3/Linux1/REPORT.md`). `RunStandaloneWorldExport` in `VoxelForgeExploreCommandlet.cpp` serializes owner assets on the game thread to schema-v1 JSON. The adapter parses it through `ResolvedWorldSnapshot::FromJson` and keeps diff --git a/Standalone/BuildLinux.sh b/Standalone/BuildLinux.sh new file mode 100755 index 0000000..5ac912a --- /dev/null +++ b/Standalone/BuildLinux.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "${VOXELFORGE_LIMITED_BUILD:-0}" != "1" ]]; then + exec taskset -c 0-11 prlimit --as=6442450944 -- env VOXELFORGE_LIMITED_BUILD=1 "$0" "$@" +fi + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$SCRIPT_DIR" +COMPILER="clang++-19" +VARIANT="clang19" +FP_CONTRACT="off" +JOBS=12 + +while (($#)); do + case "$1" in + --compiler=*) COMPILER="${1#*=}" ;; + --variant=*) VARIANT="${1#*=}" ;; + --fp-contract=*) FP_CONTRACT="${1#*=}" ;; + --jobs=*) JOBS="${1#*=}" ;; + *) printf 'Unknown build option: %s\n' "$1" >&2; exit 2 ;; + esac + shift +done + +if [[ ! "$JOBS" =~ ^[0-9]+$ ]] || ((JOBS < 1 || JOBS > 12)); then + printf 'jobs must be from 1 to 12\n' >&2 + exit 2 +fi +if [[ "$FP_CONTRACT" != "off" && "$FP_CONTRACT" != "on" && "$FP_CONTRACT" != "fast" ]]; then + printf 'fp-contract must be off, on, or fast\n' >&2 + exit 2 +fi +command -v "$COMPILER" >/dev/null || { printf 'Compiler not found: %s\n' "$COMPILER" >&2; exit 2; } +command -v ar >/dev/null || { printf 'ar not found\n' >&2; exit 2; } + +BUILD_DIR="$ROOT/Build/linux-$VARIANT" +BIN_DIR="$ROOT/bin/linux" +mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR" + +INCLUDES=( + "-I$ROOT/Core/include" + "-I$ROOT/Core/src" + "-I$ROOT/Compat" + "-I$ROOT/Source" + "-I$ROOT/Source/VoxelForge/Public" + "-I$ROOT/Source/VoxelForge/Private" +) +FLAGS=( + -std=c++20 -O2 -mavx2 -mfma "-ffp-contract=$FP_CONTRACT" -fno-fast-math + -fexceptions -pthread "${INCLUDES[@]}" +) +if [[ "$($COMPILER --version | head -n 1)" == *clang* ]]; then + FLAGS+=(-Wno-c++11-narrowing) +else + FLAGS+=(-Wno-narrowing) +fi + +CORE_OBJECTS=() +LEGACY_OBJECTS=() +PIDS=() +wait_oldest() { + local pid="${PIDS[0]}" + wait "$pid" + PIDS=("${PIDS[@]:1}") +} +compile_background() { + local source="$1" output="$2" + "$COMPILER" "${FLAGS[@]}" -c "$source" -o "$output" & + PIDS+=("$!") + if ((${#PIDS[@]} >= JOBS)); then wait_oldest; fi +} + +while IFS= read -r source_name || [[ -n "$source_name" ]]; do + source_name="${source_name%%#*}" + source_name="${source_name//[[:space:]]/}" + [[ -z "$source_name" ]] && continue + source_path="$ROOT/$source_name" + [[ -f "$source_path" ]] || { printf 'Missing source from core_sources.txt: %s\n' "$source_name" >&2; exit 2; } + if [[ "$source_name" == "Core/src/VoxelForgeCore.cpp" ]]; then + object="$BUILD_DIR/objects/VoxelForgeCore.o" + compile_background "$source_path" "$object" + CORE_OBJECTS+=("$object") + continue + fi + wrapper_name="vfcore_${source_name//[^A-Za-z0-9]/_}" + wrapper="$BUILD_DIR/wrappers/$wrapper_name.cpp" + cat > "$wrapper" < #include #include +#include #include #include #include diff --git a/Standalone/Compat/StandaloneDataAssetGlue.cpp b/Standalone/Compat/StandaloneDataAssetGlue.cpp index 351c800..097234b 100644 --- a/Standalone/Compat/StandaloneDataAssetGlue.cpp +++ b/Standalone/Compat/StandaloneDataAssetGlue.cpp @@ -11,6 +11,11 @@ void UVoxelStrateDefinition::PostLoad() GenerationParams.bTunnelFloorEnabled = true; } +FPrimaryAssetId UVoxelSeasonAsset::GetPrimaryAssetId() const +{ + return FPrimaryAssetId(TEXT("VoxelSeason"),GetFName()); +} + bool UVoxelSeasonAsset::LoadManifest(FVoxelSeasonManifest& OutManifest, FString& OutReport) const { OutManifest = FVoxelSeasonManifest(); diff --git a/Standalone/Core/src/CoreNamespacePreamble.h b/Standalone/Core/src/CoreNamespacePreamble.h index 275655b..de39de3 100644 --- a/Standalone/Core/src/CoreNamespacePreamble.h +++ b/Standalone/Core/src/CoreNamespacePreamble.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include diff --git a/Standalone/Research/WorldAssemblerM3/Linux1/MeasureProcess.py b/Standalone/Research/WorldAssemblerM3/Linux1/MeasureProcess.py new file mode 100755 index 0000000..b6e6ea0 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux1/MeasureProcess.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Run one child and report its wait4 peak RSS without /usr/bin/time.""" +import os +import resource +import sys +import time + +if len(sys.argv) < 2: + raise SystemExit("usage: MeasureProcess.py executable [arguments ...]") + +address_limit, _ = resource.getrlimit(resource.RLIMIT_AS) +stack_limit, _ = resource.getrlimit(resource.RLIMIT_STACK) +if address_limit in (resource.RLIM_INFINITY, -1) or address_limit > 6 * 1024**3: + raise SystemExit("run under prlimit --as=6442450944") +if stack_limit in (resource.RLIM_INFINITY, -1) or stack_limit < 64 * 1024**2: + raise SystemExit("run under prlimit --stack=67108864:67108864") +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") +print(f"[ProcessLimits] address_limit={address_limit} stack_limit={stack_limit} affinity={sorted(os.sched_getaffinity(0))}", flush=True) + +started = time.monotonic() +pid = os.fork() +if pid == 0: + try: + os.execvp(sys.argv[1], sys.argv[1:]) + except OSError as error: + print(f"MeasureProcess: exec failed: {error}", file=sys.stderr, flush=True) + os._exit(127) + +_, status, usage = os.wait4(pid, 0) +wall = time.monotonic() - started +exit_code = os.waitstatus_to_exitcode(status) +print(f"[ProcessResources] exit_code={exit_code} wall_s={wall:.6f} peak_rss_kib={usage.ru_maxrss}", flush=True) +raise SystemExit(exit_code) diff --git a/Standalone/Research/WorldAssemblerM3/Linux1/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux1/REPORT.md new file mode 100644 index 0000000..2b1793e --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux1/REPORT.md @@ -0,0 +1,153 @@ +# L1 — Linux baseline (abyss) + +## What changed + +- `Standalone/Source/StreamBenchmarkPipeline.h` now holds the S13 streaming benchmark, hash helpers, and per-LOD reporting. The Windows viewer and the headless executable call the same functions. +- `Standalone/Source/WorldAssemblyPrototype.h` holds the existing Round 1 plan builder; both executables call it. +- `Standalone/Source/StreamBenchmarkHeadless.cpp` is the Linux CLI entry point. It loads the same owner JSON, resolves it through `StandaloneOwnerLoader`, builds the same Core snapshot, and runs the shared pipeline. It has no window, renderer, D3D11, or Win32 setup. +- `Standalone/BuildLinux.sh` builds the Core library and headless executable. `Linux1/MeasureProcess.py` checks affinity and memory/stack limits and records child peak RSS. +- `Standalone/Source/StandaloneViewer.cpp` now includes and calls the shared headers. `Standalone/Core/src/CoreNamespacePreamble.h` and `Standalone/Compat/CoreMinimal.h` include `` for existing `FLT_MAX` use. `Standalone/Compat/StandaloneDataAssetGlue.cpp` now supplies the standalone `UVoxelSeasonAsset::GetPrimaryAssetId` implementation matching the engine source. + +The headless entry point retains the parsed JSON root for the existing snapshot adapter. The route planner, Core shape compile, tile generator, hash code, and pipeline counters are shared with the viewer, rather than reimplemented. Viewer-only command switches for process affinity, priority, and frame limit are accepted for command-line parity; the Linux runner pins and limits itself externally. + +**Windows compile risk:** MSVC was unavailable here, so the Windows viewer was not compiled. The likely risk spots are the new includes and namespaced calls in `StandaloneViewer.cpp`, and the moved inline bodies in `StreamBenchmarkPipeline.h` and `WorldAssemblyPrototype.h`; `Build.ps1` compiles that viewer with `/std:c++20`. The Linux-only main, script, and RSS helper are not in the explicit Windows source list. The compatibility `GetPrimaryAssetId` mirrors `VoxelSeasonAsset.cpp`; keep the engine `.cpp` out of the Standalone compatibility manifest to avoid a duplicate if that manifest is expanded later. No density formula changed, so no recipe-version bump is needed. + +## Build and run commands + +Run from the repo root. `BuildLinux.sh` enforces CPUs 0–11 and a 6 GiB address-space limit itself; the explicit wrapper below makes those limits visible at the call site too. The build output is under `Standalone/Build/linux-*` and `Standalone/bin/linux/`. + +```bash +taskset -c 0-11 prlimit --as=6442450944 -- \ + bash Standalone/BuildLinux.sh --compiler=clang++-19 --variant=clang19 --jobs=12 --fp-contract=off \ + > Saved/Linux/build-clang19-final.log 2>&1 + +taskset -c 0-11 prlimit --as=6442450944 -- \ + bash Standalone/BuildLinux.sh --compiler=g++-14 --variant=gcc14 --jobs=12 --fp-contract=off \ + > Saved/Linux/build-gcc14.log 2>&1 +``` + +The measured build flags were `-std=c++20 -O2 -mavx2 -mfma -ffp-contract=off -fno-fast-math`; Clang also suppresses the legacy narrowing diagnostic that MSVC accepts. The benchmark wrapper below enforces the 6 GiB address-space cap, a 64 MiB stack, and CPU affinity, then records `wait4` peak RSS. These are the exact run calls; `yes` adds `--profile-column-overlap`. + +```bash +run_bench() { + local variant="$1" workers="$2" overlap="$3" name="$4" + local profile=() + if [[ "$overlap" == yes ]]; then profile+=(--profile-column-overlap); fi + taskset -c 0-11 prlimit --as=6442450944 --stack=67108864:67108864 -- \ + python3 Standalone/Research/WorldAssemblerM3/Linux1/MeasureProcess.py \ + "Standalone/bin/linux/VoxelForgeCoreHeadless-$variant" Standalone/data/owner_world.json \ + --workers="$workers" --world-assembler-prototype --stream-benchmark --stream-pipeline --stream-hash \ + --force-generate --clip-radius=3 --max-clip-level=8 \ + --camera=0,-35,-487.94,0,-22 --far-horizon-lod8-step=0 \ + --process-priority=Normal --process-affinity=4095 --frame-limit=60 \ + "${profile[@]}" > "Saved/Linux/$name.log" 2>&1 +} + +run_bench clang19 1 no bench-clang19-w1-r1 +run_bench clang19 8 no bench-clang19-w8-r1 +run_bench clang19 12 no bench-clang19-w12-r1 +run_bench clang19 8 no bench-clang19-w8-r2 +run_bench clang19 12 no bench-clang19-w12-r2 +run_bench clang19 1 no bench-clang19-w1-r2 +run_bench clang19 12 no bench-clang19-w12-r3 +run_bench clang19 1 no bench-clang19-w1-r3 +run_bench clang19 8 no bench-clang19-w8-r3 +run_bench clang19 8 yes bench-clang19-w8-columns-r1 +run_bench clang19 8 yes bench-clang19-w8-columns-r2 +run_bench clang19 8 yes bench-clang19-w8-columns-r3 +run_bench gcc14 8 no bench-gcc14-w8 +``` + +Every run used a fresh process and a newly constructed streamer; the pipeline reported `cache_hits=0`, so no process or thread-local tile/column cache carried over. `--force-generate` was passed for CLI parity, not relied on for isolation. The kernel file cache was not purged: doing so would require a system-wide setting outside the allowed write boundary. Owner JSON load is included in the cold-start timer and took about 5 ms. + +## Hashes and determinism + +Every Linux hash below covers the same 2,553-tile Round17 view. Clang used `-ffp-contract=off`, matching the intended MSVC `/fp:precise` behavior; GCC used the same contraction setting. Both builds enabled AVX2/FMA instructions but disabled contraction. + +| Build / workers | Runs | Density | Mesh | Payload | Topology | Tiles | +|---|---:|---|---|---|---|---:| +| Windows S13 reference, W1/W8/W12 | S13 | `c10697a8f48380c7` | `e75c9119a1498cd5` | `49a9a8d1835a6c8c` | `edcb23288b5e8724` | 2,553 | +| Linux Clang 19, W1 | 3 | `c10697a8f48380c7` | `e75c9119a1498cd5` | `49a9a8d1835a6c8c` | `edcb23288b5e8724` | 2,553 | +| Linux Clang 19, W8 | 3 | `c10697a8f48380c7` | `e75c9119a1498cd5` | `49a9a8d1835a6c8c` | `edcb23288b5e8724` | 2,553 | +| Linux Clang 19, W12 | 3 | `c10697a8f48380c7` | `e75c9119a1498cd5` | `49a9a8d1835a6c8c` | `edcb23288b5e8724` | 2,553 | +| Linux GCC 14, W8 | 1 | `c10697a8f48380c7` | `e75c9119a1498cd5` | `49a9a8d1835a6c8c` | `edcb23288b5e8724` | 2,553 | + +There was no divergence in this owner seed/view, so there is no first sample or stage to bisect and no density fix to make. The result covers the full hashed view, not every possible seed or platform math-library input. The tested flags show no observable FMA, libm, integer-width, container-order, or SIMD difference here; they do not establish what default Clang contraction would do. + +Clang W1/W8/W12 each produced identical hashes across all three runs. The GCC W8 run agreed too. The overlap-capture counts varied slightly with worker scheduling, while the generated hashes stayed fixed. + +## Linux timing and per-LOD cost + +Machine: Debian 13, Intel i9-12900H, 15 GB RAM, 24 MiB L3. Processes were pinned to logical CPUs 0–11 (the six P-cores and their SMT siblings); CPUs 12–19 remained free. CPUs 0–11 reported `powersave` governor and `balance_performance` EPP. No governor or system setting was changed; priority remained normal. `RLIMIT_AS` was 6 GiB for every build and benchmark process. The benchmark stack was limited to 64 MiB after the default-stack failures below. No run reached the 30-minute limit. + +`whole_view_ready` is the cold-start timer from process start through all 2,553 tiles, including owner load, plan, shape compile, and streamed generation. Values are median `[min–max]` of three interleaved runs. Peak RSS values are the three `wait4` readings in KiB, in run order; the maximum across all successful runs was 604,652 KiB. + +| Workers | `whole_view_ready` median [range] | Peak RSS per run (KiB) | +|---:|---:|---| +| W1 | 16.555 s [16.069–16.660] | 218,372 / 329,308 / 329,012 | +| W8 | 3.562 s [3.558–3.602] | 445,884 / 462,588 / 462,216 | +| W12 | 3.512 s [3.482–3.557] | 536,884 / 537,208 / 539,036 | +| W8 with overlap capture | 3.718 s [3.638–3.718] | 603,820 / 603,480 / 604,652 | +| GCC 14 W8 hash check | 3.789 s (one run) | 463,276 | + +The S13 W8 Windows reference was 3.518 s on the owner’s 5800X3D. The Linux W8 result is 44 ms (about 1.3%) slower for the same source view and timer, but these are different CPUs and this is a baseline comparison, not an optimization result. Round17 documents no corresponding W1/W12 timing for a direct comparison. + +The next table divides S13’s accumulated `worker_sum_s` by tile count. Each cell is median `[min–max]` milliseconds per tile across the three Clang runs. It is average worker time per tile, not a per-tile p95 or whole-pipeline elapsed time. + +| LOD | W1 ms/tile | W8 ms/tile | W12 ms/tile | +|---:|---:|---:|---:| +| 0 | 2.131 [2.066–2.144] | 4.222 [4.146–4.343] | 6.405 [6.364–6.489] | +| 1 | 4.429 [4.281–4.441] | 7.394 [7.335–7.491] | 11.084 [11.000–11.296] | +| 2 | 1.319 [1.282–1.319] | 1.871 [1.842–1.943] | 2.771 [2.740–2.813] | +| 3 | 5.520 [5.366–5.527] | 7.916 [7.860–8.179] | 11.777 [11.670–11.916] | +| 4 | 16.305 [15.787–16.344] | 23.403 [23.382–23.798] | 34.479 [34.229–34.654] | +| 5 | 7.389 [7.223–7.443] | 10.714 [10.567–10.926] | 15.520 [15.288–15.605] | +| 6 | 15.521 [15.021–15.638] | 22.183 [21.813–22.350] | 32.063 [31.641–32.163] | +| 7 | 17.203 [16.495–17.226] | 24.476 [23.925–24.560] | 33.760 [33.085–33.885] | +| 8 | 12.087 [11.673–12.228] | 16.591 [16.559–16.617] | 22.738 [22.610–23.673] | + +S13’s exclusive stage counters are also retained per LOD: column evaluation, marching cubes, ground/cap refinement, and mesh assembly. At W8, column evaluation averaged 21.124 ms/tile at LOD4 `[21.083–21.462]`, versus 6.815 ms at LOD3 `[6.761–7.014]`; LOD4 remains the standout cost. Standalone has matching aggregate stage sums, but no BuildHost-style per-tile p95 or per-LOD classify-exclusive p95. It also has no counterpart for full-scan versus candidate feature-list p95, field-point calls p95, column misses p95, or approximate per-LOD hit rate; its field/cache totals are whole-run totals, and its overlap capture measures cross-tile XY reuse instead. BuildHost `UWorld::Tick`, apply, and stationary-ring timings also have no headless equivalent. + +The W8 overlap capture completed for all tiles and all LODs. Each cell is median `[min–max]` across three captures. + +| LOD | Unique XY columns | Repeated tile evaluations | Neighbor repeats | Cross-worker neighbor repeats | +|---:|---:|---:|---:|---:| +| 0 | 128,240 [128,240–128,240] | 360,764 [359,953–364,225] | 301,491 [300,316–301,913] | 296,573 [294,996–297,862] | +| 1 | 103,957 [103,464–104,384] | 300,707 [296,768–312,183] | 243,479 [236,767–246,556] | 238,030 [228,885–241,780] | +| 2 | 32,267 [32,263–32,271] | 106,072 [104,091–106,939] | 90,338 [88,410–90,534] | 87,693 [84,976–87,921] | +| 3 | 33,286 [33,286–33,286] | 87,531 [87,310–88,408] | 81,853 [79,915–83,171] | 78,814 [76,126–78,903] | +| 4 | 91,325 [91,325–91,325] | 231,088 [230,967–231,122] | 228,336 [226,998–228,369] | 207,383 [191,310–207,789] | +| 5 | 71,960 [71,960–71,960] | 118,805 [118,676–118,806] | 118,665 [117,782–118,797] | 111,453 [108,197–112,585] | +| 6 | 280,336 [280,336–280,336] | 63,463 [63,148–63,624] | 63,463 [63,148–63,624] | 63,204 [62,944–63,579] | +| 7 | 71,530 [71,530–71,530] | 7,762 [7,752–7,807] | 7,762 [7,752–7,807] | 7,155 [7,074–7,262] | +| 8 | 106,219 [106,219–106,219] | 32,092 [32,013–32,155] | 32,092 [32,013–32,155] | 28,393 [27,959–28,598] | + +## Failed and retried attempts + +- `build-clang19.log` failed because `FLT_MAX` was undeclared; adding `` fixed it. +- `build-clang19-retry1.log` failed on existing double-to-float list initializers in `VoxelStrateManager.cpp` and an incorrect dereference in the new runner. The Linux build suppresses that legacy Clang narrowing diagnostic and passes the snapshot value correctly. +- `build-clang19-retry2.log` linked with unresolved `typeinfo for UVoxelSeasonAsset`; the compatibility glue now provides the missing `GetPrimaryAssetId`. `build-clang19-retry3.log` and `build-clang19-final.log` succeeded. +- A temporary diagnostic relink in `rebuild-headless-crash-handler.log` omitted `main` and failed; `relink-headless-crash-handler.log` corrected it. This was only for diagnosis and is not part of the final source. +- `smoke-clang19-w1.log` stopped before launching because `/usr/bin/time` was absent. No package was installed; `MeasureProcess.py` replaced it. +- Seven default-stack W1 attempts (`smoke-clang19-w1-r1.log` through `r7.log`) and three instrumented diagnostic runs (`smoke-clang19-direct.log`, `smoke-clang19-entry.log`, `smoke-clang19-stack.log`) exited with SIGSEGV while entering `FStandaloneStreamingGenerator::Generate` under the default 8 MiB stack. Their peak RSS was 211,056–329,008 KiB. A 64 MiB stack completed the diagnostic run (`smoke-clang19-stack64m.log`, 330,288 KiB peak RSS); temporary tracing pointed to worker stack exhaustion. That diagnostic included an extra direct tile and was excluded from the baseline. All clean baseline runs used the 64 MiB stack. + +## Note for the owner + +For this owner seed and view, a Linux server and Windows client generate identical hashed density, mesh, payload, and topology, and the hashes stayed stable across Linux worker counts and compilers. This i9-12900H result is a local Linux speed baseline; the W8 time is close to the S13 Windows number, but the machines differ and it should not be read as a speedup. + +## Orchestrator review (Claude) + +- **The default-stack crash is thread-local storage, not deep recursion.** `-fstack-usage` shows no frame over + ~4 KB in Core or the headless tool (`Generate` is 8 KB). But the binary's TLS segment is + `0x115068d` = **17.3 MiB per thread** (6.1 MiB of it initialised data), and glibc carves static TLS out of + each thread's stack, so an 8 MiB default stack overflows before any work runs. Largest pieces: + `world_assembly_detail::HeightfieldColumnCache` 6.8 MB, `GFloorReliefCache` 1.3 MB (x2: Core and + legacy copies), `IslandColumnCache` 1.1 MB, `GRoomGraphCache` 0.97 MB (x2), `GSurfColCache` 0.83 MB (x2). + Windows allocates TLS separately from the 1 MB thread stack, which is why it never showed there. + It matters for a Linux dedicated server: every worker thread pays ~17 MB and copies 6 MB on start. + Open for a later round (after S14, which reworks the column cache). +- **Untested:** the hash match is with `-ffp-contract=off`. Whether the hash would *detect* contraction + (`on`/`fast`) has not been shown, so it is not yet proven the flag is load-bearing. +- Viewer move checked: of 738 removed viewer lines, all but 12 signature lines (now `inline`/namespaced) + reappear verbatim in the two shared headers; every remaining viewer use is qualified or `using`-imported. + MSVC compile still unverified. diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 5d07b57..80d8e6f 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1,5 +1,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" +#include "StreamBenchmarkPipeline.h" +#include "WorldAssemblyPrototype.h" #include "StandaloneCoreScheduler.h" #include "../Core/src/StandaloneSnapshotAdapter.h" #include "../Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h" @@ -52,6 +54,9 @@ namespace { + using StandaloneStreamBenchmark::FHashedTile; + using StandaloneStreamBenchmark::HashTileRows; + struct V3 { float X=0.0f,Y=0.0f,Z=0.0f; }; V3 Add(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};} V3 Sub(V3 A,V3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};} @@ -6932,354 +6937,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; return TotalSamples>0&&LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched==0?0:10; } - void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8) - { - for(int32 I=0;I(Word>>(I*8)); - Hash*=1099511628211ull; - } - } - - struct FHashedTile - { - FVoxelTileKey Key; - uint64 Hash=14695981039346656037ull; - }; - - bool TileKeyLessForHash(const FVoxelTileKey& A,const FVoxelTileKey& B) - { - if(A.Level!=B.Level)return A.Level& Rows) - { - std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B) - {return TileKeyLessForHash(A.Key,B.Key);}); - uint64 Hash=14695981039346656037ull; - for(const FHashedTile& Row:Rows) - { - HashWord(Hash,static_cast(Row.Key.Level),4); - HashWord(Hash,static_cast(Row.Key.Coord.X),4); - HashWord(Hash,static_cast(Row.Key.Coord.Y),4); - HashWord(Hash,static_cast(Row.Key.Coord.Z),4); - HashWord(Hash,Row.Hash); - } - return Hash; - } - - int RunPipelinedStreamingBenchmark(std::shared_ptr OwnerWorld, - const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot, - const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel, - VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, - bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, - std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap, - int32 FarHorizonLOD8InteriorStepVoxels) - { - using Clock=std::chrono::steady_clock; - FStandaloneOwnerWorld& World=*OwnerWorld; - const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels; - const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; - VoxelForgeCore::World Shape(CoreSnapshot); - FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); - const auto CoreTiles=Shape.desiredTiles( - {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); - Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size()); - for(const auto& Key:CoreTiles) - Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); - if(MaxLevel>=0) - Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(), - [&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end()); - FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN); - FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN); - for(const FVoxelTileKey& Tile:Desired.Tiles) - { - const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels(); - ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z); - ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent); - if(Tile.Level Neighborhood; - Neighborhood.reserve(32); - for(const FVoxelTileKey& Tile:Desired.Tiles) - if(Tile.Level==0&&std::abs(Tile.Coord.X-CenterChunk.X)<=1 - &&std::abs(Tile.Coord.Y-CenterChunk.Y)<=1&&std::abs(Tile.Coord.Z-CenterChunk.Z)<=1) - Neighborhood.insert(Tile); - if(Neighborhood.size()!=27) - {std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;} - - // The initial desired set now owns all clipmap metadata needed by this Core-only run. - // Release the separate compatibility-world generation graph before compiling Core. - World.Generator.reset(); - World.Manager.reset(); - World.DefinitionStorage.clear(); - World.BiomeStorage.clear(); - World.OperationStorage.clear(); - World.DensitySamples.clear(); - - std::string BuildError; - const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels; - const auto ShapeBuildStart=Clock::now(); - VoxelForgeCore::ShapeBuildRequest ShapeRequest; - ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; - ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z}; - ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; - ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; - ShapeRequest.workerLimit=static_cast(std::clamp(WorkerCount,1,16)); - ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; - ShapeRequest.roundTunnels=bRoundTunnels; - ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; - if(!Shape.compile(ShapeRequest,Scheduler,BuildError)) - {std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;} - const auto ShapeReady=Clock::now(); - const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); - FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, - bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, - FarHorizonLOD8InteriorStepVoxels); - const auto StreamStart=Clock::now(); - const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); - if(Update.DesiredTiles!=static_cast(Desired.Tiles.size())) - { - Streamer.Stop(); - std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n", - Desired.Tiles.size(),Update.DesiredTiles);return 7; - } - - std::vector DensityRows,MeshRows,PayloadRows,TopologyRows; - int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0; - int32 FullDetailExpected=0; - struct FColumnXY - { - std::int64_t X=0,Y=0; - bool operator==(const FColumnXY&)const=default; - }; - struct FColumnXYHash - { - std::size_t operator()(const FColumnXY& Value)const noexcept - { - const std::size_t X=std::hash{}(Value.X); - const std::size_t Y=std::hash{}(Value.Y); - return X^(Y+static_cast(0x9E3779B9u)+(X<<6)+(X>>2)); - } - }; - struct FFirstColumnUse - { - FVoxelTileKey Tile; - std::uint64_t WorkerHash=0; - }; - std::array,FColumnXYHash>,9> ColumnUsesByLOD; - std::array ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{}; - std::array CrossWorkerNeighbourDuplicateEventsByLOD{}; - std::array ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{}; - std::array ColumnCaptureCompleteByLOD{}; - ColumnCaptureCompleteByLOD.fill(true); - std::array WorkerSecondsByLOD{}; - std::array ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{}; - std::array GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{}; - std::array MeshAssemblyMsByLOD{}; - std::array TileCountByLOD{}; - for(const FVoxelTileKey& Tile:Desired.Tiles) - if(Tile.Level>=0&&Tile.Level(ExpectedTilesByLOD.size())) - ++ExpectedTilesByLOD[static_cast(Tile.Level)]; - const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B) - { - if(A.Level!=B.Level)return false; - const std::int64_t DX=std::abs(static_cast(A.Coord.X)-B.Coord.X); - const std::int64_t DY=std::abs(static_cast(A.Coord.Y)-B.Coord.Y); - const std::int64_t DZ=std::abs(static_cast(A.Coord.Z)-B.Coord.Z); - return std::max({DX,DY,DZ})<=1&&(DX+DY+DZ)>0; - }; - uint64 NativeSolidBoxes=0,NativeAirBoxes=0,NativeMixedBoxes=0,NativeUnknownBoxes=0; - uint64 NativeFieldCalls=0,NativeColumnHits=0,NativeColumnMisses=0,NativeMeshingSkips=0; - double NativeClassificationMilliseconds=0.0,NativeColumnComputeMilliseconds=0.0; - double NativeMeshingMilliseconds=0.0; - for(const FVoxelTileKey& Tile:Desired.Tiles) - if(Tile.Level Ready=Streamer.TakeReady(); - if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;} - for(const FStandaloneCpuTile& Tile:Ready) - { - ++Finished; - if(Tile.Key.Level>=0&&Tile.Key.Level(TileCountByLOD.size())) - { - const std::size_t LOD=static_cast(Tile.Key.Level); - ++TileCountByLOD[LOD]; - WorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds; - ColumnEvalMsByLOD[LOD]+=Tile.CoreDiagnostics.columnEvaluationExclusiveMilliseconds; - MarchingExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesExclusiveMilliseconds; - GroundRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.groundRefinementExclusiveMilliseconds; - CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds; - MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds; - } - if(bProfileColumnOverlap&&Tile.Key.Level>=0 - &&Tile.Key.Level(ColumnCaptureTilesByLOD.size())) - { - const std::size_t LOD=static_cast(Tile.Key.Level); - ++ColumnCaptureTilesByLOD[LOD]; - ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD] - &&Tile.CoreDiagnostics.columnCoordinateCaptureComplete; - std::vector> Coordinates= - Tile.CoreDiagnostics.evaluatedColumnCoordinates; - std::sort(Coordinates.begin(),Coordinates.end()); - Coordinates.erase(std::unique(Coordinates.begin(),Coordinates.end()),Coordinates.end()); - for(const auto& Coordinate:Coordinates) - { - const FColumnXY XY{Coordinate[0],Coordinate[1]}; - auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY); - std::vector& PriorUses=Uses->second; - if(Inserted) - { - PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); - continue; - } - ++ColumnDuplicateEventsByLOD[LOD]; - bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false; - for(const FFirstColumnUse& Prior:PriorUses) - { - if(!AreSameLODNeighbours(Prior.Tile,Tile.Key))continue; - bHasNeighbourDuplicate=true; - bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate - ||Prior.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash; - } - if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD]; - if(bHasCrossWorkerNeighbourDuplicate) - ++CrossWorkerNeighbourDuplicateEventsByLOD[LOD]; - PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); - } - } - WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt); - if(Neighborhood.contains(Tile.Key)) - { - ++NeighborhoodFinished; - NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt); - } - if(Tile.Key.Level(Neighborhood.size())) - { - std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n", - std::chrono::duration(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished); - bNeighborhoodReported=true; - } - if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected) - { - std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n", - std::chrono::duration(FullDetailReady-ColdStart).count(), - FullDetailFinished,FullDetailExpected); - bFullDetailReported=true; - } - } - Streamer.Stop(); - bool bColumnOverlapCapturePassing=true; - if(bProfileColumnOverlap) - { - for(std::size_t LOD=0;LOD(ColumnCaptureTilesByLOD[LOD]), - static_cast(ExpectedTilesByLOD[LOD]), - ColumnUsesByLOD[LOD].size(), - static_cast(ColumnDuplicateEventsByLOD[LOD]), - static_cast(NeighbourDuplicateEventsByLOD[LOD]), - static_cast(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]), - bLODComplete?1:0); - } - } - for(std::size_t LOD=0;LOD(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD], - ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD], - GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],MeshAssemblyMsByLOD[LOD]); - } - const double NeighborhoodSeconds=std::chrono::duration(NeighborhoodReady-ColdStart).count(); - const double NeighborhoodMeshSeconds=std::chrono::duration(NeighborhoodReady-ShapeReady).count(); - const double FullDetailSeconds=std::chrono::duration(FullDetailReady-ColdStart).count(); - const double WholeViewSeconds=std::chrono::duration(WholeViewReady-ColdStart).count(); - const double StreamSeconds=std::chrono::duration(WholeViewReady-StreamStart).count(); - std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n", - WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds, - FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished, - FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds); - std::printf("[WorldAssemblerTilePipeline] full_detail_tiles=%d solid_boxes=%llu air_boxes=%llu mixed_boxes=%llu unknown_boxes=%llu whole_tile_skips=%llu field_calls=%llu column_cache_hits=%llu column_cache_misses=%llu column_compute_worker_ms=%.3f classification_worker_ms=%.3f meshing_worker_ms=%.3f\n", - FullDetailExpected,static_cast(NativeSolidBoxes), - static_cast(NativeAirBoxes), - static_cast(NativeMixedBoxes), - static_cast(NativeUnknownBoxes), - static_cast(NativeMeshingSkips), - static_cast(NativeFieldCalls), - static_cast(NativeColumnHits), - static_cast(NativeColumnMisses), - NativeColumnComputeMilliseconds,NativeClassificationMilliseconds, - NativeMeshingMilliseconds); - - bool bHashesPassing=true; - if(bHashDensity) - { - bHashesPassing=DensityRows.size()==Desired.Tiles.size() - &&MeshRows.size()==Desired.Tiles.size() - &&PayloadRows.size()==Desired.Tiles.size() - &&TopologyRows.size()==Desired.Tiles.size(); - std::vector FullDensity,FullMesh,FullPayload,FullTopology; - for(const FHashedTile& Row:DensityRows)if(Row.Key.Level(HashTileRows(FullDensity)), - static_cast(HashTileRows(FullMesh)), - static_cast(HashTileRows(FullPayload)), - static_cast(HashTileRows(FullTopology))); - std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n", - Desired.Tiles.size(),static_cast(HashTileRows(DensityRows)), - static_cast(HashTileRows(MeshRows)), - static_cast(HashTileRows(PayloadRows)), - static_cast(HashTileRows(TopologyRows))); - } - return NeighborhoodFinished==static_cast(Neighborhood.size()) - &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing - &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6; - } int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld, const FVector& CameraPositionVoxels, int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels, @@ -8132,414 +7789,6 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; &&analyticReverseCovered==1&&packedAnalyticRenderSame?0:12; } - std::int64_t WorldAssemblyFixed(double voxels) - {return static_cast(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));} - - std::int64_t WorldAssemblyQ16(double value) - {return static_cast(std::llround(value*65536.0));} - - std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels) - { - const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels; - return std::max(1,WorldAssemblyFixed(wavelength)); - } - - uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind) - { - uint64 hash=14695981039346656037ull; - const auto add=[&](uint64 value,int bytes) - { - for(int i=0;i(value>>(8*i));hash*=1099511628211ull;} - }; - add(seed,4);add(static_cast(x),8);add(static_cast(y),8);add(kind,4); - return hash?hash:1; - } - - bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe, - VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested, - std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error) - { - using namespace VoxelForgeCore; - constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale. - constexpr std::int64_t PlayerHeightQ=1802; // 1.76 m in Core's 0.25 m voxel scale. - constexpr std::int64_t HalfHeightQ=PlayerHeightQ/2; - constexpr std::int64_t StepQ=128; // 0.5 Core voxel, finer than the density lattice. - constexpr std::int64_t ProbeRadiusQ=174; - constexpr std::int64_t SupportDepthQ=256; - constexpr std::int64_t GradeQ16=17560; // tan(15 degrees), rounded upward. - constexpr std::int64_t RouteRadiusQ=4*WorldAssemblySubvoxelsPerVoxel; - struct Direction {std::int32_t x,y;}; - constexpr std::array directions{{ - {65536,0},{46341,46341},{0,65536},{-46341,46341}, - {-65536,0},{-46341,-46341},{0,-65536},{46341,-46341}}}; - struct Candidate - { - std::int64_t score=0; - std::int32_t centerX=0,centerY=0,direction=0; - std::vector points; - }; - - auto& constraints=stratum.routeConstraints; - constraints.version=1; - constraints.capsuleRadiusSubvoxels=PlayerRadiusQ; - constraints.capsuleHeightSubvoxels=PlayerHeightQ; - constraints.maximumGradeQ16=GradeQ16; - constraints.sweepSpacingSubvoxels=StepQ; - constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ; - constraints.supportSearchDepthSubvoxels=SupportDepthQ; - constraints.minimumClearanceSubvoxels=8; - constraints.minimumSupportProbes=3; - - const Heightfield2D finalField(recipe,stratum); - if(!finalField.isValid()) - {error="route planner received an invalid final composed field";return false;} - std::vector candidates; - constexpr std::int32_t halfLength=128; - constexpr std::int32_t waypointSpacing=8; - for(std::int32_t centerY=-256;centerY<=256;centerY+=64) - for(std::int32_t centerX=-256;centerX<=256;centerX+=64) - for(std::int32_t directionIndex=0;directionIndex(directions.size());++directionIndex) - { - Candidate candidate; - candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex; - candidate.points.reserve(33); - std::int64_t previousGround=0,previousX=0,previousY=0; - bool gradePass=true; - for(std::int32_t index=0;index<=32;++index) - { - const std::int64_t along=(index-16)*waypointSpacing; - const std::int64_t x=centerX+std::llround( - static_cast(along)*directions[directionIndex].x/65536.0); - const std::int64_t y=centerY+std::llround( - static_cast(along)*directions[directionIndex].y/65536.0); - const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), - WorldAssemblyFixed(static_cast(y)),0); - const std::int64_t horizontalQ=static_cast(std::llround( - std::hypot(static_cast(x-previousX),static_cast(y-previousY)) - *WorldAssemblySubvoxelsPerVoxel)); - if(index>0) - { - const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround); - if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16) - {gradePass=false;break;} - candidate.score+=riseQ*65536/std::max(1,horizontalQ); - } - WorldAssemblyRoutePointV1 point; - point.positionSubvoxels={WorldAssemblyFixed(static_cast(x)), - WorldAssemblyFixed(static_cast(y)),column.groundHeightSubvoxels+HalfHeightQ}; - point.radiusSubvoxels=RouteRadiusQ; - point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed, - static_cast(x),static_cast(y), - 0x52540000u+static_cast(index)); - candidate.points.push_back(point); - previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; - } - ++tested; - if(gradePass)candidates.push_back(std::move(candidate)); - else ++gradeRejected; - } - std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b) - { - if(a.score!=b.score)return a.score0) - for(std::size_t pointIndex=0;pointIndex0) - std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n", - static_cast(liftQ),static_cast(SupportDepthQ)); - return true; - } - if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break; - const std::int64_t needed=std::max(1, - constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels); - const std::int64_t steps=(needed+RepairStepQ-1)/RepairStepQ; - const std::int64_t nextLift=std::max(liftQ+RepairStepQ,liftQ+steps*RepairStepQ); - if(nextLift>MaximumRepairLiftQ)break; - liftQ=nextLift; - ++repairAttempt; - } - if(sweepAttempt<3) - std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n", - static_cast(sweepAttempt), - static_cast(lastAudit.blockedCapsuleSamples), - static_cast(lastAudit.unsupportedCenterSamples), - static_cast(lastAudit.gradeFailures), - static_cast(lastAudit.minimumClearanceSubvoxels)); - ++sweepAttempt; - ++sweepRejected; - } - std::printf("[WorldAssemblerRouteSearchFailed] tested=%llu grade_rejected=%llu sweepable_candidates=%llu sweep_rejected=%llu last_blocked=%llu last_unsupported=%llu last_grade_failures=%llu\n", - static_cast(tested),static_cast(gradeRejected), - static_cast(candidates.size()),static_cast(sweepRejected), - static_cast(lastAudit.blockedCapsuleSamples), - static_cast(lastAudit.unsupportedCenterSamples), - static_cast(lastAudit.gradeFailures)); - std::printf("[WorldAssemblerRouteRepairSummary] repair_attempts=%llu\n", - static_cast(repairAttempt)); - stratum.route.clear(); - error="finite-strate route candidates failed the composed-field capsule and support sweep"; - return false; - } - - bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root, - std::string& error,const std::string& manifestOutputPath) - { - if(!owner.Manager||!owner.Generator) - {error="world assembler prototype needs the resolved standalone owner world";return false;} - int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0; - bool foundSurface=false; - for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ) - { - if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ)) - !=ECaveGeneratorType::SurfaceWorld)continue; - surfaceChunkZ=chunkZ; - foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ); - break; - } - if(!foundSurface) - {error="world assembler prototype could not find a finite SurfaceWorld strate";return false;} - const FIntVector chunk(0,0,surfaceChunkZ); - const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk); - const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk); - const std::int64_t bottomVoxel=static_cast(bottomChunkZ)*CHUNK_SIZE; - const std::int64_t heightVoxels=static_cast(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE; - const FBiomeMapParams& map=biomeContext.Map; - const UVoxelStrateDefinition* definition=owner.Manager->GetStrateForChunk(chunk); - - VoxelForgeCore::WorldAssemblyManifest manifest; - auto& recipe=manifest.recipe; - recipe.seed=static_cast(owner.Seed); - recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize); - recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength); - recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0); - recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend); - recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0); - recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0); - recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast); - const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params, - const FBiomeResolved* climate) - { - VoxelForgeCore::WorldAssemblyBiomeProfileV1 profile; - profile.stableId=stableId; - profile.baseHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) - +static_cast(heightVoxels)*params.BaseGroundRelative); - profile.continentAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange); - profile.continentWavelengthSubvoxels=WorldAssemblyWavelength(params.ContinentFrequency,1000.0); - profile.mountainAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange*params.MountainStrength); - profile.mountainWavelengthSubvoxels=WorldAssemblyWavelength(params.MountainFrequency,500.0); - profile.detailAmplitudeSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness); - profile.detailWavelengthSubvoxels=WorldAssemblyWavelength(params.DetailFrequency,25.0); - profile.reliefMinimumQ16=climate?WorldAssemblyQ16(climate->ReliefMin):0; - profile.reliefMaximumQ16=climate?WorldAssemblyQ16(climate->ReliefMax):65536; - profile.moistureMinimumQ16=climate?WorldAssemblyQ16(climate->MoistureMin):0; - profile.moistureMaximumQ16=climate?WorldAssemblyQ16(climate->MoistureMax):65536; - profile.reliefStrengthQ16=WorldAssemblyQ16(params.ReliefStrength); - profile.reliefContrastQ16=WorldAssemblyQ16(params.ReliefContrast); - profile.reliefWavelengthSubvoxels=WorldAssemblyWavelength(params.ReliefFrequency,667.0); - profile.heightWarpStrengthSubvoxels=WorldAssemblyFixed(params.HeightWarpStrength); - profile.heightWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.HeightWarpFrequency,125.0); - profile.beachWidthSubvoxels=WorldAssemblyFixed(params.BeachWidth); - profile.cliffStrengthQ16=WorldAssemblyQ16(params.CliffStrength); - profile.cliffSlopeThresholdQ16=std::max(1,WorldAssemblyQ16(params.CliffSlopeThreshold)); - profile.cliffSharpnessQ16=WorldAssemblyQ16(params.CliffSharpness); - profile.cliffSampleDistanceSubvoxels=WorldAssemblyFixed(std::max(0.5f,params.CliffSampleDist)); - const double structuralAbsBound=static_cast(params.ElevationRange) - +static_cast(params.ElevationRange)*params.MountainStrength - +2.0*params.SurfaceRoughness; - const double cliffAbsBound=2.0*structuralAbsBound*params.CliffStrength*params.CliffSharpness; - profile.cliffMaximumDisplacementSubvoxels=WorldAssemblyFixed(params.CliffStrength>0.0f - ?std::max(1.0,cliffAbsBound):0.0); - profile.hasCapProfile=true; - profile.capHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) - +static_cast(heightVoxels)*params.CeilingRelative); - profile.capWarpStrengthSubvoxels=WorldAssemblyFixed(params.CeilingWarpStrength); - profile.capWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingWarpFrequency,100.0); - profile.capUndulationSubvoxels=WorldAssemblyFixed(params.CeilingUndulation); - profile.capWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingUndulationFrequency,500.0); - profile.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRoughness); - profile.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRoughnessFrequency,25.0); - profile.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRidgeStrength); - profile.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRidgeFrequency,50.0); - return profile; - }; - if(biomeContext.IsValid()&&definition) - { - for(const FBiomeResolved& resolved:biomeContext.Biomes) - { - const UVoxelBiomeDefinition* biome=definition->Biomes.IsValidIndex(resolved.Index) - ?definition->Biomes[resolved.Index]:nullptr; - FSurfaceGenerationParams params=surface; - if(biome&&biome->bOverrideTerrain&&biome->GeneratorType==ECaveGeneratorType::SurfaceWorld) - { - params=biome->SurfaceParams; - params.StrateTopWorldZ=surface.StrateTopWorldZ; - params.StrateBottomWorldZ=surface.StrateBottomWorldZ; - params.BoundarySealThickness=surface.BoundarySealThickness; - params.BaseDensity=surface.BaseDensity; - params.WaterLevelRelative=surface.WaterLevelRelative; - } - recipe.biomes.push_back(makeProfile(0x1001u+static_cast(resolved.Index),params,&resolved)); - } - } - if(recipe.biomes.empty())recipe.biomes.push_back(makeProfile(0x1001u,surface,nullptr)); - - VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum; - stratum.strateIndex=owner.Manager->GetStrateIndex( - static_cast((static_cast(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); - stratum.topChunkZ=topChunkZ; - stratum.bottomChunkZ=bottomChunkZ; - const double strateBottom=static_cast(bottomVoxel); - const double strateHeight=static_cast(heightVoxels); - stratum.capHeightSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.CeilingRelative); - stratum.capUndulationSubvoxels=WorldAssemblyFixed(surface.CeilingUndulation); - stratum.capWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingUndulationFrequency,250.0); - stratum.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRoughness); - stratum.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRoughnessFrequency,25.0); - stratum.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRidgeStrength); - stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0); - stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative); - stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f; - - // Select a deterministic steep edge in the resolved owner field for a bounded shelf. - // Selection d'une pente deterministe dans le champ resolu pour une saillie bornee. - constexpr int probeSide=49,probeStep=32; - std::array heights{}; - std::array valid{}; - for(int y=0;y((x-probeSide/2)*probeStep); - const float py=static_cast((y-probeSide/2)*probeStep); - float ceiling=0.0f; - const std::size_t index=static_cast(y)*probeSide+x; - valid[index]=owner.Generator->GetSurfaceHeightAt(px,py,topChunkZ,heights[index],ceiling)?1u:0u; - } - double steepest=0.0;int cliffX=0,cliffY=0;bool xAxis=true;int downhill=1; - for(int y=1;y(y)*probeSide+x; - if(!valid[center]||!valid[center-1]||!valid[center+1]||!valid[center-probeSide]||!valid[center+probeSide])continue; - const double dx=static_cast(heights[center+1]-heights[center-1]); - const double dy=static_cast(heights[center+probeSide]-heights[center-probeSide]); - const double slope=(std::abs(dx)+std::abs(dy))/(4.0*probeStep); - if(slope>steepest){steepest=slope;cliffX=(x-probeSide/2)*probeStep;cliffY=(y-probeSide/2)*probeStep; - xAxis=std::abs(dx)>=std::abs(dy);downhill=(xAxis?dx:dy)>=0.0?-1:1;} - } - if(steepest>0.05) - { - float ground=0.0f,ceiling=0.0f; - if(owner.Generator->GetSurfaceHeightAt(static_cast(cliffX),static_cast(cliffY), - topChunkZ,ground,ceiling)) - { - VoxelForgeCore::WorldAssemblyFeatureV1 shelf; - shelf.stableId=WorldAssemblyFeatureId(recipe.seed,cliffX,cliffY,0x4f564552u); - shelf.type=4; - constexpr int reach=72,width=36,height=64,feather=8; - const int dirX=xAxis?downhill:0,dirY=xAxis?0:downhill; - const int minX=xAxis?(dirX>0?cliffX-feather:cliffX-reach-feather) - :cliffX-width-feather; - const int maxX=xAxis?(dirX>0?cliffX+reach+feather:cliffX+feather) - :cliffX+width+feather; - const int minY=xAxis?cliffY-width-feather - :(dirY>0?cliffY-feather:cliffY-reach-feather); - const int maxY=xAxis?cliffY+width+feather - :(dirY>0?cliffY+reach+feather:cliffY+feather); - VoxelForgeCore::Heightfield2D baseField(recipe,stratum); - double lowestGround=std::numeric_limits::infinity(); - double highestGround=-std::numeric_limits::infinity(); - for(int sampleY=minY;sampleY<=maxY;sampleY+=4) - for(int sampleX=minX;sampleX<=maxX;sampleX+=4) - { - const auto sample=baseField.point(WorldAssemblyFixed(sampleX), - WorldAssemblyFixed(sampleY),0); - const double heightQ=static_cast(sample.groundHeightSubvoxels) - /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; - lowestGround=std::min(lowestGround,heightQ); - highestGround=std::max(highestGround,heightQ); - } - if(!std::isfinite(lowestGround)||!std::isfinite(highestGround)) - {error="world assembler cliff shelf could not bound its terrain columns";return false;} - shelf.minimumSubvoxels={WorldAssemblyFixed(minX),WorldAssemblyFixed(minY), - WorldAssemblyFixed(std::max(bottomVoxel,lowestGround-4.0))}; - shelf.maximumSubvoxels={WorldAssemblyFixed(maxX),WorldAssemblyFixed(maxY), - WorldAssemblyFixed(std::min((topChunkZ+1)*CHUNK_SIZE, - highestGround+height+4.0))}; - shelf.parametersSubvoxels={WorldAssemblyFixed(dirX),WorldAssemblyFixed(dirY), - WorldAssemblyFixed(height),WorldAssemblyFixed(reach)}; - stratum.features.push_back(shelf); - } - } - std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0; - if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected, - routeSweepRejected,error))return false; - std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n", - static_cast(routeCandidates), - static_cast(routeGradeRejected), - static_cast(routeSweepRejected),stratum.route.size(), - stratum.routeConstraints.minimumSupportProbes); - manifest.plan.strates.push_back(std::move(stratum)); - if(!manifest.finalize(error))return false; - const std::string manifestText=manifest.ToJson(); - const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error); - if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText - ||roundTrip->contentHash!=manifest.contentHash) - {if(error.empty())error="world assembly canonical plan serialization did not round-trip";return false;} - std::printf("[WorldAssemblerPlanSerialization] bytes=%zu canonical_round_trip=true route_points=%zu plan_hash=%016llx manifest_hash=%016llx status=PASS\n", - manifestText.size(),manifest.plan.strates.front().route.size(), - static_cast(manifest.plan.contentHash), - static_cast(manifest.contentHash)); - if(!manifestOutputPath.empty()) - { - const std::filesystem::path outputPath(manifestOutputPath); - std::error_code directoryError; - if(!outputPath.parent_path().empty()) - std::filesystem::create_directories(outputPath.parent_path(),directoryError); - if(directoryError) - {error="could not create the world assembly manifest output directory";return false;} - std::ofstream output(outputPath,std::ios::binary); - if(!output) - {error="could not open the world assembly manifest output path";return false;} - output.write(manifestText.data(),static_cast(manifestText.size())); - if(!output) - {error="could not write the world assembly manifest payload";return false;} - } - VFJson::Value manifestJson; - if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false; - auto* rootObject=std::get_if(&root.Data); - if(!rootObject) - {error="owner snapshot root is not a JSON object";return false;} - (*rootObject)["world_assembly"]=std::move(manifestJson); - std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=%zu features=%zu\n", - manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(), - manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ, - manifestText.size(),static_cast(manifest.recipe.contentHash), - static_cast(manifest.plan.contentHash), - static_cast(manifest.contentHash),manifest.plan.strates.front().route.size(), - manifest.plan.strates.front().features.size()); - return true; - } - bool RunWorldAssemblerCapAudit(FStandaloneOwnerWorld& owner, const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,VoxelForgeCore::World& world, VoxelForgeCore::Vec3d walkCameraVoxels) @@ -9916,7 +9165,7 @@ int main(int Argc,char** Argv) if(App.bWorldAssemblerPrototype) { const auto PlanStart=std::chrono::steady_clock::now(); - const bool bPlanReady=AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot, + const bool bPlanReady=StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot, Error,WorldAssemblyManifestOutputPath); const double PlanMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-PlanStart).count(); @@ -9978,7 +9227,7 @@ int main(int Argc,char** Argv) if(bStreamBenchmark) { if(bStreamPipeline) - return RunPipelinedStreamingBenchmark(App.World,App.Snapshot, + return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(App.World,App.Snapshot, FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler, bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups, diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp new file mode 100644 index 0000000..ba52612 --- /dev/null +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -0,0 +1,186 @@ +#include "OwnerWorldLoader.h" +#include "StandaloneCoreScheduler.h" +#include "StreamBenchmarkPipeline.h" +#include "WorldAssemblyPrototype.h" +#include "../Core/src/StandaloneSnapshotAdapter.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct FOptions + { + std::filesystem::path DataPath; + int WorkerCount=8; + int ClipRadius=3; + int MaxClipLevel=8; + int MaxLevel=-1; + int FarHorizonLOD8InteriorStepVoxels=0; + bool WorldAssemblerPrototype=false; + bool StreamBenchmark=false; + bool StreamPipeline=false; + bool StreamHash=false; + bool ProfileColumnOverlap=false; + bool ForceGenerate=false; + bool HasCamera=false; + FVector CameraMeters=FVector::ZeroVector; + }; + + bool ParseInteger(std::string_view text,int& output) + { + if(text.empty())return false; + char* end=nullptr; + const std::string value(text); + const long parsed=std::strtol(value.c_str(),&end,10); + if(end==value.c_str()||*end!='\0'||parsedINT32_MAX)return false; + output=static_cast(parsed); + return true; + } + + bool ParseCamera(std::string_view text,FVector& meters) + { + const std::string values(text); + float x=0.0f,y=0.0f,z=0.0f,yaw=0.0f,pitch=0.0f; + if(std::sscanf(values.c_str(),"%f,%f,%f,%f,%f",&x,&y,&z,&yaw,&pitch)!=5) + return false; + meters={x,y,z}; + return true; + } + + bool ParseOptions(int argc,char** argv,FOptions& options) + { + const std::filesystem::path executable=std::filesystem::absolute(argv[0]); + options.DataPath=executable.parent_path().parent_path()/"data/owner_world.json"; + for(int index=1;index(arg.size()),arg.data());return false;} + options.HasCamera=true; + } + else if(arg.starts_with("--workers=")) + {if(!readInt("--workers=",options.WorkerCount))return false;} + else if(arg.starts_with("--clip-radius=")) + {if(!readInt("--clip-radius=",options.ClipRadius))return false;} + else if(arg.starts_with("--max-clip-level=")) + {if(!readInt("--max-clip-level=",options.MaxClipLevel))return false;} + else if(arg.starts_with("--max-level=")) + {if(!readInt("--max-level=",options.MaxLevel))return false;} + else if(arg.starts_with("--far-horizon-lod8-step=")) + {if(!readInt("--far-horizon-lod8-step=",options.FarHorizonLOD8InteriorStepVoxels))return false;} + else if(arg.starts_with("--process-affinity=")||arg.starts_with("--process-priority=") + ||arg.starts_with("--frame-limit=")) + { + // These viewer switches are accepted for command-line parity. The caller pins + // this headless process with taskset, and a headless run has no frame limiter. + } + else if(!arg.empty()&&arg.front()!='-')options.DataPath=std::filesystem::path(arg); + else + {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast(arg.size()),arg.data());return false;} + } + options.WorkerCount=std::clamp(options.WorkerCount,1,16); + options.ClipRadius=std::clamp(options.ClipRadius,1,64); + options.MaxClipLevel=std::clamp(options.MaxClipLevel,0,8); + options.FarHorizonLOD8InteriorStepVoxels=std::max(0,options.FarHorizonLOD8InteriorStepVoxels); + if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline) + { + std::fprintf(stderr,"This headless target runs the world-assembler streaming pipeline; pass --world-assembler-prototype --stream-benchmark --stream-pipeline.\n"); + return false; + } + return true; + } +} + +int main(int argc,char** argv) +{ + using Clock=std::chrono::steady_clock; + const auto coldStart=Clock::now(); + FOptions options; + if(!ParseOptions(argc,argv,options))return 2; + std::printf("[HeadlessConfig] data=%s workers=%d clip_radius=%d max_clip_level=%d max_level=%d hash=%s column_overlap=%s force_generate=%s\n", + options.DataPath.string().c_str(),options.WorkerCount,options.ClipRadius,options.MaxClipLevel, + options.MaxLevel,options.StreamHash?"on":"off",options.ProfileColumnOverlap?"on":"off", + options.ForceGenerate?"on":"off"); + std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n", + std::chrono::duration(Clock::now()-coldStart).count()); + + std::string error; + auto ownerWorld=std::make_shared(); + VFJson::Value ownerJsonRoot; + { + std::ifstream jsonFile(options.DataPath,std::ios::binary); + if(!jsonFile) + {std::fprintf(stderr,"Owner data load failed: cannot open %s\n",options.DataPath.string().c_str());return 2;} + std::string ownerJsonText{std::istreambuf_iterator(jsonFile),std::istreambuf_iterator{}}; + if(!jsonFile&&!jsonFile.eof()) + {std::fprintf(stderr,"Owner data load failed: read error for %s\n",options.DataPath.string().c_str());return 2;} + if(!VFJson::Parser(std::move(ownerJsonText)).Parse(ownerJsonRoot,error)) + {std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;} + } + if(!StandaloneOwnerLoader::Resolve(ownerJsonRoot,*ownerWorld,error)) + {std::fprintf(stderr,"Owner data load failed: %s\n",error.c_str());return 2;} + std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n", + std::chrono::duration(Clock::now()-coldStart).count()); + + const int seed=ownerWorld->Seed; + const int exportedMaxClipLevel=ownerWorld->MaxClipLevel; + ownerWorld->ClipRadius=options.ClipRadius; + ownerWorld->Settings->ClipRadius=options.ClipRadius; + ownerWorld->MaxClipLevel=options.MaxClipLevel; + ownerWorld->Settings->MaxClipLevel=options.MaxClipLevel; + if(exportedMaxClipLevel!=options.MaxClipLevel) + std::printf("Standalone max clip level override: exported=%d -> max_level=%d.\n", + exportedMaxClipLevel,options.MaxClipLevel); + + FVector cameraVoxels=ownerWorld->ViewerStartVoxels; + if(options.HasCamera) + cameraVoxels=FVector(options.CameraMeters.X*4.0f,options.CameraMeters.Y*4.0f,options.CameraMeters.Z*4.0f); + + const auto planStart=Clock::now(); + if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( + *ownerWorld,ownerJsonRoot,error,{})) + {std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;} + std::printf("[WorldAssemblerPlanTiming] total_ms=%.3f status=PASS\n", + std::chrono::duration(Clock::now()-planStart).count()); + + auto snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(ownerJsonRoot,seed, + options.ClipRadius,options.MaxClipLevel,false,false,error); + if(!snapshot) + {std::fprintf(stderr,"Shape interval build failed: %s\n",error.c_str());return 2;} + + FStandaloneCoreScheduler scheduler; + return StandaloneStreamBenchmark::RunPipelinedStreamingBenchmark(ownerWorld,snapshot, + cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent, + scheduler,options.StreamHash,false,false,false,false,coldStart, + options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels); +} diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h new file mode 100644 index 0000000..76930c6 --- /dev/null +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -0,0 +1,368 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "StreamingWorld.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace StandaloneStreamBenchmark +{ + inline void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8) + { + for(int32 I=0;I(Word>>(I*8)); + Hash*=1099511628211ull; + } + } + + struct FHashedTile + { + FVoxelTileKey Key; + uint64 Hash=14695981039346656037ull; + }; + + inline bool TileKeyLessForHash(const FVoxelTileKey& A,const FVoxelTileKey& B) + { + if(A.Level!=B.Level)return A.Level& Rows) + { + std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B) + {return TileKeyLessForHash(A.Key,B.Key);}); + uint64 Hash=14695981039346656037ull; + for(const FHashedTile& Row:Rows) + { + HashWord(Hash,static_cast(Row.Key.Level),4); + HashWord(Hash,static_cast(Row.Key.Coord.X),4); + HashWord(Hash,static_cast(Row.Key.Coord.Y),4); + HashWord(Hash,static_cast(Row.Key.Coord.Z),4); + HashWord(Hash,Row.Hash); + } + return Hash; + } + + inline int RunPipelinedStreamingBenchmark(std::shared_ptr OwnerWorld, + const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot, + const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel, + VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler, + bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors, + std::chrono::steady_clock::time_point ColdStart,bool bProfileColumnOverlap, + int32 FarHorizonLOD8InteriorStepVoxels) + { + using Clock=std::chrono::steady_clock; + FStandaloneOwnerWorld& World=*OwnerWorld; + const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels; + const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; + VoxelForgeCore::World Shape(CoreSnapshot); + FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels); + const auto CoreTiles=Shape.desiredTiles( + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}); + Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + if(MaxLevel>=0) + Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(), + [&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end()); + FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN); + FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN); + for(const FVoxelTileKey& Tile:Desired.Tiles) + { + const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels(); + ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z); + ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent); + if(Tile.Level Neighborhood; + Neighborhood.reserve(32); + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(Tile.Level==0&&std::abs(Tile.Coord.X-CenterChunk.X)<=1 + &&std::abs(Tile.Coord.Y-CenterChunk.Y)<=1&&std::abs(Tile.Coord.Z-CenterChunk.Z)<=1) + Neighborhood.insert(Tile); + if(Neighborhood.size()!=27) + {std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;} + + // The initial desired set now owns all clipmap metadata needed by this Core-only run. + // Release the separate compatibility-world generation graph before compiling Core. + World.Generator.reset(); + World.Manager.reset(); + World.DefinitionStorage.clear(); + World.BiomeStorage.clear(); + World.OperationStorage.clear(); + World.DensitySamples.clear(); + + std::string BuildError; + const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels; + const auto ShapeBuildStart=Clock::now(); + VoxelForgeCore::ShapeBuildRequest ShapeRequest; + ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; + ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z}; + ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z}; + ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z}; + ShapeRequest.workerLimit=static_cast(std::clamp(WorkerCount,1,16)); + ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; + ShapeRequest.roundTunnels=bRoundTunnels; + ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; + if(!Shape.compile(ShapeRequest,Scheduler,BuildError)) + {std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;} + const auto ShapeReady=Clock::now(); + const double ShapeSeconds=std::chrono::duration(ShapeReady-ShapeBuildStart).count(); + FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, + bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, + FarHorizonLOD8InteriorStepVoxels); + const auto StreamStart=Clock::now(); + const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); + if(Update.DesiredTiles!=static_cast(Desired.Tiles.size())) + { + Streamer.Stop(); + std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n", + Desired.Tiles.size(),Update.DesiredTiles);return 7; + } + + std::vector DensityRows,MeshRows,PayloadRows,TopologyRows; + int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0; + int32 FullDetailExpected=0; + struct FColumnXY + { + std::int64_t X=0,Y=0; + bool operator==(const FColumnXY&)const=default; + }; + struct FColumnXYHash + { + std::size_t operator()(const FColumnXY& Value)const noexcept + { + const std::size_t X=std::hash{}(Value.X); + const std::size_t Y=std::hash{}(Value.Y); + return X^(Y+static_cast(0x9E3779B9u)+(X<<6)+(X>>2)); + } + }; + struct FFirstColumnUse + { + FVoxelTileKey Tile; + std::uint64_t WorkerHash=0; + }; + std::array,FColumnXYHash>,9> ColumnUsesByLOD; + std::array ColumnDuplicateEventsByLOD{},NeighbourDuplicateEventsByLOD{}; + std::array CrossWorkerNeighbourDuplicateEventsByLOD{}; + std::array ColumnCaptureTilesByLOD{},ExpectedTilesByLOD{}; + std::array ColumnCaptureCompleteByLOD{}; + ColumnCaptureCompleteByLOD.fill(true); + std::array WorkerSecondsByLOD{}; + std::array ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{}; + std::array GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{}; + std::array MeshAssemblyMsByLOD{}; + std::array TileCountByLOD{}; + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(Tile.Level>=0&&Tile.Level(ExpectedTilesByLOD.size())) + ++ExpectedTilesByLOD[static_cast(Tile.Level)]; + const auto AreSameLODNeighbours=[](const FVoxelTileKey& A,const FVoxelTileKey& B) + { + if(A.Level!=B.Level)return false; + const std::int64_t DX=std::abs(static_cast(A.Coord.X)-B.Coord.X); + const std::int64_t DY=std::abs(static_cast(A.Coord.Y)-B.Coord.Y); + const std::int64_t DZ=std::abs(static_cast(A.Coord.Z)-B.Coord.Z); + return std::max({DX,DY,DZ})<=1&&(DX+DY+DZ)>0; + }; + uint64 NativeSolidBoxes=0,NativeAirBoxes=0,NativeMixedBoxes=0,NativeUnknownBoxes=0; + uint64 NativeFieldCalls=0,NativeColumnHits=0,NativeColumnMisses=0,NativeMeshingSkips=0; + double NativeClassificationMilliseconds=0.0,NativeColumnComputeMilliseconds=0.0; + double NativeMeshingMilliseconds=0.0; + for(const FVoxelTileKey& Tile:Desired.Tiles) + if(Tile.Level Ready=Streamer.TakeReady(); + if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;} + for(const FStandaloneCpuTile& Tile:Ready) + { + ++Finished; + if(Tile.Key.Level>=0&&Tile.Key.Level(TileCountByLOD.size())) + { + const std::size_t LOD=static_cast(Tile.Key.Level); + ++TileCountByLOD[LOD]; + WorkerSecondsByLOD[LOD]+=Tile.WorkerSeconds; + ColumnEvalMsByLOD[LOD]+=Tile.CoreDiagnostics.columnEvaluationExclusiveMilliseconds; + MarchingExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.marchingCubesExclusiveMilliseconds; + GroundRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.groundRefinementExclusiveMilliseconds; + CapRefineExclusiveMsByLOD[LOD]+=Tile.CoreDiagnostics.capRefinementExclusiveMilliseconds; + MeshAssemblyMsByLOD[LOD]+=Tile.CoreDiagnostics.meshAssemblyMilliseconds; + } + if(bProfileColumnOverlap&&Tile.Key.Level>=0 + &&Tile.Key.Level(ColumnCaptureTilesByLOD.size())) + { + const std::size_t LOD=static_cast(Tile.Key.Level); + ++ColumnCaptureTilesByLOD[LOD]; + ColumnCaptureCompleteByLOD[LOD]=ColumnCaptureCompleteByLOD[LOD] + &&Tile.CoreDiagnostics.columnCoordinateCaptureComplete; + std::vector> Coordinates= + Tile.CoreDiagnostics.evaluatedColumnCoordinates; + std::sort(Coordinates.begin(),Coordinates.end()); + Coordinates.erase(std::unique(Coordinates.begin(),Coordinates.end()),Coordinates.end()); + for(const auto& Coordinate:Coordinates) + { + const FColumnXY XY{Coordinate[0],Coordinate[1]}; + auto [Uses,Inserted]=ColumnUsesByLOD[LOD].try_emplace(XY); + std::vector& PriorUses=Uses->second; + if(Inserted) + { + PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); + continue; + } + ++ColumnDuplicateEventsByLOD[LOD]; + bool bHasNeighbourDuplicate=false,bHasCrossWorkerNeighbourDuplicate=false; + for(const FFirstColumnUse& Prior:PriorUses) + { + if(!AreSameLODNeighbours(Prior.Tile,Tile.Key))continue; + bHasNeighbourDuplicate=true; + bHasCrossWorkerNeighbourDuplicate=bHasCrossWorkerNeighbourDuplicate + ||Prior.WorkerHash!=Tile.CoreDiagnostics.columnEvaluationWorkerHash; + } + if(bHasNeighbourDuplicate)++NeighbourDuplicateEventsByLOD[LOD]; + if(bHasCrossWorkerNeighbourDuplicate) + ++CrossWorkerNeighbourDuplicateEventsByLOD[LOD]; + PriorUses.push_back({Tile.Key,Tile.CoreDiagnostics.columnEvaluationWorkerHash}); + } + } + WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt); + if(Neighborhood.contains(Tile.Key)) + { + ++NeighborhoodFinished; + NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt); + } + if(Tile.Key.Level(Neighborhood.size())) + { + std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n", + std::chrono::duration(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished); + bNeighborhoodReported=true; + } + if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected) + { + std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n", + std::chrono::duration(FullDetailReady-ColdStart).count(), + FullDetailFinished,FullDetailExpected); + bFullDetailReported=true; + } + } + Streamer.Stop(); + bool bColumnOverlapCapturePassing=true; + if(bProfileColumnOverlap) + { + for(std::size_t LOD=0;LOD(ColumnCaptureTilesByLOD[LOD]), + static_cast(ExpectedTilesByLOD[LOD]), + ColumnUsesByLOD[LOD].size(), + static_cast(ColumnDuplicateEventsByLOD[LOD]), + static_cast(NeighbourDuplicateEventsByLOD[LOD]), + static_cast(CrossWorkerNeighbourDuplicateEventsByLOD[LOD]), + bLODComplete?1:0); + } + } + for(std::size_t LOD=0;LOD(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD], + ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD], + GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],MeshAssemblyMsByLOD[LOD]); + } + const double NeighborhoodSeconds=std::chrono::duration(NeighborhoodReady-ColdStart).count(); + const double NeighborhoodMeshSeconds=std::chrono::duration(NeighborhoodReady-ShapeReady).count(); + const double FullDetailSeconds=std::chrono::duration(FullDetailReady-ColdStart).count(); + const double WholeViewSeconds=std::chrono::duration(WholeViewReady-ColdStart).count(); + const double StreamSeconds=std::chrono::duration(WholeViewReady-StreamStart).count(); + std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n", + WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds, + FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished, + FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds); + std::printf("[WorldAssemblerTilePipeline] full_detail_tiles=%d solid_boxes=%llu air_boxes=%llu mixed_boxes=%llu unknown_boxes=%llu whole_tile_skips=%llu field_calls=%llu column_cache_hits=%llu column_cache_misses=%llu column_compute_worker_ms=%.3f classification_worker_ms=%.3f meshing_worker_ms=%.3f\n", + FullDetailExpected,static_cast(NativeSolidBoxes), + static_cast(NativeAirBoxes), + static_cast(NativeMixedBoxes), + static_cast(NativeUnknownBoxes), + static_cast(NativeMeshingSkips), + static_cast(NativeFieldCalls), + static_cast(NativeColumnHits), + static_cast(NativeColumnMisses), + NativeColumnComputeMilliseconds,NativeClassificationMilliseconds, + NativeMeshingMilliseconds); + + bool bHashesPassing=true; + if(bHashDensity) + { + bHashesPassing=DensityRows.size()==Desired.Tiles.size() + &&MeshRows.size()==Desired.Tiles.size() + &&PayloadRows.size()==Desired.Tiles.size() + &&TopologyRows.size()==Desired.Tiles.size(); + std::vector FullDensity,FullMesh,FullPayload,FullTopology; + for(const FHashedTile& Row:DensityRows)if(Row.Key.Level(HashTileRows(FullDensity)), + static_cast(HashTileRows(FullMesh)), + static_cast(HashTileRows(FullPayload)), + static_cast(HashTileRows(FullTopology))); + std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n", + Desired.Tiles.size(),static_cast(HashTileRows(DensityRows)), + static_cast(HashTileRows(MeshRows)), + static_cast(HashTileRows(PayloadRows)), + static_cast(HashTileRows(TopologyRows))); + } + return NeighborhoodFinished==static_cast(Neighborhood.size()) + &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing + &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6; + } +} // namespace StandaloneStreamBenchmark diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h new file mode 100644 index 0000000..e001eb7 --- /dev/null +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -0,0 +1,427 @@ +#pragma once + +#include "OwnerWorldLoader.h" +#include "VoxelForgeCore/VoxelForgeCore.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace StandaloneWorldAssemblyPrototype +{ + inline std::int64_t WorldAssemblyFixed(double voxels) + {return static_cast(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));} + + inline std::int64_t WorldAssemblyQ16(double value) + {return static_cast(std::llround(value*65536.0));} + + inline std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels) + { + const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels; + return std::max(1,WorldAssemblyFixed(wavelength)); + } + + inline uint64 WorldAssemblyFeatureId(uint32 seed,int64 x,int64 y,uint32 kind) + { + uint64 hash=14695981039346656037ull; + const auto add=[&](uint64 value,int bytes) + { + for(int i=0;i(value>>(8*i));hash*=1099511628211ull;} + }; + add(seed,4);add(static_cast(x),8);add(static_cast(y),8);add(kind,4); + return hash?hash:1; + } + + inline bool BuildFiniteStrateRoute(VoxelForgeCore::WorldAssemblyRecipeV1& recipe, + VoxelForgeCore::WorldAssemblyStratumPlanV1& stratum,std::uint64_t& tested, + std::uint64_t& gradeRejected,std::uint64_t& sweepRejected,std::string& error) + { + using namespace VoxelForgeCore; + constexpr std::int64_t PlayerRadiusQ=348; // 0.34 m in Core's 0.25 m voxel scale. + constexpr std::int64_t PlayerHeightQ=1802; // 1.76 m in Core's 0.25 m voxel scale. + constexpr std::int64_t HalfHeightQ=PlayerHeightQ/2; + constexpr std::int64_t StepQ=128; // 0.5 Core voxel, finer than the density lattice. + constexpr std::int64_t ProbeRadiusQ=174; + constexpr std::int64_t SupportDepthQ=256; + constexpr std::int64_t GradeQ16=17560; // tan(15 degrees), rounded upward. + constexpr std::int64_t RouteRadiusQ=4*WorldAssemblySubvoxelsPerVoxel; + struct Direction {std::int32_t x,y;}; + constexpr std::array directions{{ + {65536,0},{46341,46341},{0,65536},{-46341,46341}, + {-65536,0},{-46341,-46341},{0,-65536},{46341,-46341}}}; + struct Candidate + { + std::int64_t score=0; + std::int32_t centerX=0,centerY=0,direction=0; + std::vector points; + }; + + auto& constraints=stratum.routeConstraints; + constraints.version=1; + constraints.capsuleRadiusSubvoxels=PlayerRadiusQ; + constraints.capsuleHeightSubvoxels=PlayerHeightQ; + constraints.maximumGradeQ16=GradeQ16; + constraints.sweepSpacingSubvoxels=StepQ; + constraints.supportProbeRadiusSubvoxels=ProbeRadiusQ; + constraints.supportSearchDepthSubvoxels=SupportDepthQ; + constraints.minimumClearanceSubvoxels=8; + constraints.minimumSupportProbes=3; + + const Heightfield2D finalField(recipe,stratum); + if(!finalField.isValid()) + {error="route planner received an invalid final composed field";return false;} + std::vector candidates; + constexpr std::int32_t halfLength=128; + constexpr std::int32_t waypointSpacing=8; + for(std::int32_t centerY=-256;centerY<=256;centerY+=64) + for(std::int32_t centerX=-256;centerX<=256;centerX+=64) + for(std::int32_t directionIndex=0;directionIndex(directions.size());++directionIndex) + { + Candidate candidate; + candidate.centerX=centerX;candidate.centerY=centerY;candidate.direction=directionIndex; + candidate.points.reserve(33); + std::int64_t previousGround=0,previousX=0,previousY=0; + bool gradePass=true; + for(std::int32_t index=0;index<=32;++index) + { + const std::int64_t along=(index-16)*waypointSpacing; + const std::int64_t x=centerX+std::llround( + static_cast(along)*directions[directionIndex].x/65536.0); + const std::int64_t y=centerY+std::llround( + static_cast(along)*directions[directionIndex].y/65536.0); + const auto column=finalField.point(WorldAssemblyFixed(static_cast(x)), + WorldAssemblyFixed(static_cast(y)),0); + const std::int64_t horizontalQ=static_cast(std::llround( + std::hypot(static_cast(x-previousX),static_cast(y-previousY)) + *WorldAssemblySubvoxelsPerVoxel)); + if(index>0) + { + const std::int64_t riseQ=std::abs(column.groundHeightSubvoxels-previousGround); + if(horizontalQ==0||riseQ*65536>horizontalQ*GradeQ16) + {gradePass=false;break;} + candidate.score+=riseQ*65536/std::max(1,horizontalQ); + } + WorldAssemblyRoutePointV1 point; + point.positionSubvoxels={WorldAssemblyFixed(static_cast(x)), + WorldAssemblyFixed(static_cast(y)),column.groundHeightSubvoxels+HalfHeightQ}; + point.radiusSubvoxels=RouteRadiusQ; + point.stableFeatureId=WorldAssemblyFeatureId(recipe.seed, + static_cast(x),static_cast(y), + 0x52540000u+static_cast(index)); + candidate.points.push_back(point); + previousGround=column.groundHeightSubvoxels;previousX=x;previousY=y; + } + ++tested; + if(gradePass)candidates.push_back(std::move(candidate)); + else ++gradeRejected; + } + std::stable_sort(candidates.begin(),candidates.end(),[](const Candidate& a,const Candidate& b) + { + if(a.score!=b.score)return a.score0) + for(std::size_t pointIndex=0;pointIndex0) + std::printf("[WorldAssemblerRouteRepaired] uniform_lift_q=%lld support_limit_q=%lld\n", + static_cast(liftQ),static_cast(SupportDepthQ)); + return true; + } + if(audit.blockedCapsuleSamples==0||liftQ>=MaximumRepairLiftQ)break; + const std::int64_t needed=std::max(1, + constraints.minimumClearanceSubvoxels-audit.minimumClearanceSubvoxels); + const std::int64_t steps=(needed+RepairStepQ-1)/RepairStepQ; + const std::int64_t nextLift=std::max(liftQ+RepairStepQ,liftQ+steps*RepairStepQ); + if(nextLift>MaximumRepairLiftQ)break; + liftQ=nextLift; + ++repairAttempt; + } + if(sweepAttempt<3) + std::printf("[WorldAssemblerRouteCandidateRejected] ordinal=%llu blocked=%llu unsupported=%llu grade_failures=%llu minimum_clearance_q=%lld\n", + static_cast(sweepAttempt), + static_cast(lastAudit.blockedCapsuleSamples), + static_cast(lastAudit.unsupportedCenterSamples), + static_cast(lastAudit.gradeFailures), + static_cast(lastAudit.minimumClearanceSubvoxels)); + ++sweepAttempt; + ++sweepRejected; + } + std::printf("[WorldAssemblerRouteSearchFailed] tested=%llu grade_rejected=%llu sweepable_candidates=%llu sweep_rejected=%llu last_blocked=%llu last_unsupported=%llu last_grade_failures=%llu\n", + static_cast(tested),static_cast(gradeRejected), + static_cast(candidates.size()),static_cast(sweepRejected), + static_cast(lastAudit.blockedCapsuleSamples), + static_cast(lastAudit.unsupportedCenterSamples), + static_cast(lastAudit.gradeFailures)); + std::printf("[WorldAssemblerRouteRepairSummary] repair_attempts=%llu\n", + static_cast(repairAttempt)); + stratum.route.clear(); + error="finite-strate route candidates failed the composed-field capsule and support sweep"; + return false; + } + + inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root, + std::string& error,const std::string& manifestOutputPath) + { + if(!owner.Manager||!owner.Generator) + {error="world assembler prototype needs the resolved standalone owner world";return false;} + int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0; + bool foundSurface=false; + for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ) + { + if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ)) + !=ECaveGeneratorType::SurfaceWorld)continue; + surfaceChunkZ=chunkZ; + foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ); + break; + } + if(!foundSurface) + {error="world assembler prototype could not find a finite SurfaceWorld strate";return false;} + const FIntVector chunk(0,0,surfaceChunkZ); + const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk); + const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk); + const std::int64_t bottomVoxel=static_cast(bottomChunkZ)*CHUNK_SIZE; + const std::int64_t heightVoxels=static_cast(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE; + const FBiomeMapParams& map=biomeContext.Map; + const UVoxelStrateDefinition* definition=owner.Manager->GetStrateForChunk(chunk); + + VoxelForgeCore::WorldAssemblyManifest manifest; + auto& recipe=manifest.recipe; + recipe.seed=static_cast(owner.Seed); + recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize); + recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength); + recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0); + recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend); + recipe.biomeClimateReliefWavelengthSubvoxels=WorldAssemblyWavelength(map.ReliefFrequency,3333.0); + recipe.biomeClimateMoistureWavelengthSubvoxels=WorldAssemblyWavelength(map.MoistureFrequency,2500.0); + recipe.biomeClimateReliefContrastQ16=WorldAssemblyQ16(map.ReliefContrast); + const auto makeProfile=[&](uint32 stableId,const FSurfaceGenerationParams& params, + const FBiomeResolved* climate) + { + VoxelForgeCore::WorldAssemblyBiomeProfileV1 profile; + profile.stableId=stableId; + profile.baseHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) + +static_cast(heightVoxels)*params.BaseGroundRelative); + profile.continentAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange); + profile.continentWavelengthSubvoxels=WorldAssemblyWavelength(params.ContinentFrequency,1000.0); + profile.mountainAmplitudeSubvoxels=WorldAssemblyFixed(params.ElevationRange*params.MountainStrength); + profile.mountainWavelengthSubvoxels=WorldAssemblyWavelength(params.MountainFrequency,500.0); + profile.detailAmplitudeSubvoxels=WorldAssemblyFixed(params.SurfaceRoughness); + profile.detailWavelengthSubvoxels=WorldAssemblyWavelength(params.DetailFrequency,25.0); + profile.reliefMinimumQ16=climate?WorldAssemblyQ16(climate->ReliefMin):0; + profile.reliefMaximumQ16=climate?WorldAssemblyQ16(climate->ReliefMax):65536; + profile.moistureMinimumQ16=climate?WorldAssemblyQ16(climate->MoistureMin):0; + profile.moistureMaximumQ16=climate?WorldAssemblyQ16(climate->MoistureMax):65536; + profile.reliefStrengthQ16=WorldAssemblyQ16(params.ReliefStrength); + profile.reliefContrastQ16=WorldAssemblyQ16(params.ReliefContrast); + profile.reliefWavelengthSubvoxels=WorldAssemblyWavelength(params.ReliefFrequency,667.0); + profile.heightWarpStrengthSubvoxels=WorldAssemblyFixed(params.HeightWarpStrength); + profile.heightWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.HeightWarpFrequency,125.0); + profile.beachWidthSubvoxels=WorldAssemblyFixed(params.BeachWidth); + profile.cliffStrengthQ16=WorldAssemblyQ16(params.CliffStrength); + profile.cliffSlopeThresholdQ16=std::max(1,WorldAssemblyQ16(params.CliffSlopeThreshold)); + profile.cliffSharpnessQ16=WorldAssemblyQ16(params.CliffSharpness); + profile.cliffSampleDistanceSubvoxels=WorldAssemblyFixed(std::max(0.5f,params.CliffSampleDist)); + const double structuralAbsBound=static_cast(params.ElevationRange) + +static_cast(params.ElevationRange)*params.MountainStrength + +2.0*params.SurfaceRoughness; + const double cliffAbsBound=2.0*structuralAbsBound*params.CliffStrength*params.CliffSharpness; + profile.cliffMaximumDisplacementSubvoxels=WorldAssemblyFixed(params.CliffStrength>0.0f + ?std::max(1.0,cliffAbsBound):0.0); + profile.hasCapProfile=true; + profile.capHeightSubvoxels=WorldAssemblyFixed(static_cast(bottomVoxel) + +static_cast(heightVoxels)*params.CeilingRelative); + profile.capWarpStrengthSubvoxels=WorldAssemblyFixed(params.CeilingWarpStrength); + profile.capWarpWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingWarpFrequency,100.0); + profile.capUndulationSubvoxels=WorldAssemblyFixed(params.CeilingUndulation); + profile.capWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingUndulationFrequency,500.0); + profile.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRoughness); + profile.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRoughnessFrequency,25.0); + profile.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(params.CeilingRidgeStrength); + profile.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(params.CeilingRidgeFrequency,50.0); + return profile; + }; + if(biomeContext.IsValid()&&definition) + { + for(const FBiomeResolved& resolved:biomeContext.Biomes) + { + const UVoxelBiomeDefinition* biome=definition->Biomes.IsValidIndex(resolved.Index) + ?definition->Biomes[resolved.Index]:nullptr; + FSurfaceGenerationParams params=surface; + if(biome&&biome->bOverrideTerrain&&biome->GeneratorType==ECaveGeneratorType::SurfaceWorld) + { + params=biome->SurfaceParams; + params.StrateTopWorldZ=surface.StrateTopWorldZ; + params.StrateBottomWorldZ=surface.StrateBottomWorldZ; + params.BoundarySealThickness=surface.BoundarySealThickness; + params.BaseDensity=surface.BaseDensity; + params.WaterLevelRelative=surface.WaterLevelRelative; + } + recipe.biomes.push_back(makeProfile(0x1001u+static_cast(resolved.Index),params,&resolved)); + } + } + if(recipe.biomes.empty())recipe.biomes.push_back(makeProfile(0x1001u,surface,nullptr)); + + VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum; + stratum.strateIndex=owner.Manager->GetStrateIndex( + static_cast((static_cast(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE)); + stratum.topChunkZ=topChunkZ; + stratum.bottomChunkZ=bottomChunkZ; + const double strateBottom=static_cast(bottomVoxel); + const double strateHeight=static_cast(heightVoxels); + stratum.capHeightSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.CeilingRelative); + stratum.capUndulationSubvoxels=WorldAssemblyFixed(surface.CeilingUndulation); + stratum.capWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingUndulationFrequency,250.0); + stratum.capDetailAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRoughness); + stratum.capDetailWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRoughnessFrequency,25.0); + stratum.capRidgeAmplitudeSubvoxels=WorldAssemblyFixed(surface.CeilingRidgeStrength); + stratum.capRidgeWavelengthSubvoxels=WorldAssemblyWavelength(surface.CeilingRidgeFrequency,50.0); + stratum.waterSurfaceSubvoxels=WorldAssemblyFixed(strateBottom+strateHeight*surface.WaterLevelRelative); + stratum.waterEnabled=definition&&definition->bHasWater&&surface.WaterLevelRelative>0.0f; + + // Select a deterministic steep edge in the resolved owner field for a bounded shelf. + // Selection d'une pente deterministe dans le champ resolu pour une saillie bornee. + constexpr int probeSide=49,probeStep=32; + std::array heights{}; + std::array valid{}; + for(int y=0;y((x-probeSide/2)*probeStep); + const float py=static_cast((y-probeSide/2)*probeStep); + float ceiling=0.0f; + const std::size_t index=static_cast(y)*probeSide+x; + valid[index]=owner.Generator->GetSurfaceHeightAt(px,py,topChunkZ,heights[index],ceiling)?1u:0u; + } + double steepest=0.0;int cliffX=0,cliffY=0;bool xAxis=true;int downhill=1; + for(int y=1;y(y)*probeSide+x; + if(!valid[center]||!valid[center-1]||!valid[center+1]||!valid[center-probeSide]||!valid[center+probeSide])continue; + const double dx=static_cast(heights[center+1]-heights[center-1]); + const double dy=static_cast(heights[center+probeSide]-heights[center-probeSide]); + const double slope=(std::abs(dx)+std::abs(dy))/(4.0*probeStep); + if(slope>steepest){steepest=slope;cliffX=(x-probeSide/2)*probeStep;cliffY=(y-probeSide/2)*probeStep; + xAxis=std::abs(dx)>=std::abs(dy);downhill=(xAxis?dx:dy)>=0.0?-1:1;} + } + if(steepest>0.05) + { + float ground=0.0f,ceiling=0.0f; + if(owner.Generator->GetSurfaceHeightAt(static_cast(cliffX),static_cast(cliffY), + topChunkZ,ground,ceiling)) + { + VoxelForgeCore::WorldAssemblyFeatureV1 shelf; + shelf.stableId=WorldAssemblyFeatureId(recipe.seed,cliffX,cliffY,0x4f564552u); + shelf.type=4; + constexpr int reach=72,width=36,height=64,feather=8; + const int dirX=xAxis?downhill:0,dirY=xAxis?0:downhill; + const int minX=xAxis?(dirX>0?cliffX-feather:cliffX-reach-feather) + :cliffX-width-feather; + const int maxX=xAxis?(dirX>0?cliffX+reach+feather:cliffX+feather) + :cliffX+width+feather; + const int minY=xAxis?cliffY-width-feather + :(dirY>0?cliffY-feather:cliffY-reach-feather); + const int maxY=xAxis?cliffY+width+feather + :(dirY>0?cliffY+reach+feather:cliffY+feather); + VoxelForgeCore::Heightfield2D baseField(recipe,stratum); + double lowestGround=std::numeric_limits::infinity(); + double highestGround=-std::numeric_limits::infinity(); + for(int sampleY=minY;sampleY<=maxY;sampleY+=4) + for(int sampleX=minX;sampleX<=maxX;sampleX+=4) + { + const auto sample=baseField.point(WorldAssemblyFixed(sampleX), + WorldAssemblyFixed(sampleY),0); + const double heightQ=static_cast(sample.groundHeightSubvoxels) + /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; + lowestGround=std::min(lowestGround,heightQ); + highestGround=std::max(highestGround,heightQ); + } + if(!std::isfinite(lowestGround)||!std::isfinite(highestGround)) + {error="world assembler cliff shelf could not bound its terrain columns";return false;} + shelf.minimumSubvoxels={WorldAssemblyFixed(minX),WorldAssemblyFixed(minY), + WorldAssemblyFixed(std::max(bottomVoxel,lowestGround-4.0))}; + shelf.maximumSubvoxels={WorldAssemblyFixed(maxX),WorldAssemblyFixed(maxY), + WorldAssemblyFixed(std::min((topChunkZ+1)*CHUNK_SIZE, + highestGround+height+4.0))}; + shelf.parametersSubvoxels={WorldAssemblyFixed(dirX),WorldAssemblyFixed(dirY), + WorldAssemblyFixed(height),WorldAssemblyFixed(reach)}; + stratum.features.push_back(shelf); + } + } + std::uint64_t routeCandidates=0,routeGradeRejected=0,routeSweepRejected=0; + if(!BuildFiniteStrateRoute(recipe,stratum,routeCandidates,routeGradeRejected, + routeSweepRejected,error))return false; + std::printf("[WorldAssemblerRoutePlan] candidates=%llu grade_rejected=%llu sweep_rejected=%llu accepted_points=%zu capsule_m=0.34x1.76 corridor_radius_vox=4 support_probe_points=5 minimum_support_probes=%u status=PASS\n", + static_cast(routeCandidates), + static_cast(routeGradeRejected), + static_cast(routeSweepRejected),stratum.route.size(), + stratum.routeConstraints.minimumSupportProbes); + manifest.plan.strates.push_back(std::move(stratum)); + if(!manifest.finalize(error))return false; + const std::string manifestText=manifest.ToJson(); + const auto roundTrip=VoxelForgeCore::WorldAssemblyManifest::FromJson(manifestText,error); + if(!roundTrip||!roundTrip->validate(error)||roundTrip->ToJson()!=manifestText + ||roundTrip->contentHash!=manifest.contentHash) + {if(error.empty())error="world assembly canonical plan serialization did not round-trip";return false;} + std::printf("[WorldAssemblerPlanSerialization] bytes=%zu canonical_round_trip=true route_points=%zu plan_hash=%016llx manifest_hash=%016llx status=PASS\n", + manifestText.size(),manifest.plan.strates.front().route.size(), + static_cast(manifest.plan.contentHash), + static_cast(manifest.contentHash)); + if(!manifestOutputPath.empty()) + { + const std::filesystem::path outputPath(manifestOutputPath); + std::error_code directoryError; + if(!outputPath.parent_path().empty()) + std::filesystem::create_directories(outputPath.parent_path(),directoryError); + if(directoryError) + {error="could not create the world assembly manifest output directory";return false;} + std::ofstream output(outputPath,std::ios::binary); + if(!output) + {error="could not open the world assembly manifest output path";return false;} + output.write(manifestText.data(),static_cast(manifestText.size())); + if(!output) + {error="could not write the world assembly manifest payload";return false;} + } + VFJson::Value manifestJson; + if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false; + auto* rootObject=std::get_if(&root.Data); + if(!rootObject) + {error="owner snapshot root is not a JSON object";return false;} + (*rootObject)["world_assembly"]=std::move(manifestJson); + std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=%zu features=%zu\n", + manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(), + manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ, + manifestText.size(),static_cast(manifest.recipe.contentHash), + static_cast(manifest.plan.contentHash), + static_cast(manifest.contentHash),manifest.plan.strates.front().route.size(), + manifest.plan.strates.front().features.size()); + return true; + } + +} // namespace StandaloneWorldAssemblyPrototype