diff --git a/Standalone/BuildLinux.sh b/Standalone/BuildLinux.sh index 617cc0d..9e96f42 100755 --- a/Standalone/BuildLinux.sh +++ b/Standalone/BuildLinux.sh @@ -33,6 +33,8 @@ if [[ "$FP_CONTRACT" != "default" && "$FP_CONTRACT" != "off" && "$FP_CONTRACT" ! 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; } +command -v pkg-config >/dev/null || { printf 'pkg-config not found\n' >&2; exit 2; } +pkg-config --exists egl glesv2 libpng || { printf 'EGL, GLESv2, and libpng development files are required\n' >&2; exit 2; } BUILD_DIR="$ROOT/Build/linux-$VARIANT" BIN_DIR="$ROOT/bin/linux" @@ -117,5 +119,10 @@ CORE_LIBRARY="$BUILD_DIR/libVoxelForgeCore.a" "$(command -v ar)" rcs "$CORE_LIBRARY" "${CORE_OBJECTS[@]}" OUTPUT="$BIN_DIR/VoxelForgeCoreHeadless-$VARIANT" "$COMPILER" -pthread "${LEGACY_OBJECTS[@]}" "$CORE_LIBRARY" -o "$OUTPUT" +CAPTURE_OUTPUT="$BIN_DIR/VoxelForgeOffscreenCapture-$VARIANT" +"$COMPILER" -std=c++20 -O2 -fno-fast-math -pthread \ + "$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \ + $(pkg-config --cflags --libs egl glesv2 libpng) printf 'Built Core library: %s\nBuilt headless benchmark: %s\n' "$CORE_LIBRARY" "$OUTPUT" +printf 'Built EGL offscreen capture: %s\n' "$CAPTURE_OUTPUT" printf 'Compiler: %s | variant: %s | fp-contract: %s | jobs: %s\n' "$COMPILER" "$VARIANT" "$FP_CONTRACT" "$JOBS" diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 8466849..b6caf5f 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -137,6 +137,7 @@ namespace VoxelForgeCore double marchingCubesGradientNormalsMilliseconds = 0.0; double marchingCubesEdgeInterpolationMilliseconds = 0.0; double marchingCubesVertexDedupStreamMilliseconds = 0.0; + double marchingCubesSkirtMilliseconds = 0.0; double marchingCubesOtherMilliseconds = 0.0; std::uint64_t marchingCubesGradientFallbackDensityCalls = 0; std::uint64_t marchingCubesEdgeMidpointDensityCalls = 0; @@ -704,6 +705,10 @@ namespace VoxelForgeCore std::int64_t maximumZSubvoxels, std::int32_t stepVoxels) const noexcept; static void resetThreadColumnCacheStats(bool captureCoordinates = false, bool clearCache = false, std::uint32_t octaveDrop = 0) noexcept; + static void setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept; + static std::uint32_t threadColumnOctaveDrop() noexcept; + static void setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept; + static std::int64_t threadColumnInterpolationStrideSubvoxels() noexcept; static void setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept; static const HeightfieldColumnCacheStats& threadColumnCacheStats() noexcept; FieldInterval interval(std::int64_t minimumXSubvoxels, @@ -1050,6 +1055,10 @@ namespace VoxelForgeCore // Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version; // densite et hashes LOD0-3 restent inchanges. std::uint32_t farHorizonVersion = 3; + // Linux research controls for reproducing L5 negative controls. Defaults preserve output. + std::uint32_t diagnosticGlobalOctaveDrop = 0; + std::uint32_t diagnosticCellStride = 1; + std::uint32_t diagnosticColumnInterpolationStride = 1; }; class VFCORE_API World diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 98828ad..58763cf 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1446,6 +1446,14 @@ namespace VoxelForgeCore } } } + int32 MeshCells=cells,MeshStep=step; + if(options.diagnosticCellStride>1&&level>=3) + { + const int32 Stride=static_cast(std::min( + options.diagnosticCellStride,static_cast(std::max(1,cells)))); + MeshCells=FMath::Max(1,cells/Stride); + MeshStep=FMath::Max(1,extent/MeshCells); + } int64 sampleCount=0;std::vector densityValues; FVoxelMarchingCubesStageTimings McStageTimings; TileMesh out;out.key=key; @@ -1678,13 +1686,32 @@ namespace VoxelForgeCore out.diagnostics.featureListMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-FeatureListStart).count(); const ScopedWorldAssemblyFeatureQueriesV1 featureScope(featureQueries); + const bool bDiagnosticColumnSampling=options.diagnosticGlobalOctaveDrop!=0 + ||options.diagnosticColumnInterpolationStride>1; + const std::uint32_t PreviousOctaveDrop=bDiagnosticColumnSampling + ?Heightfield2D::threadColumnOctaveDrop():0; + const std::int64_t PreviousInterpolationStride=bDiagnosticColumnSampling + ?Heightfield2D::threadColumnInterpolationStrideSubvoxels():0; + if(bDiagnosticColumnSampling) + { + Heightfield2D::setThreadColumnOctaveDrop(options.diagnosticGlobalOctaveDrop); + Heightfield2D::setThreadColumnInterpolationStrideSubvoxels( + options.diagnosticColumnInterpolationStride>1&&level>=3 + ?static_cast(MeshStep)*WorldAssemblySubvoxelsPerVoxel + *options.diagnosticColumnInterpolationStride:0); + } const double ColumnMillisecondsBeforeMeshing= VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds; const auto MarchingCubesStart=std::chrono::steady_clock::now(); - FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr, + FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr, MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr, (options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr, options.profileMesherStages?&McStageTimings:nullptr); + if(bDiagnosticColumnSampling) + { + Heightfield2D::setThreadColumnOctaveDrop(PreviousOctaveDrop); + Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(PreviousInterpolationStride); + } const double MarchingCubesWallMilliseconds=std::chrono::duration( std::chrono::steady_clock::now()-MarchingCubesStart).count(); const double ColumnMillisecondsAfterMeshing= @@ -1695,6 +1722,10 @@ namespace VoxelForgeCore ColumnMillisecondsAfterMeshing-ColumnMillisecondsBeforeMeshing); if(options.profileMesherStages) { + out.diagnostics.marchingCubesSkirtMilliseconds=McStageTimings.SkirtMilliseconds; + out.diagnostics.marchingCubesExclusiveMilliseconds=std::max(0.0, + out.diagnostics.marchingCubesExclusiveMilliseconds + -out.diagnostics.marchingCubesSkirtMilliseconds); out.diagnostics.marchingCubesDensityGridMilliseconds=McStageTimings.DensityGridMilliseconds; out.diagnostics.marchingCubesCellClassificationMilliseconds= McStageTimings.CellClassificationMilliseconds; diff --git a/Standalone/Core/src/WorldAssemblerInternal.h b/Standalone/Core/src/WorldAssemblerInternal.h index 09118f1..5ce67d4 100644 --- a/Standalone/Core/src/WorldAssemblerInternal.h +++ b/Standalone/Core/src/WorldAssemblerInternal.h @@ -1031,6 +1031,7 @@ namespace world_assembly_detail // Audit-only override; normal tile generation resets this to zero before evaluating a recipe. // Surcharge reservee aux mesures; la generation remet zero avant d'evaluer une recette. inline thread_local std::uint32_t HeightfieldColumnOctaveDrop = 0; + inline thread_local std::int64_t HeightfieldColumnInterpolationStrideSubvoxels = 0; inline std::int32_t FractalNoiseQ16(std::int64_t xQ16, std::int64_t yQ16, std::int64_t zQ16, std::uint32_t octaves, bool ridged = false) @@ -1506,6 +1507,43 @@ namespace world_assembly_detail inline HeightfieldPointSample CachedColumn(const WorldAssemblyRecipeV1& recipe, const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y) { + const std::int64_t InterpolationStride=HeightfieldColumnInterpolationStrideSubvoxels; + if(InterpolationStride>0) + { + const std::int64_t X0=FloorDiv(x,InterpolationStride)*InterpolationStride; + const std::int64_t Y0=FloorDiv(y,InterpolationStride)*InterpolationStride; + const std::int64_t X1=X0+InterpolationStride,Y1=Y0+InterpolationStride; + const std::int64_t Fx=x-X0,Fy=y-Y0; + struct FScopedInterpolationSampling + { + std::int64_t Previous=HeightfieldColumnInterpolationStrideSubvoxels; + ~FScopedInterpolationSampling() + {HeightfieldColumnInterpolationStrideSubvoxels=Previous;} + } SamplingScope; + HeightfieldColumnInterpolationStrideSubvoxels=0; + const HeightfieldPointSample C00=CachedColumn(recipe,plan,X0,Y0); + const HeightfieldPointSample C10=CachedColumn(recipe,plan,X1,Y0); + const HeightfieldPointSample C01=CachedColumn(recipe,plan,X0,Y1); + const HeightfieldPointSample C11=CachedColumn(recipe,plan,X1,Y1); + const auto Lerp=[&](std::int64_t A,std::int64_t B,std::int64_t T) + {return A+((B-A)*T)/InterpolationStride;}; + const auto Bilinear=[&](std::int64_t A,std::int64_t B, + std::int64_t C,std::int64_t D) + { + const std::int64_t Top=Lerp(A,B,Fx),Bottom=Lerp(C,D,Fx); + return Lerp(Top,Bottom,Fy); + }; + HeightfieldPointSample Out=C00; + Out.groundHeightSubvoxels=Bilinear(C00.groundHeightSubvoxels, + C10.groundHeightSubvoxels,C01.groundHeightSubvoxels, + C11.groundHeightSubvoxels); + Out.capHeightSubvoxels=Bilinear(C00.capHeightSubvoxels, + C10.capHeightSubvoxels,C01.capHeightSubvoxels, + C11.capHeightSubvoxels); + return Out; + } + if(HeightfieldColumnOctaveDrop!=0) + return EvaluateColumn(recipe,plan,x,y); auto& cache=HeightfieldColumnCache.Get(); auto& nextWay=HeightfieldColumnCacheNextWay.Get(); HeightfieldColumnCacheEntry* replacement=nullptr; @@ -2058,6 +2096,19 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl void Heightfield2D::setThreadColumnCacheLegacyDirectMapped(bool enabled) noexcept { world_assembly_detail::HeightfieldColumnCacheLegacyDirectMapped=enabled; } +void Heightfield2D::setThreadColumnOctaveDrop(std::uint32_t octaveDrop) noexcept +{ world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop; } + +std::uint32_t Heightfield2D::threadColumnOctaveDrop() noexcept +{ return world_assembly_detail::HeightfieldColumnOctaveDrop; } + +void Heightfield2D::setThreadColumnInterpolationStrideSubvoxels(std::int64_t stride) noexcept +{ world_assembly_detail::HeightfieldColumnInterpolationStrideSubvoxels=std::max(0,stride); } + +std::int64_t Heightfield2D::threadColumnInterpolationStrideSubvoxels() noexcept +{ return world_assembly_detail::HeightfieldColumnInterpolationStrideSubvoxels; } + + const HeightfieldColumnCacheStats& Heightfield2D::threadColumnCacheStats() noexcept { return world_assembly_detail::HeightfieldColumnStats; } diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/MeasureProcess.py b/Standalone/Research/WorldAssemblerM3/Linux5/MeasureProcess.py new file mode 100644 index 0000000..a2e5f39 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux5/MeasureProcess.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +import os +import signal +import subprocess +import sys +import time + +if len(sys.argv) < 4 or sys.argv[2] != "--": + raise SystemExit("usage: MeasureProcess.py LOG -- COMMAND [ARGS...]") + +log_path = sys.argv[1] +command = sys.argv[3:] +os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True) +start = time.monotonic() +with open(log_path, "wb") as log_file: + process = subprocess.Popen(command, stdout=log_file, stderr=subprocess.STDOUT, + start_new_session=True) + max_hwm_kib = 0 + max_rss_kib = 0 + max_pid = process.pid + max_name = "" + deadline = start + 1800.0 + while process.poll() is None: + if time.monotonic() >= deadline: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + print("MONITOR_TIMEOUT_SECONDS=1800", file=log_file, flush=True) + break + stat_by_pid = {} + for entry in os.scandir("/proc"): + if not entry.name.isdigit(): + continue + pid = int(entry.name) + try: + with open(f"/proc/{pid}/stat", "r", encoding="ascii") as stat_file: + fields = stat_file.read().rsplit(")", 1)[1].split() + ppid = int(fields[1]) + stat_by_pid[pid] = ppid + except (OSError, ValueError, IndexError): + continue + descendants = {process.pid} + changed = True + while changed: + changed = False + for pid, ppid in stat_by_pid.items(): + if ppid in descendants and pid not in descendants: + descendants.add(pid) + changed = True + for pid in descendants: + try: + with open(f"/proc/{pid}/status", "r", encoding="ascii") as status_file: + status = status_file.read().splitlines() + values = {line.split(":", 1)[0]: line.split(":", 1)[1].strip() + for line in status if ":" in line} + hwm = int(values.get("VmHWM", "0 kB").split()[0]) + rss = int(values.get("VmRSS", "0 kB").split()[0]) + name = values.get("Name", "") + except (OSError, ValueError, IndexError): + continue + if hwm > max_hwm_kib: + max_hwm_kib, max_pid, max_name = hwm, pid, name + max_rss_kib = max(max_rss_kib, rss) + time.sleep(0.05) + result = process.wait() + elapsed = time.monotonic() - start + for line in ( + f"MONITOR_EXIT_CODE={result}", + f"MONITOR_WALL_SECONDS={elapsed:.3f}", + f"MONITOR_PEAK_PROCESS_HWM_KIB={max_hwm_kib}", + f"MONITOR_PEAK_PROCESS_PID={max_pid}", + f"MONITOR_PEAK_PROCESS_NAME={max_name}", + f"MONITOR_MAX_SAMPLED_PROCESS_RSS_KIB={max_rss_kib}", + ): + log_file.write((line + "\n").encode("utf-8")) +raise SystemExit(result) + diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux5/REPORT.md new file mode 100644 index 0000000..34ed868 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux5/REPORT.md @@ -0,0 +1,165 @@ +# World Assembler M3 — Linux L5 + +**Result:** no octave tail is safely removable under the declared bounds. The exact far meshes already exceed the 1 px field-error and LOD-swap budgets, and the cutoff flag is 2.2% slower in the W12 view. The candidate therefore generated the exact same meshes; recipe version stays 1. + +## Build and machine + +Built `VoxelForgeCoreHeadless-linux5-l5-final7` and its capture target on `assembler/world` at `7770074` with Clang 19 `-ffp-contract=off`, then GCC 14 default contraction, both with 12 jobs. W1/W12 hashes match across both compilers; the L3 FP-contract preamble and runtime self-test remain active. Runs used CPUs 0–11, normal priority, and the 6 GiB process limit. Maximum process HWM was 898,692 KiB (GCC compile); the largest runtime process was 454,828 KiB. CPU governor stayed `powersave`. `/dev/dri/renderD128` was absent, so EGL used Mesa llvmpipe with `LP_NUM_THREADS=12`. No packages or system settings were changed. + +## Capture and gate + +`LinuxOffscreenCapture.cpp` reads the `.vfmesh` export from the headless streaming pipeline, creates a surfaceless EGL/GLES3 pbuffer, and writes 2560×1440 PNGs with libpng. `ffmpeg` made 1280×720 review JPEGs; the largest is 73,923 bytes. The shader combines surface normals and 80 m height bands. Only the small JPEGs were opened for review. + +The gate samples a 65×65 XY grid on the closest native-surface tile at each LOD, excluding skirts. It reports two comparisons: displayed mesh against exact `Heightfield2D` ground/cap roots, and against a regenerated exact same-LOD mesh. The second is the zero-delta regression control; the first measures the actual field error. Swap error compares the coarse tile with its fine children at those same field roots. Full missing-surface, 2 m floating-surface, and missing fine-ring injections were rejected at every LOD3–8; so were both S13 controls. + +This is a sampled heightfield gate, not a full camera-ray rasterizer. It can miss cracks narrower than one grid cell (1 m at LOD3, growing to 32 m at LOD8), and its direct-field row does not model tunnel walls or other non-heightfield faces. The same-LOD mesh comparison covers extra and missing mesh intersections at sampled points; a denser or full-screen geometry pass would be needed to certify sub-cell cracks and non-heightfield surfaces. + +## Closest display distances and budgets + +The code sweeps clipmap desired sets across X-boundary phases and measures tile AABB distance. The result matches the clipmap ring formula `3 × 32 chunks × 2^(LOD−1) × 0.25 m`; LOD0 is the player tile. The pixel angle is `90° / 2560 = 0.000613592 rad`. + +| LOD | Closest display distance | 1 px at that distance | Active rule | +|---:|---:|---:|---:| +| 0 | 0 m | 0 cm | exact, 2 cm | +| 1 | 24.001 m | 1.473 cm | exact, 2 cm | +| 2 | 48.001 m | 2.945 cm | exact, 2 cm | +| 3 | 96.001 m | 5.891 cm | 1 px | +| 4 | 192.001 m | 11.781 cm | 1 px | +| 5 | 384.001 m | 23.562 cm | 1 px | +| 6 | 768.001 m | 47.124 cm | 1 px | +| 7 | 1,536.001 m | 94.248 cm | 1 px | +| 8 | 3,072.001 m | 188.496 cm | 1 px | + +## Exact-field error and swap pop + +Baseline and cutoff values are identical. The exact same-LOD regression control is 0.0000 px p50/p99/max at all LOD3–8, with no missing or floating samples. Against the continuous `Heightfield2D` roots, however, the current exact meshes already exceed 1 px at every LOD. The owner’s expected near-zero control holds for exact-versus-exact mesh output, not for the true-field comparison. + +| LOD | Exact field p50 / p99 / max px (baseline → cutoff) | Swap p50 / p99 / max px | Swap cracks / floating | +|---:|---:|---:|---:| +| 3 → 2 | 0.9035 / 4.5839 / 7.3277 → same | 0.6714 / 4.1861 / 7.3277 | 0 / 0 | +| 4 → 3 | 1.1170 / 5.6056 / 8.3307 → same | 0.8269 / 5.1958 / 8.2105 | 0 / 0 | +| 5 → 4 | 1.5667 / 45.3010 / 126.2667 → same | 0.0000 / 20.7227 / 63.2266 | 0 / 0 | +| 6 → 5 | 0.5870 / 21.2445 / 82.3615 → same | 0.0000 / 21.2445 / 82.3615 | 0 / 0 | +| 7 → 6 | 0.3393 / 16.4341 / 47.9760 → same | 0.0000 / 16.4341 / 47.9760 | 0 / 0 | +| 8 → 7 | 0.9616 / 24.9479 / 50.0934 → same | 0.9616 / 24.9479 / 50.0934 | 0 / 0 | + +The baseline gate rejects on the field row and every swap row. The cutoff run returns the same rejection. Fault injections also rejected: missing-surface samples were 4,225–13,642; floating-surface samples 4,225–13,641; missing fine-ring cracks 4,225–8,450. + +| LOD | S13 octave-drop p99 / max px | Stride-2 column interpolation p99 / max px | +|---:|---:|---:| +| 3 | 960.2333 / 998.8654 | 9.3917 / 13.9093 | +| 4 | 3.7405 / 5.1103 | 9.2115 / 15.3435 | +| 5 | 105.7114 / 129.3089 | 125.1435 / 191.6424 | +| 6 | 218.7777 / 749.9535 | 82.1098 / 148.9485 | +| 7 | 95.1867 / 129.1796 | 40.6881 / 80.6012 | +| 8 | 38.1461 / 282.3288 | 28.8473 / 278.6236 | + +Both controls were rejected at every LOD3–8. The octave-drop case produced missing samples at LOD3, 5–8 and floating samples at LOD5–8; stride-2 interpolation produced missing/floating samples at LOD5, 7 and 8. + +## Cutoff contract and detail cost + +The version-2 shared visual-detail contract declares stable octave terms with wavelength, octave count, displacement bound, and noise bound. A suffix is eligible only when its wavelengths are undersampled and its full displacement bound fits within one sixty-fourth of the tile’s pixel budget. Skipped zero-mean terms retain the original normalization denominator. Recipes with no terms keep the exact path; `FloatingIslandFieldV1` declares none, and its baseline/cutoff LOD hashes match. This is separate from content recipe version 1, which stays unchanged because output did not change. + +SurfaceWorld declares 17 terms per LOD. No term or octave tail qualifies at LOD3–8. Sample steps are 16, 32, 64, 128, 256, and 512 voxels respectively. The bounds explain the result: even the smallest declared maximum displacement is 256 voxels, against a 0.2356-voxel 1 px budget at LOD3. The cutoff skips zero octaves, so eligible octave time is 0 ms. + +| Octave | Terms | Wavelength range (voxels) | Max displacement range (voxels) | +|---:|---:|---:|---:| +| 0 | 17 | 25–10,000 | 1,024–640,000 | +| 1 | 17 | 12.5–5,000 | 512–320,000 | +| 2 | 15 | 6.25–1,250 | 256–160,000 | +| 3 | 4 | 41.664–250 | 3,968–24,800 | + +W12 profile from the exact baseline; octave milliseconds are summed across evaluated calls/workers. They are all measured detail work, not removable work under the bound above. + +| LOD | Worker sum s | Octave 0 ms | Octave 1 ms | Octave 2 ms | Octave 3 ms | Detail total ms (% worker sum) | Eligible ms | +|---:|---:|---:|---:|---:|---:|---:|---:| +| 3 | 1.660 | 71.585 | 69.911 | 68.535 | 15.407 | 225.437 (13.58%) | 0 | +| 4 | 3.321 | 135.512 | 131.224 | 128.645 | 26.506 | 421.887 (12.70%) | 0 | +| 5 | 2.283 | 82.131 | 80.450 | 77.545 | 16.262 | 256.388 (11.23%) | 0 | +| 6 | 3.587 | 153.409 | 148.854 | 144.175 | 29.984 | 476.422 (13.28%) | 0 | +| 7 | 1.447 | 35.301 | 34.493 | 33.138 | 7.069 | 110.001 (7.60%) | 0 | +| 8 | 2.685 | 58.763 | 57.751 | 56.277 | 11.848 | 184.638 (6.88%) | 0 | + +The cutoff is recipe-generic, display-mesh-only, and activated only for LOD3+. Exact density, detail, collision, edit, and walkability paths remain outside it. LOD0–2 keep the 2 cm path. ARCHITECTURE §8.10 invariants remain: lazy thread-local caches, two-pass MC, SSE noise, and clipmap streaming. The cutoff state is scoped to mesh generation under the async-safety rules. + +## W12 speed, interleaved with L4 + +Three L4/candidate pairs, all W12, measured on the L1 camera. The candidate is the final build with `--visual-detail-cutoff`. + +| Measure (median) | L4 binary | Candidate | Change | +|---|---:|---:|---:| +| Whole-view ready | 2.183 s | 2.232 s | +2.2% | +| Stream wall | 1.895 s | 1.963 s | +3.6% | +| Far worker sum, LOD3–8 | 12.247 s | 12.458 s | +1.7% | + +| LOD | L4 worker sum s | Candidate worker sum s | +|---:|---:|---:| +| 0 | 3.799 | 4.094 | +| 1 | 5.304 | 5.573 | +| 2 | 1.281 | 1.340 | +| 3 | 1.102 | 1.138 | +| 4 | 2.723 | 2.719 | +| 5 | 1.898 | 1.976 | +| 6 | 2.875 | 2.944 | +| 7 | 1.243 | 1.282 | +| 8 | 2.355 | 2.400 | + +There is no speed benefit because nothing qualifies for skipping; the small regression is cutoff setup and bookkeeping. + +## Hashes and captures + +Baseline and cutoff hashes match for all LOD0–8. The old L4 binary’s aggregate hashes also match: whole view `c10697a8f48380c7 / e75c9119a1498cd5 / 49a9a8d1835a6c8c / edcb23288b5e8724` and full detail `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc`. The recipe stays version 1 because output did not change. + +| LOD | Density | Mesh | Payload | Topology | +|---:|---|---|---|---| +| 0 | 769f2a706d585a72 | 33eeeb39fa443816 | 645621a1d968983f | c2457ed905036c9d | +| 1 | 69cc5a5c0a2c60af | b48335f5410c57d5 | d15c5480162ab8ba | feca1bd025823604 | +| 2 | 85deed5683a77202 | 53469895987aeea2 | e55f8ae7e1532033 | d5feb12a0e85c09b | +| 3 | 6518caefb3be7e7f | ec513c90b853a614 | f62fa1137ff17f82 | ccf1dcf681965816 | +| 4 | 62d4be96021992f4 | f1e4c6efcc2615a9 | 9dc5f9c88be02f33 | fe6f55125a07c3bf | +| 5 | 5e4a8c38b8cea6fc | 08ee56998aaf21e5 | 541eea8558f26cc8 | 6539374a8b3336d8 | +| 6 | e036e95b9006945b | 624a33f941a99b02 | 81b73e14f68bdf86 | 4de4c63ce1f80a56 | +| 7 | 8ee358af56a30d8d | 46ab3c6184048b7c | 0a6164b7e68e80c5 | 0c26b85bb03bc831 | +| 8 | c3458e1cbdb9c471 | 46c03d0994d75192 | 8f89fc5bde12d230 | d16dc0cf62031898 | + +Nine direct-Core query probes also match baseline/cutoff and W1/W12 Clang/GCC: density and unclamped `e4d4a8eb95b35933`, detail `efcba20c0c70c415`, collision `e604823a249029bf`. + +| Before / after pair | View | +|---|---| +| `before_l1.jpg` / `after_l1.jpg` | Exact L1 camera `0,-35,-487.94,0,-22` | +| `before_horizon.jpg` / `after_horizon.jpg` | Same position, pitch −2° | +| `before_boundary_lod2_3.jpg` / `after_boundary_lod2_3.jpg` | 20 m above local heightfield, aimed at the 96 m LOD3 entry band | + +The boundary camera is `0,-35,-480.77246,0,-12`; the local heightfield is at −500.77246 m, placing the ground intersection about 96 m away. + +The six review JPEGs are byte-identical within each pair. PNGs are in `Saved/Linux/captures/`; JPEGs are in `Linux5/captures/`. + +## Failed and retried runs + +- The first headless invocation omitted required streaming flags; the next used the default binary-relative data path and could not open the owner JSON. Both were retried with all flags and the explicit `Standalone/data/owner_world.json` path. +- The first process-monitor wrapper errored after its Clang build had completed; the run was repeated with `MeasureProcess.py` to record process HWM and duration. +- The first capture named a nonexistent mesh path and exited 2; the exported `.vfmesh` path was corrected. +- The first gate build failed on a duplicate `Distances` local; the corrected build passed. A first stride-2 control changed the MC lattice instead of reproducing S13’s every-second-column interpolation; the final control uses bilinear column interpolation. +- Initial comparison against only the continuous field mixed surface interpolation with unrelated tunnel intersections. The final instrument reports exact heightfield roots separately from the exact-mesh regression and ring checks. +- Early W12 cutoff candidates took 2.70–2.91 s. The zero-eligible path and distance setup were moved off the expensive profiling/phase-sweep path; the final interleaved candidate median is 2.232 s. It still does not improve L4. +- One final GCC W12 command omitted `--stream-pipeline` and exited 2; the corrected rerun passed. The initial −50° boundary capture mostly showed near ground; the final pair uses the camera listed above. +- The final baseline and cutoff gate commands exit 6 by design: both correctly reject the exact-field error and swap pops. No process exceeded 30 minutes or stalled for 15 minutes. + +Core/API changes are in `VoxelForgeCore.h`, `WorldAssemblerInternal.h`, `FloatingIslandsInternal.h`, and `VoxelForgeCore.cpp`. Standalone changes are in `StreamingWorld.h`, `StreamBenchmarkPipeline.h`, `StreamBenchmarkHeadless.cpp`, `LinuxScreenSpaceGate.h`, `LinuxOffscreenCapture.cpp`, and `BuildLinux.sh`; process measurements are in `MeasureProcess.py`. + +**For the owner:** Far terrain will look exactly as it does now because no octave tail met the bound. Existing far LOD swaps exceed the 1 px target, so the mesh sampling and transition error need attention before octave skipping can be used. + +## Orchestrator review (Claude) + +- **Not merged into `assembler/world`:** kept on side branch `wip/l5-far-cutoff`. With the cutoff flag OFF, + the L5 build is slower than L4. My interleaved W12 runs: L4 2.054/2.097/2.179 s vs L5 `final7` + 2.127/2.193/2.208 s (medians 2.097 → 2.193, +4.6%), same world. Cost and no benefit. +- **Instrument caveat:** the gate prints `metric=vertical_height_error`. On steep slopes and cliffs, a + small horizontal offset becomes a large vertical error, so vertical error overstates screen + displacement. p50 is ~1 px but p99/max reach 20–126 px, the pattern such an artifact produces. The + claims "today's far meshes exceed 1 px" and "LOD swaps pop by up to 126 px" are therefore NOT yet + established. They need a screen-space measure: error along the surface normal or along camera rays, + projected at the display distance. +- The cutoff criterion (full displacement supremum ≤ 1/64 px) cannot pass for any SurfaceWorld octave + (smallest bound 256 voxels vs 0.24), so the null result was decided by the criterion, not measured. +- The capture path (EGL/GLES pbuffer; llvmpipe, since the sandbox hides `/dev/dri`) works and is worth keeping. diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary.jpg new file mode 100644 index 0000000..52547b8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_final.jpg new file mode 100644 index 0000000..09db1a3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3.jpg new file mode 100644 index 0000000..ccfeb02 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final.jpg new file mode 100644 index 0000000..73323ae Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final2.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final2.jpg new file mode 100644 index 0000000..4f34a6f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_boundary_lod2_3_final2.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon.jpg new file mode 100644 index 0000000..06e1e22 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon_final.jpg new file mode 100644 index 0000000..fea652f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_horizon_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1.jpg new file mode 100644 index 0000000..28922c9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1_final.jpg new file mode 100644 index 0000000..073782d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/after_l1_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary.jpg new file mode 100644 index 0000000..52547b8 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_final.jpg new file mode 100644 index 0000000..09db1a3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3.jpg new file mode 100644 index 0000000..ccfeb02 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final.jpg new file mode 100644 index 0000000..b3aa276 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final2.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final2.jpg new file mode 100644 index 0000000..b70382b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_lod2_3_final2.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_topdown.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_topdown.jpg new file mode 100644 index 0000000..4687520 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_boundary_topdown.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon.jpg new file mode 100644 index 0000000..06e1e22 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon_final.jpg new file mode 100644 index 0000000..fea652f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_horizon_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1.jpg new file mode 100644 index 0000000..28922c9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1_final.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1_final.jpg new file mode 100644 index 0000000..073782d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/before_l1_final.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_after.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_after.jpg new file mode 100644 index 0000000..8b581ec Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_before.jpg b/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_before.jpg new file mode 100644 index 0000000..8b581ec Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux5/captures/surface_boundary_before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux6/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux6/REPORT.md new file mode 100644 index 0000000..1000d52 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux6/REPORT.md @@ -0,0 +1,102 @@ +# Linux6 report + +## Result + +The screen-space gate rejects today’s far meshes at every sampled LOD3–8: the largest measured same-field error is 92.0 px, and the largest sampled swap pop is 70.3 px at LOD6→5. Same-LOD regeneration is exact, the reported swaps have no cracks or floaters, and no display mesh changed in this round. + +Source base: `wip/l5-far-cutoff` at `b21a19d`; L4 reference: `7770074`. + +## Screen-space method + +For one surface tile nearest the L1 camera at each LOD, the gate rasterizes triangles on a 65×65 XY grid, excluding skirts. It compares the mesh hit to direct `Heightfield2D` ground/cap roots; slopes use ±1-voxel finite differences. For vertical residual `Δz` and slopes `sx,sy`, it computes normal distance `|Δz|/sqrt(1+sx²+sy²)`, projects that normal onto the camera’s screen plane, then divides by the one-pixel world distance at that LOD’s closest display distance. The view uses 90° horizontal FOV, 2560 px, yaw 0°, pitch −22°; one pixel is 0.000613592 rad. Same-LOD regeneration is the mesh reference for swaps; direct field roots are the reference for true surface error. + +The known failure control is a 1 m cell on an 89.427° plane, shifted sideways by 5 mm (0.005 cell): its vertical metric says 8.488 px, while its true normal distance is 5 mm and projected screen error is 0.0849 px. This confirms that vertical height can badly overstate visible displacement. + +| LOD | Closest distance (m) | Field error p50 / p99 / max (px) | Max normal distance (m) | +|---:|---:|---:|---:| +| 3 | 96 | 0.81 / 4.16 / 6.00 | 0.423 | +| 4 | 192 | 1.01 / 5.12 / 7.93 | 0.952 | +| 5 | 384 | 0.79 / 16.37 / 92.02 | 25.122 | +| 6 | 768 | 0.51 / 9.75 / 70.34 | 37.644 | +| 7 | 1,536 | 0.29 / 9.04 / 42.36 | 42.714 | +| 8 | 3,072 | 0.83 / 18.03 / 47.18 | 93.175 | + +| Swap | Distance (m) | Pop p50 / p99 / max (px) | Max normal distance (m) | +|---|---:|---:|---:| +| 3→2 | 96 | 0.61 / 3.77 / 6.05 | 0.367 | +| 4→3 | 192 | 0.75 / 4.63 / 7.51 | 0.896 | +| 5→4 | 384 | 0.00 / 9.58 / 31.78 | 8.139 | +| 6→5 | 768 | 0.00 / 9.75 / 70.34 | 37.644 | +| 7→6 | 1,536 | 0.00 / 9.04 / 42.36 | 42.714 | +| 8→7 | 3,072 | 0.83 / 18.03 / 47.18 | 93.175 | + +The tails are not confined to cliffs. The field maximum slopes at LOD5–8 are 8.34°, 14.09°, 6.76°, and 9.33°; LOD5’s p99 is 19.8 px below 30° and 21.1 px at 45–60°. The sampled LOD6→5 maximum is at `(-472, -1432, -463.151) m`; [the marked offscreen capture](captures/lod6_to_5_max_pop.jpg) shows that location at the 768 m swap distance. This is a sampled-tile result, not a whole-world supremum. + +All same-LOD mesh comparisons at LOD3–8 pass with zero error. The S13 octave-drop and corrected stride-2 column-interpolation controls are rejected at every LOD3–8, as are missing displayed surface, 2 m floating surface, and missing fine-ring injections. The measured swap rows have zero cracks and floaters; they reject solely on displacement. + +## Far worker time, W12 + +The L1 view has 2553 tiles. Clean-run phase totals for LOD3–8 are shown below. A separate temporary timer measured complete `FractalNoiseQ16` calls; “column outside” is clean column time minus that subtotal. The timer added 3–6% to worker time, so the split between those two column subphases is approximate; it has been removed from the final tree. + +| LOD | Worker (s) | Column outside noise (ms) | Fractal/noise calls (ms) | MC (ms) | Ground refine (ms) | Cap refine (ms) | Skirts (ms) | Assembly (ms) | +|---:|---:|---:|---:|---:|---:|---:|---:|---:| +| 3 | 1.084 | 171 | 276 | 473 | 0 | 0 | 118 | 33 | +| 4 | 2.581 | 1,000 | 534 | 440 | 0 | 304 | 196 | 102 | +| 5 | 1.829 | 658 | 358 | 100 | 464 | 22 | 161 | 70 | +| 6 | 2.761 | 1,608 | 284 | 59 | 385 | 58 | 162 | 70 | +| 7 | 1.192 | 234 | 198 | 32 | 275 | 202 | 153 | 96 | +| 8 | 2.224 | 264 | 469 | 256 | 424 | 299 | 333 | 177 | +| **Total** | **11.671** | **3,934** | **2,119** | **1,360** | **1,548** | **885** | **1,123** | **548** | + +Column work outside the fractal calls is the largest single phase (33.7% of far worker time); all column work is 51.9%. Ground and cap refinement total 2.43 worker-s, skirts 1.12 worker-s, MC 1.36 worker-s, and assembly 0.55 worker-s. The earlier 7–13% profile timed individual Perlin calls; the L6 subtotal times complete fractal calls, including their octave accumulation. + +The listed subphases total 11.52 worker-s; about 0.15 worker-s remains in timer and per-tile residuals. + +The overlap scan counted about 411,000 cross-worker neighboring duplicate XY evaluations at LOD3–8 (about 166,000 at LOD4 alone). Exact column reuse is the largest plausible lever because it targets the largest phase. I did not build it: no candidate has yet shown a screen-gated, greater-than-3% whole-view win. Stride-2 interpolation is not a safe substitute; its control fails at every far LOD. + +## Cost with the feature off + +Three interleaved W12 runs per binary, same L1 camera and forced generation. `whole_view_ready` is the comparison metric; the six whole-view hashes matched exactly. + +| Binary | Runs (s) | Median | +|---|---|---:| +| L4 reference | 2.082, 2.180, 2.183 | 2.180 s | +| L6 cleaned tree | 2.127, 2.185, 2.223 | 2.185 s (+0.2%) | + +Run-to-run spread was about 0.10 s, so the 5 ms median difference is within noise. The no-op visual cutoff contract, plan, and CLI flag were removed. The opt-in research gate retains its negative controls; skirt timing uses the existing optional stage-timing pointer, which is null on the default path. Governor stayed `powersave`; runs used CPUs 0–11, W12, 6 GiB address-space limits, and normal priority. Peak runtime RSS was 403 MiB; peak build process RSS was 874 MiB (GCC). + +## Determinism and hashes + +Clang 19 `-ffp-contract=off` W1/W12 and GCC default W1/W12 produced the same hashes. LOD0–2, exact queries, and all whole-view hashes equal L4; no recipe bump is needed. + +- Recipe / plan / manifest: `83b616a3f4fe781d` / `1ca84619c8600ee5` / `e55991385bf0ca29` (recipe version 1). +- Whole view (density / mesh / payload / topology): `c10697a8f48380c7` / `e75c9119a1498cd5` / `49a9a8d1835a6c8c` / `edcb23288b5e8724`. +- Full detail (density / mesh / payload / topology): `0ee0579bd15cbbe4` / `32e5d4964d610fee` / `1932b96ee4b1d910` / `69743c6bad3447dc`. +- Exact queries (density / unclamped / detail / collision): `e4d4a8eb95b35933` / `e4d4a8eb95b35933` / `efcba20c0c70c415` / `e604823a249029bf`. +- SHA-256 L4 binary / Clang L6 binary / GCC L6 binary: `6efb8f19a41ca3cd23aca9294ceee1e9cdd1d3dc8da23b3ee61ef8ae943aa763` / `3f5a4f88b0d911e8e66af9ef72e294926a5507a73d44fcfe868df27c666bf01e` / `de872e8fdaa9f40e8a6c096700fd998bcf35f232b06f2b4e35a374b4b0e9fa07`. +- Capture JPEG SHA-256: `483b434f3ad752dd09b82cd1c78fe81ee8cd31803ecf4dcd3c2918a8ae5d66b1` (156 KiB). + +## Failed or retried runs + +- First Clang build failed on a missing view argument in `LinuxScreenSpaceGate.h`; fixed and rebuilt successfully. +- First gate pass paired extra mesh hits incorrectly and reported same-LOD floaters at LOD5–8. The reference pairing was corrected; the final gate reports zero same-LOD displacement and no floaters. +- A `/usr/bin/time -v` wrapper attempt exited 127 because that binary is unavailable; the runs were retried with `/proc` RSS monitoring. +- The final quality gates exit 6 by design because measured displacement exceeds the 1 px rule. Builds, captures, determinism runs, and cost runs completed successfully. EGL could not write `~/.cache` in this environment, disabled shader caching, and rendered successfully on llvmpipe. + +## Note for the owner + +Today’s `assembler/world` remains at L4; this sampled view measures up to 92.0 px from the field and a 70.3 px LOD6→5 pop at the tested display distances, with the largest point marked in the capture. This round changes no default mesh or recipe output, removes the no-op cutoff, and leaves W12 cost within run-to-run noise. + +## Orchestrator review (Claude) + +- Cost re-run by me (interleaved W12, L1 view): L4 2.098/2.138/2.171 s vs L6 2.116/2.150/2.197 s + (median +0.6%, within noise), density `c10697a8f48380c7`; the L6 binary SHA matches the report. + Merged into `assembler/world` with L5 squashed in. +- The marked LOD6→5 maximum sits on the **rim of a flat-topped hill (mesa edge)**, not on open gentle + ground: the "8–14° slope" is the field slope at the sample, on the plateau top beside the drop. The + coarse mesh rounds the rim by about a cell (16–32 m at LOD5–6), so the pop is a real **silhouette snap + at sharp rims** (tens of px at 768 m), while p50 stays at 0 px. That is ordinary LOD-switch behaviour + without geomorphing or cross-fade, so a fix belongs in rendering (Unreal), not in generation. The + owner decides whether it is visible enough to matter. +- Next speed lever, per the far breakdown: exact cross-tile column reuse (column work outside noise is + 34% of far worker time; ~411k duplicate XY evaluations at LOD3–8). diff --git a/Standalone/Research/WorldAssemblerM3/Linux6/captures/lod6_to_5_max_pop.jpg b/Standalone/Research/WorldAssemblerM3/Linux6/captures/lod6_to_5_max_pop.jpg new file mode 100644 index 0000000..6c16c9f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux6/captures/lod6_to_5_max_pop.jpg differ diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp new file mode 100644 index 0000000..95757cc --- /dev/null +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -0,0 +1,397 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + struct FVertex + { + float Position[3]; + float Normal[3]; + float Height; + }; + + struct FScene + { + std::uint32_t VoxelSizeMicrons=0; + std::array CameraMeters{}; + double YawDegrees=0.0,PitchDegrees=0.0; + std::vector Vertices; + std::vector Indices; + std::uint64_t Triangles=0,SkirtTriangles=0; + std::uint32_t TileCount=0; + }; + + template bool Read(std::ifstream& File,T& Value) + { + File.read(reinterpret_cast(&Value),sizeof(Value)); + return static_cast(File); + } + + bool LoadScene(const std::filesystem::path& Path,FScene& Scene,std::string& Error) + { + std::ifstream File(Path,std::ios::binary); + if(!File){Error="cannot open mesh scene";return false;} + char Magic[8]{}; + File.read(Magic,sizeof(Magic)); + if(!File||std::memcmp(Magic,"VFMSH001",8)!=0) + {Error="mesh scene magic is invalid";return false;} + std::uint32_t Version=0,ExpectedTiles=0; + if(!Read(File,Version)||!Read(File,Scene.VoxelSizeMicrons)||!Read(File,ExpectedTiles) + ||Version!=1||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000) + {Error="mesh scene header is malformed";return false;} + for(double& Value:Scene.CameraMeters)if(!Read(File,Value)) + {Error="mesh scene camera is truncated";return false;} + if(!Read(File,Scene.YawDegrees)||!Read(File,Scene.PitchDegrees)) + {Error="mesh scene camera orientation is truncated";return false;} + Scene.TileCount=ExpectedTiles; + std::uint64_t TotalVertices=0,TotalIndices=0; + for(std::uint32_t Tile=0;Tile10000000||IndexCount>60000000||MaskCount>20000000 + ||IndexCount%3!=0||(MaskCount!=0&&MaskCount!=IndexCount/3)) + {Error="tile mesh header is malformed";return false;} + if(TotalVertices+VertexCount>std::numeric_limits::max() + ||TotalIndices+IndexCount>std::numeric_limits::max()) + {Error="mesh scene exceeds supported index space";return false;} + const std::size_t Base=Scene.Vertices.size(); + Scene.Vertices.resize(Base+VertexCount); + Scene.Indices.reserve(Scene.Indices.size()+IndexCount); + for(std::size_t I=Base;I(Scene.Indices.data()+IndexOffset), + static_cast(sizeof(std::uint32_t)*IndexCount)); + std::vector Masks(MaskCount); + if(MaskCount)File.read(reinterpret_cast(Masks.data()),MaskCount); + if(!File){Error="tile index or skirt data is truncated";return false;} + for(std::size_t Index=IndexOffset;Index=VertexCount) + {Error="tile index points outside its vertex array";return false;} + Scene.Indices[Index]+=static_cast(Base); + } + Scene.Triangles+=IndexCount/3; + Scene.SkirtTriangles+=SkirtCount; + TotalVertices+=VertexCount;TotalIndices+=IndexCount; + } + if(File.peek()!=std::char_traits::eof()) + {Error="mesh scene has trailing bytes";return false;} + if(Scene.Indices.empty()){Error="mesh scene contains no triangles";return false;} + return true; + } + + EGLDisplay MakeDisplay() + { + using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*); + auto GetPlatformDisplay=reinterpret_cast( + eglGetProcAddress("eglGetPlatformDisplayEXT")); +#ifdef EGL_PLATFORM_SURFACELESS_MESA + if(GetPlatformDisplay) + { + const EGLDisplay Display=GetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, + EGL_DEFAULT_DISPLAY,nullptr); + if(Display!=EGL_NO_DISPLAY)return Display; + } +#endif + return eglGetDisplay(EGL_DEFAULT_DISPLAY); + } + + GLuint CompileShader(GLenum Type,const char* Source) + { + const GLuint Shader=glCreateShader(Type); + glShaderSource(Shader,1,&Source,nullptr); + glCompileShader(Shader); + GLint Success=GL_FALSE; + glGetShaderiv(Shader,GL_COMPILE_STATUS,&Success); + if(Success==GL_FALSE) + { + GLint Length=0;glGetShaderiv(Shader,GL_INFO_LOG_LENGTH,&Length); + std::string Log(static_cast(std::max(Length,1)),0); + glGetShaderInfoLog(Shader,Length,nullptr,Log.data()); + std::fprintf(stderr,"[OffscreenGL] shader compile failed: %s\n",Log.c_str()); + glDeleteShader(Shader);return 0; + } + return Shader; + } + + GLuint MakeProgram() + { + static constexpr char VertexSource[]=R"GLSL(#version 300 es +precision highp float; +layout(location=0) in vec3 inCameraPosition; +layout(location=1) in vec3 inNormal; +layout(location=2) in float inHeight; +out vec3 vNormal; +out float vHeight; +uniform float uTanHalfHorizontal; +uniform float uTanHalfVertical; +uniform float uNear; +uniform float uFar; +void main() +{ + float z=max(inCameraPosition.z,0.0001); + float clipZ=((uFar+uNear)/(uFar-uNear))*z-(2.0*uFar*uNear)/(uFar-uNear); + gl_Position=vec4(inCameraPosition.x/uTanHalfHorizontal, + inCameraPosition.y/uTanHalfVertical,clipZ,z); + vNormal=normalize(inNormal); + vHeight=inHeight; +})GLSL"; + static constexpr char FragmentSource[]=R"GLSL(#version 300 es +precision highp float; +in vec3 vNormal; +in float vHeight; +out vec4 outColor; +void main() +{ + vec3 n=normalize(vNormal); + vec3 normalColor=abs(n)*0.78+vec3(0.12); + float band=mod(floor((vHeight+10000.0)/80.0),2.0); + vec3 bandColor=mix(vec3(0.24,0.31,0.37),vec3(0.39,0.42,0.35),band); + outColor=vec4(mix(bandColor,normalColor,0.68),1.0); +})GLSL"; + const GLuint VS=CompileShader(GL_VERTEX_SHADER,VertexSource); + const GLuint FS=CompileShader(GL_FRAGMENT_SHADER,FragmentSource); + if(!VS||!FS)return 0; + const GLuint Program=glCreateProgram(); + glAttachShader(Program,VS);glAttachShader(Program,FS);glLinkProgram(Program); + glDeleteShader(VS);glDeleteShader(FS); + GLint Success=GL_FALSE;glGetProgramiv(Program,GL_LINK_STATUS,&Success); + if(Success==GL_FALSE) + { + GLint Length=0;glGetProgramiv(Program,GL_INFO_LOG_LENGTH,&Length); + std::string Log(static_cast(std::max(Length,1)),0); + glGetProgramInfoLog(Program,Length,nullptr,Log.data()); + std::fprintf(stderr,"[OffscreenGL] program link failed: %s\n",Log.c_str()); + glDeleteProgram(Program);return 0; + } + return Program; + } + + bool WritePng(const std::filesystem::path& Path,int Width,int Height, + const std::vector& Pixels) + { + std::error_code Error; + if(!Path.parent_path().empty())std::filesystem::create_directories(Path.parent_path(),Error); + if(Error)return false; + FILE* File=std::fopen(Path.c_str(),"wb"); + if(!File)return false; + png_structp Png=png_create_write_struct(PNG_LIBPNG_VER_STRING,nullptr,nullptr,nullptr); + png_infop Info=Png?png_create_info_struct(Png):nullptr; + if(!Png||!Info){if(Png)png_destroy_write_struct(&Png,nullptr);std::fclose(File);return false;} + if(setjmp(png_jmpbuf(Png))) + {png_destroy_write_struct(&Png,&Info);std::fclose(File);return false;} + png_init_io(Png,File); + png_set_IHDR(Png,Info,static_cast(Width),static_cast(Height), + 8,PNG_COLOR_TYPE_RGBA,PNG_INTERLACE_NONE,PNG_COMPRESSION_TYPE_DEFAULT,PNG_FILTER_TYPE_DEFAULT); + png_write_info(Png,Info); + const std::size_t Stride=static_cast(Width)*4; + for(int Y=Height-1;Y>=0;--Y) + png_write_row(Png,const_cast(Pixels.data()+static_cast(Y)*Stride)); + png_write_end(Png,nullptr); + png_destroy_write_struct(&Png,&Info); + const bool Good=std::fclose(File)==0; + return Good; + } + + bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height, + double HorizontalFovDegrees) + { + EGLDisplay Display=MakeDisplay(); + EGLint Major=0,Minor=0; + if(Display==EGL_NO_DISPLAY||eglInitialize(Display,&Major,&Minor)!=EGL_TRUE) + {std::fprintf(stderr,"[OffscreenEGL] eglInitialize failed (0x%x)\n",eglGetError());return false;} + if(eglBindAPI(EGL_OPENGL_ES_API)!=EGL_TRUE) + {std::fprintf(stderr,"[OffscreenEGL] eglBindAPI failed (0x%x)\n",eglGetError());eglTerminate(Display);return false;} + const EGLint ConfigAttributes[]={EGL_SURFACE_TYPE,EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE,EGL_OPENGL_ES3_BIT,EGL_RED_SIZE,8,EGL_GREEN_SIZE,8, + EGL_BLUE_SIZE,8,EGL_ALPHA_SIZE,8,EGL_DEPTH_SIZE,24,EGL_NONE}; + EGLConfig Config{};EGLint ConfigCount=0; + if(eglChooseConfig(Display,ConfigAttributes,&Config,1,&ConfigCount)!=EGL_TRUE||ConfigCount==0) + {std::fprintf(stderr,"[OffscreenEGL] no GLES3 pbuffer config (0x%x)\n",eglGetError());eglTerminate(Display);return false;} + const EGLint SurfaceAttributes[]={EGL_WIDTH,Width,EGL_HEIGHT,Height,EGL_NONE}; + EGLSurface Surface=eglCreatePbufferSurface(Display,Config,SurfaceAttributes); + const EGLint ContextAttributes[]={EGL_CONTEXT_CLIENT_VERSION,3,EGL_NONE}; + EGLContext Context=eglCreateContext(Display,Config,EGL_NO_CONTEXT,ContextAttributes); + if(Surface==EGL_NO_SURFACE||Context==EGL_NO_CONTEXT + ||eglMakeCurrent(Display,Surface,Surface,Context)!=EGL_TRUE) + {std::fprintf(stderr,"[OffscreenEGL] context/surface creation failed (0x%x)\n",eglGetError()); + if(Context!=EGL_NO_CONTEXT)eglDestroyContext(Display,Context); + if(Surface!=EGL_NO_SURFACE)eglDestroySurface(Display,Surface); + eglTerminate(Display);return false;} + + const GLubyte* Renderer=glGetString(GL_RENDERER); + const GLubyte* GlVersion=glGetString(GL_VERSION); + std::printf("[OffscreenEGL] renderer=%s version=%s pbuffer=%dx%d\n", + Renderer?reinterpret_cast(Renderer):"unknown", + GlVersion?reinterpret_cast(GlVersion):"unknown",Width,Height); + GLuint ColorTexture=0,DepthBuffer=0,Framebuffer=0; + glGenTextures(1,&ColorTexture);glBindTexture(GL_TEXTURE_2D,ColorTexture); + glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA8,Width,Height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); + glGenRenderbuffers(1,&DepthBuffer);glBindRenderbuffer(GL_RENDERBUFFER,DepthBuffer); + glRenderbufferStorage(GL_RENDERBUFFER,GL_DEPTH_COMPONENT24,Width,Height); + glGenFramebuffers(1,&Framebuffer);glBindFramebuffer(GL_FRAMEBUFFER,Framebuffer); + glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,ColorTexture,0); + glFramebufferRenderbuffer(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_RENDERBUFFER,DepthBuffer); + if(glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE) + {std::fprintf(stderr,"[OffscreenGL] framebuffer is incomplete\n");return false;} + + constexpr double Pi=3.14159265358979323846; + const double Yaw=Scene.YawDegrees*Pi/180.0,Pitch=Scene.PitchDegrees*Pi/180.0; + const std::array Forward={std::sin(Yaw)*std::cos(Pitch), + std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)}; + const std::array Right={std::cos(Yaw),-std::sin(Yaw),0.0}; + const std::array Up={Right[1]*Forward[2]-Right[2]*Forward[1], + Right[2]*Forward[0]-Right[0]*Forward[2],Right[0]*Forward[1]-Right[1]*Forward[0]}; + const double TanHalfHorizontal=std::tan(HorizontalFovDegrees*Pi/360.0); + const double Aspect=static_cast(Width)/Height; + const double TanHalfVertical=TanHalfHorizontal/Aspect; + const double Near=0.1,Far=20000.0; + for(FVertex& Vertex:Scene.Vertices) + { + const double DX=Vertex.Position[0]-Scene.CameraMeters[0]; + const double DY=Vertex.Position[1]-Scene.CameraMeters[1]; + const double DZ=Vertex.Position[2]-Scene.CameraMeters[2]; + const float CameraX=static_cast(DX*Right[0]+DY*Right[1]+DZ*Right[2]); + const float CameraY=static_cast(DX*Up[0]+DY*Up[1]+DZ*Up[2]); + const float CameraZ=static_cast(DX*Forward[0]+DY*Forward[1]+DZ*Forward[2]); + Vertex.Position[0]=CameraX;Vertex.Position[1]=CameraY;Vertex.Position[2]=CameraZ; + } + const GLuint Program=MakeProgram(); + if(!Program)return false; + GLuint Vao=0,Vbo=0,Ebo=0; + glGenVertexArrays(1,&Vao);glBindVertexArray(Vao); + glGenBuffers(1,&Vbo);glBindBuffer(GL_ARRAY_BUFFER,Vbo); + glBufferData(GL_ARRAY_BUFFER,static_cast(Scene.Vertices.size()*sizeof(FVertex)), + Scene.Vertices.data(),GL_STATIC_DRAW); + glGenBuffers(1,&Ebo);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,Ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER,static_cast(Scene.Indices.size()*sizeof(std::uint32_t)), + Scene.Indices.data(),GL_STATIC_DRAW); + glEnableVertexAttribArray(0);glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,sizeof(FVertex), + reinterpret_cast(offsetof(FVertex,Position))); + glEnableVertexAttribArray(1);glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,sizeof(FVertex), + reinterpret_cast(offsetof(FVertex,Normal))); + glEnableVertexAttribArray(2);glVertexAttribPointer(2,1,GL_FLOAT,GL_FALSE,sizeof(FVertex), + reinterpret_cast(offsetof(FVertex,Height))); + glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f); + glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); + glUseProgram(Program); + glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast(TanHalfHorizontal)); + glUniform1f(glGetUniformLocation(Program,"uTanHalfVertical"),static_cast(TanHalfVertical)); + glUniform1f(glGetUniformLocation(Program,"uNear"),static_cast(Near)); + glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast(Far)); + glDrawElements(GL_TRIANGLES,static_cast(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr); + glFinish(); + std::vector Pixels(static_cast(Width)*Height*4); + glReadPixels(0,0,Width,Height,GL_RGBA,GL_UNSIGNED_BYTE,Pixels.data()); + const GLenum GlError=glGetError(); + const bool PngGood=GlError==GL_NO_ERROR&&WritePng(Output,Width,Height,Pixels); + if(!PngGood)std::fprintf(stderr,"[OffscreenGL] render or PNG write failed (GL=0x%x)\n",GlError); + else + { + std::error_code SizeError; + const auto Bytes=std::filesystem::file_size(Output,SizeError); + std::printf("[OffscreenCapture] triangles=%llu tiles=%u skirts=%llu png_bytes=%llu status=PASS path=%s\n", + static_cast(Scene.Triangles),Scene.TileCount, + static_cast(Scene.SkirtTriangles), + static_cast(SizeError?0:Bytes),Output.string().c_str()); + } + glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo); + glDeleteVertexArrays(1,&Vao);glDeleteFramebuffers(1,&Framebuffer); + glDeleteRenderbuffers(1,&DepthBuffer);glDeleteTextures(1,&ColorTexture); + eglMakeCurrent(Display,EGL_NO_SURFACE,EGL_NO_SURFACE,EGL_NO_CONTEXT); + eglDestroyContext(Display,Context);eglDestroySurface(Display,Surface);eglTerminate(Display); + return PngGood; + } +} + +int main(int argc,char** argv) +{ + if(argc<3) + { + std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N]\n",argv[0]); + return 2; + } + int Width=2560,Height=1440;double Fov=90.0; + bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0; + bool HasCamera=false;std::array Camera{}; + for(int I=3;I(Arg.size()),Arg.data());return 2;} + } + FScene Scene;std::string Error; + if(!LoadScene(argv[1],Scene,Error)) + {std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;} + if(HasCamera)Scene.CameraMeters=Camera; + if(HasYaw)Scene.YawDegrees=Yaw; + if(HasPitch)Scene.PitchDegrees=Pitch; + std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f vertices=%zu triangles=%llu\n", + 1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2], + Scene.YawDegrees,Scene.PitchDegrees,Scene.Vertices.size(), + static_cast(Scene.Triangles)); + return Render(Scene,argv[2],Width,Height,Fov)?0:3; +} + diff --git a/Standalone/Source/LinuxScreenSpaceGate.h b/Standalone/Source/LinuxScreenSpaceGate.h new file mode 100644 index 0000000..10c56b0 --- /dev/null +++ b/Standalone/Source/LinuxScreenSpaceGate.h @@ -0,0 +1,721 @@ +#pragma once + +#include "StreamingWorld.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace StandaloneScreenSpaceGate +{ + inline constexpr double OnePixelRadians=1.57079632679489661923/2560.0; + + struct FErrorRow + { + int Lod=0; + std::uint64_t Samples=0,Missing=0,Floating=0; + double P50=0.0,P99=0.0,Maximum=0.0; + double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0; + double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0; + double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0; + double MaximumSlopeDegrees=0.0; + std::array SlopeSamples{}; + std::array SlopeP99{},SlopeMaximum{}; + bool Valid=false; + }; + + struct FPopRow + { + int CoarseLod=0; + std::uint64_t Samples=0,Cracks=0,Floating=0; + double P50=0.0,P99=0.0,Maximum=0.0; + double VerticalP50=0.0,VerticalP99=0.0,VerticalMaximum=0.0; + double MaximumNormalDistanceMeters=0.0,MaximumX=0.0,MaximumY=0.0; + double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0; + double MaximumSlopeDegrees=0.0; + std::array SlopeSamples{}; + std::array SlopeP99{},SlopeMaximum{}; + bool Valid=false; + }; + + struct FSurfaceRoot + { + double ZMeters=0.0; + double SlopeX=0.0; + double SlopeY=0.0; + }; + + struct FViewBasis + { + std::array Right{}; + std::array Up{}; + }; + + struct FErrorAccumulator + { + std::vector Pixels,VerticalPixels; + std::array,4> BySlope; + double Maximum=-1.0,MaximumVertical=0.0,MaximumNormalDistanceMeters=0.0; + double MaximumX=0.0,MaximumY=0.0,MaximumSlopeDegrees=0.0; + double MaximumReferenceZ=0.0,MaximumDisplayedZ=0.0; + std::array SlopeSamples{}; + }; + + inline double Percentile(std::vector& Values,double P); + + inline FViewBasis MakeViewBasis(double YawDegrees,double PitchDegrees) + { + constexpr double Pi=3.14159265358979323846; + const double Yaw=YawDegrees*Pi/180.0,Pitch=PitchDegrees*Pi/180.0; + const std::array Forward={std::sin(Yaw)*std::cos(Pitch), + std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)}; + FViewBasis Basis; + Basis.Right={std::cos(Yaw),-std::sin(Yaw),0.0}; + Basis.Up={Basis.Right[1]*Forward[2]-Basis.Right[2]*Forward[1], + Basis.Right[2]*Forward[0]-Basis.Right[0]*Forward[2], + Basis.Right[0]*Forward[1]-Basis.Right[1]*Forward[0]}; + return Basis; + } + + inline std::size_t SlopeBin(double Degrees) + { + if(Degrees<30.0)return 0; + if(Degrees<45.0)return 1; + if(Degrees<60.0)return 2; + return 3; + } + + inline void AddError(FErrorAccumulator& Accumulator,double DeltaZMeters, + const FSurfaceRoot& Root,double PixelBudgetMeters,const FViewBasis& View, + double X,double Y) + { + constexpr double Pi=3.14159265358979323846; + const double GradientSquared=Root.SlopeX*Root.SlopeX+Root.SlopeY*Root.SlopeY; + const double NormalLength=std::sqrt(1.0+GradientSquared); + const double NormalX=-Root.SlopeX/NormalLength; + const double NormalY=-Root.SlopeY/NormalLength; + const double NormalZ=1.0/NormalLength; + const double NormalDistance=std::abs(DeltaZMeters)/NormalLength; + const double ProjectedNormal=std::hypot( + NormalX*View.Right[0]+NormalY*View.Right[1]+NormalZ*View.Right[2], + NormalX*View.Up[0]+NormalY*View.Up[1]+NormalZ*View.Up[2]); + const double ProjectedVertical=std::hypot(View.Right[2],View.Up[2]); + const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters; + const double VerticalPixels=std::abs(DeltaZMeters)*ProjectedVertical/PixelBudgetMeters; + const double SlopeDegrees=std::atan(std::sqrt(GradientSquared))*180.0/Pi; + Accumulator.Pixels.push_back(NormalPixels); + Accumulator.VerticalPixels.push_back(VerticalPixels); + const std::size_t Bin=SlopeBin(SlopeDegrees); + Accumulator.BySlope[Bin].push_back(NormalPixels); + ++Accumulator.SlopeSamples[Bin]; + if(NormalPixels>Accumulator.Maximum) + { + Accumulator.Maximum=NormalPixels; + Accumulator.MaximumVertical=VerticalPixels; + Accumulator.MaximumNormalDistanceMeters=NormalDistance; + Accumulator.MaximumX=X;Accumulator.MaximumY=Y; + Accumulator.MaximumSlopeDegrees=SlopeDegrees; + Accumulator.MaximumReferenceZ=Root.ZMeters; + Accumulator.MaximumDisplayedZ=Root.ZMeters+DeltaZMeters; + } + } + + inline void AddMissing(FErrorAccumulator& Accumulator) + { + Accumulator.Pixels.push_back(2.0); + Accumulator.VerticalPixels.push_back(2.0); + } + + inline void FinalizeError(FErrorRow& Row,FErrorAccumulator& Accumulator) + { + Row.Samples=Accumulator.Pixels.size(); + Row.P50=Percentile(Accumulator.Pixels,0.50); + Row.P99=Percentile(Accumulator.Pixels,0.99); + Row.Maximum=Accumulator.Pixels.empty()?0.0 + :*std::max_element(Accumulator.Pixels.begin(),Accumulator.Pixels.end()); + Row.VerticalP50=Percentile(Accumulator.VerticalPixels,0.50); + Row.VerticalP99=Percentile(Accumulator.VerticalPixels,0.99); + Row.VerticalMaximum=Accumulator.VerticalPixels.empty()?0.0 + :*std::max_element(Accumulator.VerticalPixels.begin(),Accumulator.VerticalPixels.end()); + Row.MaximumNormalDistanceMeters=Accumulator.MaximumNormalDistanceMeters; + Row.MaximumX=Accumulator.MaximumX;Row.MaximumY=Accumulator.MaximumY; + Row.MaximumReferenceZ=Accumulator.MaximumReferenceZ; + Row.MaximumDisplayedZ=Accumulator.MaximumDisplayedZ; + Row.MaximumSlopeDegrees=Accumulator.MaximumSlopeDegrees; + Row.SlopeSamples=Accumulator.SlopeSamples; + for(std::size_t Bin=0;Bin> Hits; + + FHeightGrid()=default; + FHeightGrid(int InSide,double MinX,double MaxX,double MinY,double MaxY) + :Side(InSide),MinimumX(MinX),MaximumX(MaxX),MinimumY(MinY),MaximumY(MaxY), + Hits(static_cast(InSide)*InSide) + {} + + double X(int Ix) const + {return MinimumX+(MaximumX-MinimumX)*Ix/(Side-1);} + double Y(int Iy) const + {return MinimumY+(MaximumY-MinimumY)*Iy/(Side-1);} + + void Add(int Ix,int Iy,double Z) + { + auto& Values=Hits[static_cast(Iy)*Side+Ix]; + Values.push_back(Z); + } + + const std::vector& At(int Ix,int Iy) const + {return Hits[static_cast(Iy)*Side+Ix];} + }; + + inline double Percentile(std::vector& Values,double P) + { + if(Values.empty())return 0.0; + std::sort(Values.begin(),Values.end()); + const std::size_t Index=std::min(Values.size()-1, + static_cast(std::ceil(P*Values.size())-1)); + return Values[Index]; + } + + inline double TileAabbDistanceMeters(const FVoxelTileKey& Key, + const FVector& CameraVoxels,double VoxelMeters) + { + const double Extent=static_cast(Key.ExtentVoxels()); + const double Min[3]={static_cast(Key.OriginVoxels().X), + static_cast(Key.OriginVoxels().Y),static_cast(Key.OriginVoxels().Z)}; + const double Cam[3]={CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z}; + double Squared=0.0; + for(int Axis=0;Axis<3;++Axis) + { + const double Delta=Cam[Axis]Min[Axis]+Extent?Cam[Axis]-(Min[Axis]+Extent):0.0; + Squared+=Delta*Delta; + } + return std::sqrt(Squared)*VoxelMeters; + } + + inline bool TileHasNativeSurface(const FVoxelTileKey& Key, + const VoxelForgeCore::WorldAssemblyManifest& Assembly, + double VoxelMeters) + { + const double Extent=static_cast(Key.ExtentVoxels()); + const double X=(static_cast(Key.OriginVoxels().X)+Extent*0.5)*256.0; + const double Y=(static_cast(Key.OriginVoxels().Y)+Extent*0.5)*256.0; + const double MinZ=static_cast(Key.OriginVoxels().Z); + const double MaxZ=MinZ+Extent; + for(const auto& Plan:Assembly.plan.strates) + { + VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan); + if(!Field.isValid())continue; + const auto Column=Field.point(static_cast(std::llround(X)), + static_cast(std::llround(Y)),0); + const double Ground=static_cast(Column.groundHeightSubvoxels)/256.0; + const double Cap=static_cast(Column.capHeightSubvoxels)/256.0; + const double Margin=std::max(2.0,0.25/VoxelMeters); + if(Ground=MinZ-Margin&&Ground<=MaxZ+Margin) + ||(Cap>=MinZ-Margin&&Cap<=MaxZ+Margin)))return true; + } + return false; + } + + inline FHeightGrid Rasterize(std::span Tiles, + const FVoxelTileKey& Patch,double VoxelMeters,int Side=65) + { + const double Extent=static_cast(Patch.ExtentVoxels())*VoxelMeters; + const double MinX=static_cast(Patch.OriginVoxels().X)*VoxelMeters; + const double MinY=static_cast(Patch.OriginVoxels().Y)*VoxelMeters; + FHeightGrid Grid(Side,MinX,MinX+Extent,MinY,MinY+Extent); + for(const FStandaloneCpuTile* Tile:Tiles) + { + if(!Tile||Tile->Indices.size()<3)continue; + const std::size_t TriangleCount=Tile->Indices.size()/3; + for(std::size_t Triangle=0;TriangleTriangleSkirtFaceMasks.size() + &&Tile->TriangleSkirtFaceMasks[Triangle]!=0)continue; + const std::uint32_t I0=Tile->Indices[Triangle*3]; + const std::uint32_t I1=Tile->Indices[Triangle*3+1]; + const std::uint32_t I2=Tile->Indices[Triangle*3+2]; + if(I0>=Tile->Vertices.size()||I1>=Tile->Vertices.size()||I2>=Tile->Vertices.size())continue; + const auto& A=Tile->Vertices[I0].Position; + const auto& B=Tile->Vertices[I1].Position; + const auto& C=Tile->Vertices[I2].Position; + const double Denominator=(B[1]-C[1])*(A[0]-C[0])+(C[0]-B[0])*(A[1]-C[1]); + if(std::abs(Denominator)<1e-10)continue; + const double MinTriX=std::min({A[0],B[0],C[0]}),MaxTriX=std::max({A[0],B[0],C[0]}); + const double MinTriY=std::min({A[1],B[1],C[1]}),MaxTriY=std::max({A[1],B[1],C[1]}); + const double StepX=Extent/(Side-1),StepY=Extent/(Side-1); + const int Ix0=std::clamp(static_cast(std::ceil((MinTriX-MinX)/StepX)),0,Side-1); + const int Ix1=std::clamp(static_cast(std::floor((MaxTriX-MinX)/StepX)),0,Side-1); + const int Iy0=std::clamp(static_cast(std::ceil((MinTriY-MinY)/StepY)),0,Side-1); + const int Iy1=std::clamp(static_cast(std::floor((MaxTriY-MinY)/StepY)),0,Side-1); + for(int Iy=Iy0;Iy<=Iy1;++Iy) + for(int Ix=Ix0;Ix<=Ix1;++Ix) + { + const double X=Grid.X(Ix),Y=Grid.Y(Iy); + const double W0=((B[1]-C[1])*(X-C[0])+(C[0]-B[0])*(Y-C[1]))/Denominator; + const double W1=((C[1]-A[1])*(X-C[0])+(A[0]-C[0])*(Y-C[1]))/Denominator; + const double W2=1.0-W0-W1; + if(W0<-1e-6||W1<-1e-6||W2<-1e-6)continue; + Grid.Add(Ix,Iy,W0*A[2]+W1*B[2]+W2*C[2]); + } + } + } + for(auto& Values:Grid.Hits) + { + std::sort(Values.begin(),Values.end()); + Values.erase(std::unique(Values.begin(),Values.end(),[](double A,double B) + {return std::abs(A-B)<0.02;}),Values.end()); + } + return Grid; + } + + inline std::vector ExactHeightRoots(const FHeightGrid& Grid,int Ix,int Iy, + const FVoxelTileKey& Patch,double VoxelMeters, + const VoxelForgeCore::WorldAssemblyManifest& Assembly) + { + std::vector Roots; + const double XSubvoxels=Grid.X(Ix)/VoxelMeters*256.0; + const double YSubvoxels=Grid.Y(Iy)/VoxelMeters*256.0; + const auto X=static_cast(std::llround(XSubvoxels)); + const auto Y=static_cast(std::llround(YSubvoxels)); + const double MinZ=static_cast(Patch.OriginVoxels().Z); + const double MaxZ=MinZ+Patch.ExtentVoxels(); + constexpr std::int64_t NormalProbeSubvoxels=256; + for(const auto& Plan:Assembly.plan.strates) + { + VoxelForgeCore::Heightfield2D Field(Assembly.recipe,Plan); + if(!Field.isValid())continue; + const auto Column=Field.point(X,Y,0); + if(Column.groundHeightSubvoxels>=Column.capHeightSubvoxels)continue; + const auto XMinus=Field.point(X-NormalProbeSubvoxels,Y,0); + const auto XPlus=Field.point(X+NormalProbeSubvoxels,Y,0); + const auto YMinus=Field.point(X,Y-NormalProbeSubvoxels,0); + const auto YPlus=Field.point(X,Y+NormalProbeSubvoxels,0); + const auto Slope=[&](std::int64_t Minus,std::int64_t Plus) + {return static_cast(Plus-Minus)/(2.0*NormalProbeSubvoxels);}; + const double Ground=static_cast(Column.groundHeightSubvoxels)/256.0*VoxelMeters; + const double Cap=static_cast(Column.capHeightSubvoxels)/256.0*VoxelMeters; + const double PlanMinZ=static_cast(Plan.bottomChunkZ)*32.0*VoxelMeters; + const double PlanMaxZ=static_cast(Plan.topChunkZ+1)*32.0*VoxelMeters; + if(Ground>=MinZ*VoxelMeters-0.25*VoxelMeters + &&Ground<=MaxZ*VoxelMeters+0.25*VoxelMeters + &&Ground>=PlanMinZ-0.25*VoxelMeters&&Ground<=PlanMaxZ+0.25*VoxelMeters) + Roots.push_back({Ground,Slope(XMinus.groundHeightSubvoxels, + XPlus.groundHeightSubvoxels),Slope(YMinus.groundHeightSubvoxels, + YPlus.groundHeightSubvoxels)}); + if(Cap>=MinZ*VoxelMeters-0.25*VoxelMeters + &&Cap<=MaxZ*VoxelMeters+0.25*VoxelMeters + &&Cap>=PlanMinZ-0.25*VoxelMeters&&Cap<=PlanMaxZ+0.25*VoxelMeters) + Roots.push_back({Cap,Slope(XMinus.capHeightSubvoxels, + XPlus.capHeightSubvoxels),Slope(YMinus.capHeightSubvoxels, + YPlus.capHeightSubvoxels)}); + } + std::sort(Roots.begin(),Roots.end(),[](const FSurfaceRoot& A,const FSurfaceRoot& B) + {return A.ZMeters& Hits,const std::vector& Used, + double Root,std::size_t& OutIndex,double& OutError) + { + double Best=std::numeric_limits::infinity();std::size_t BestIndex=Hits.size(); + for(std::size_t I=0;I=Used.size()||!Used[I]) + { + const double Error=std::abs(Hits[I]-Root); + if(Error Used(MeshGrid.At(Ix,Iy).size(),false); + for(const FSurfaceRoot& Root:Roots) + { + std::size_t HitIndex=0;double ErrorMeters=0.0; + if(!MatchNearest(MeshGrid.At(Ix,Iy),Used,Root.ZMeters,HitIndex,ErrorMeters)) + { + ++Row.Missing; + AddMissing(Accumulator); + continue; + } + Used[HitIndex]=true; + AddError(Accumulator,MeshGrid.At(Ix,Iy)[HitIndex]-Root.ZMeters, + Root,PixelBudgetMeters,View,MeshGrid.X(Ix),MeshGrid.Y(Iy)); + ++Row.Samples; + } + // Heightfield2D declares ground and cap roots only. Other mesh intersections may + // be valid tunnel geometry; the exact-mesh comparison checks extra pieces. + } + FinalizeError(Row,Accumulator); + return Row; + } + + inline FErrorRow CompareToReference(const FHeightGrid& MeshGrid, + const FHeightGrid& ReferenceGrid,const FVoxelTileKey& Patch,double VoxelMeters, + double PixelBudgetMeters,const VoxelForgeCore::WorldAssemblyManifest& Assembly, + const FViewBasis& View) + { + FErrorRow Row;Row.Lod=Patch.Level;Row.Valid=true; + FErrorAccumulator Accumulator; + for(int Iy=0;Iy UsedReference(Reference.size(),false),UsedMesh(Mesh.size(),false); + for(std::size_t ReferenceIndex=0;ReferenceIndexZMeters-ReferenceZ)<1.0) + Root=*BestRoot; + } + AddError(Accumulator,Mesh[MeshIndex]-Reference[ReferenceIndex], + Root,PixelBudgetMeters,View,ReferenceGrid.X(Ix),ReferenceGrid.Y(Iy)); + } + for(std::size_t I=0;I CoarseUsed(CoarseHits.size(),false); + std::vector FineUsed(FineHits.size(),false); + for(const FSurfaceRoot& Root:Roots) + { + std::size_t CoarseIndex=0,FineIndex=0; + double CoarseError=0.0,FineError=0.0; + if(!MatchNearest(CoarseHits,CoarseUsed,Root.ZMeters,CoarseIndex,CoarseError) + ||!MatchNearest(FineHits,FineUsed,Root.ZMeters,FineIndex,FineError)) + { + ++Row.Cracks;AddMissing(Accumulator);continue; + } + CoarseUsed[CoarseIndex]=true; + FineUsed[FineIndex]=true; + AddError(Accumulator,CoarseHits[CoarseIndex]-FineHits[FineIndex], + Root,PixelBudgetMeters,View,Coarse.X(Ix),Coarse.Y(Iy)); + ++Row.Samples; + } + } + FErrorRow Stats;Stats.Lod=Patch.Level;Stats.Valid=true; + FinalizeError(Stats,Accumulator); + Row.Samples=Stats.Samples;Row.P50=Stats.P50;Row.P99=Stats.P99;Row.Maximum=Stats.Maximum; + Row.VerticalP50=Stats.VerticalP50;Row.VerticalP99=Stats.VerticalP99; + Row.VerticalMaximum=Stats.VerticalMaximum; + Row.MaximumNormalDistanceMeters=Stats.MaximumNormalDistanceMeters; + Row.MaximumX=Stats.MaximumX;Row.MaximumY=Stats.MaximumY; + Row.MaximumReferenceZ=Stats.MaximumReferenceZ; + Row.MaximumDisplayedZ=Stats.MaximumDisplayedZ; + Row.MaximumSlopeDegrees=Stats.MaximumSlopeDegrees; + Row.SlopeSamples=Stats.SlopeSamples;Row.SlopeP99=Stats.SlopeP99; + Row.SlopeMaximum=Stats.SlopeMaximum; + return Row; + } + + inline std::array ClosestDisplayedDistancesMeters( + const FStandaloneOwnerWorld& World,const FVector& CameraVoxels, + std::uint32_t VoxelSizeMicrons) + { + const double VoxelMeters=static_cast(VoxelSizeMicrons)/1000000.0; + std::array Minimums{}; + Minimums.fill(std::numeric_limits::infinity()); + for(int Lod=0;Lod<=8;++Lod) + { + const int PhaseCount=1<(Phase*32.0+32.0-1.0/256.0); + const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,Probe); + for(const FVoxelTileKey& Tile:Desired.Tiles) + { + if(Tile.Level!=Lod)continue; + Minimums[Lod]=std::min(Minimums[Lod], + TileAabbDistanceMeters(Tile,Probe,VoxelMeters)); + } + } + } + for(double& Distance:Minimums) + if(!std::isfinite(Distance))Distance=0.0; + return Minimums; + } + + inline std::array ClipmapClosestDisplayedDistancesMeters( + const FStandaloneOwnerWorld& World,std::uint32_t VoxelSizeMicrons) + { + const double VoxelMeters=static_cast(VoxelSizeMicrons)/1000000.0; + std::array Distances{}; + for(int Lod=1;Lod<=8;++Lod) + Distances[Lod]=static_cast(World.ClipRadius) + *static_cast(CHUNK_SIZE)*(1ull<<(Lod-1))*VoxelMeters; + return Distances; + } + + inline void PrintSlopeTail(const char* Kind,int Lod,const std::array& Counts, + const std::array& P99,const std::array& Maximum) + { + constexpr const char* Labels[4]={"lt30","30to45","45to60","ge60"}; + for(std::size_t Bin=0;Bin(Counts[Bin]),P99[Bin],Maximum[Bin]); + } + + inline void PrintKnownFailureControl(double CellMeters,double DisplayDistanceMeters, + const FViewBasis& View) + { + constexpr double Slope=100.0; + const double LateralMeters=CellMeters/200.0; + const double VerticalMeters=Slope*LateralMeters; + const double NormalLength=std::sqrt(1.0+Slope*Slope); + const double NormalDistance=VerticalMeters/NormalLength; + const std::array Normal={-Slope/NormalLength,0.0,1.0/NormalLength}; + const double ProjectedNormal=std::hypot( + Normal[0]*View.Right[0]+Normal[1]*View.Right[1]+Normal[2]*View.Right[2], + Normal[0]*View.Up[0]+Normal[1]*View.Up[1]+Normal[2]*View.Up[2]); + const double PixelBudgetMeters=DisplayDistanceMeters*OnePixelRadians; + const double VerticalPixels=VerticalMeters/PixelBudgetMeters; + const double NormalPixels=NormalDistance*ProjectedNormal/PixelBudgetMeters; + const bool Passed=VerticalPixels>4.0&&NormalPixels<1.0; + std::printf("[ScreenSpaceKnownFailureControl] case=steep-plane-lateral-offset slope_deg=%.4f cell_m=%.6f lateral_m=%.6f cell_fraction=%.6f vertical_error_m=%.6f vertical_height_px=%.4f closest_normal_m=%.6f projected_normal_px=%.4f status=%s\n", + std::atan(Slope)*180.0/3.14159265358979323846,CellMeters,LateralMeters, + LateralMeters/CellMeters,VerticalMeters,VerticalPixels,NormalDistance,NormalPixels, + Passed?"EXPECTED_VERTICAL_FALSE_POSITIVE":"CONTROL_FAILED"); + } + + inline bool Run(const std::array,9>& SampleTiles, + FStandaloneStreamingGenerator& Streamer, + const VoxelForgeCore::WorldAssemblyManifest& Assembly, + const std::array& Distances,std::uint32_t VoxelSizeMicrons,int MaxLod, + const std::array& PixelBudgetsSubvoxels, + double CameraYawDegrees,double CameraPitchDegrees) + { + const double VoxelMeters=static_cast(VoxelSizeMicrons)/1000000.0; + const FViewBasis View=MakeViewBasis(CameraYawDegrees,CameraPitchDegrees); + std::array Budgets{}; + std::array ErrorRows{}; + std::array PopRows{}; + bool GatePass=true; + std::printf("[ScreenSpaceGate] angular_pixel=%.9f rad (horizontal_fov=90deg,width=2560) lod>=3_limit=1.0px sampling=65x65 skirts=excluded metric=local_surface_normal_projected field_reference=direct-Heightfield2D-roots normal_probe_vox=1 view_yaw_deg=%.3f view_pitch_deg=%.3f\n", + OnePixelRadians,CameraYawDegrees,CameraPitchDegrees); + for(int Lod=0;Lod<=MaxLod&&Lod<9;++Lod) + { + Budgets[Lod]=Distances[Lod]*OnePixelRadians; + std::printf("[ScreenSpaceDistance] lod=%d closest_m=%.3f pixel_budget_m=%.6f pixel_budget_cm=%.3f pixel_budget_subvoxels=%lld rule=%s\n", + Lod,Distances[Lod],Budgets[Lod],Budgets[Lod]*100.0, + static_cast(PixelBudgetsSubvoxels[Lod]),Lod<3?"exact-2cm":"1px"); + } + const double LOD3CellMeters=static_cast(CHUNK_SIZE*(1<<3))*VoxelMeters/64.0; + PrintKnownFailureControl(LOD3CellMeters,Distances[3],View); + VoxelForgeCore::Heightfield2D::resetThreadColumnCacheStats(false,true,0); + for(int Lod=3;Lod<=MaxLod&&Lod<9;++Lod) + { + if(!SampleTiles[Lod]||SampleTiles[Lod]->Vertices.empty()) + { + std::printf("[ScreenSpaceGateLOD] lod=%d status=NO_SURFACE_TILE\n",Lod); + GatePass=false;continue; + } + const FStandaloneCpuTile& CoarseTile=*SampleTiles[Lod]; + const FStandaloneCpuTile* CoarsePtr=&CoarseTile; + const std::span CoarseSpan(&CoarsePtr,1); + const FHeightGrid CoarseGrid=Rasterize(CoarseSpan,CoarseTile.Key,VoxelMeters); + const FStandaloneCpuTile ExactTile=Streamer.GenerateDiagnostic( + CoarseTile.Key,0,1,true); + const FStandaloneCpuTile* ExactPointer=&ExactTile; + const std::span ExactSpan(&ExactPointer,1); + const FHeightGrid ExactGrid=Rasterize(ExactSpan,ExactTile.Key,VoxelMeters); + ErrorRows[Lod]=CompareToReference(CoarseGrid,ExactGrid,CoarseTile.Key,VoxelMeters, + Budgets[Lod],Assembly,View); + const FErrorRow& Error=ErrorRows[Lod]; + const bool LodPass=Error.Valid&&Error.Missing==0&&Error.Floating==0 + &&Error.Samples>=16&&Error.Maximum<=1.0; + GatePass=GatePass&&LodPass; + std::printf("[ScreenSpaceGateLOD] lod=%d key=(%d,%d,%d) samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xy_m=(%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n", + Lod,CoarseTile.Key.Coord.X,CoarseTile.Key.Coord.Y,CoarseTile.Key.Coord.Z, + static_cast(Error.Samples),Error.P50,Error.P99,Error.Maximum, + Error.VerticalP50,Error.VerticalP99,Error.VerticalMaximum, + Error.MaximumNormalDistanceMeters,Error.MaximumX,Error.MaximumY, + Error.MaximumSlopeDegrees, + static_cast(Error.Missing),static_cast(Error.Floating), + LodPass?"PASS":"REJECT"); + PrintSlopeTail("same_lod",Lod,Error.SlopeSamples,Error.SlopeP99,Error.SlopeMaximum); + + const FErrorRow FieldError=CompareToExact(CoarseGrid,CoarseTile.Key,VoxelMeters, + Budgets[Lod],Assembly,View); + const bool FieldPass=FieldError.Valid&&FieldError.Missing==0&&FieldError.Floating==0 + &&FieldError.Samples>=16&&FieldError.Maximum<=1.0; + GatePass=GatePass&&FieldPass; + std::printf("[ScreenSpaceGateFieldLOD] lod=%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f missing=%llu floating=%llu status=%s\n", + Lod,static_cast(FieldError.Samples),FieldError.P50, + FieldError.P99,FieldError.Maximum,FieldError.VerticalP50,FieldError.VerticalP99, + FieldError.VerticalMaximum,FieldError.MaximumNormalDistanceMeters, + FieldError.MaximumX,FieldError.MaximumY,FieldError.MaximumDisplayedZ, + FieldError.MaximumSlopeDegrees, + static_cast(FieldError.Missing), + static_cast(FieldError.Floating),FieldPass?"PASS":"REJECT"); + PrintSlopeTail("field",Lod,FieldError.SlopeSamples,FieldError.SlopeP99, + FieldError.SlopeMaximum); + + bool FaultControlsPass=true; + const FHeightGrid MissingGrid(ExactGrid.Side,ExactGrid.MinimumX,ExactGrid.MaximumX, + ExactGrid.MinimumY,ExactGrid.MaximumY); + const FErrorRow MissingError=CompareToReference(MissingGrid,ExactGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); + const bool MissingRejected=MissingError.Missing>0&&MissingError.Maximum>1.0; + FaultControlsPass=FaultControlsPass&&MissingRejected; + std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-displayed-surface missing=%llu status=%s\n", + Lod,static_cast(MissingError.Missing), + MissingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); + + FStandaloneCpuTile FloatingTile=CoarseTile; + const std::uint32_t FloatingVertexBase=static_cast(FloatingTile.Vertices.size()); + for(FStandaloneCpuVertex Vertex:CoarseTile.Vertices) + { + Vertex.Position[2]+=2.0f; + FloatingTile.Vertices.push_back(Vertex); + } + for(const std::uint32_t Index:CoarseTile.Indices) + FloatingTile.Indices.push_back(FloatingVertexBase+Index); + FloatingTile.TriangleSkirtFaceMasks.insert(FloatingTile.TriangleSkirtFaceMasks.end(), + CoarseTile.TriangleSkirtFaceMasks.begin(),CoarseTile.TriangleSkirtFaceMasks.end()); + const FStandaloneCpuTile* FloatingPointer=&FloatingTile; + const std::span FloatingSpan(&FloatingPointer,1); + const FHeightGrid FloatingGrid=Rasterize(FloatingSpan,CoarseTile.Key,VoxelMeters); + const FErrorRow FloatingError=CompareToReference(FloatingGrid,ExactGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); + const bool FloatingRejected=FloatingError.Floating>0||FloatingError.Maximum>1.0; + FaultControlsPass=FaultControlsPass&&FloatingRejected; + std::printf("[ScreenSpaceFaultInjection] lod=%d case=2m-floating-surface floating=%llu max_px=%.4f status=%s\n", + Lod,static_cast(FloatingError.Floating),FloatingError.Maximum, + FloatingRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); + + std::vector FineTiles; + FineTiles.reserve(8); + for(int Dz=0;Dz<=1;++Dz) + for(int Dy=0;Dy<=1;++Dy) + for(int Dx=0;Dx<=1;++Dx) + { + const FVoxelTileKey FineKey(FIntVector(2*CoarseTile.Key.Coord.X+Dx, + 2*CoarseTile.Key.Coord.Y+Dy,2*CoarseTile.Key.Coord.Z+Dz),Lod-1); + FStandaloneCpuTile FineTile=Streamer.GenerateDiagnostic(FineKey); + if(!FineTile.Vertices.empty())FineTiles.push_back(std::move(FineTile)); + } + std::vector FinePointers; + FinePointers.reserve(FineTiles.size()); + for(const auto& Tile:FineTiles)FinePointers.push_back(&Tile); + const auto FineSpan=std::span( + FinePointers.data(),FinePointers.size()); + const FHeightGrid FineGrid=Rasterize(FineSpan,CoarseTile.Key,VoxelMeters); + PopRows[Lod]=ComparePop(CoarseGrid,FineGrid,CoarseTile.Key,VoxelMeters, + Budgets[Lod],Assembly,View); + const FPopRow& Pop=PopRows[Lod]; + const bool PopPass=Pop.Valid&&Pop.Cracks==0&&Pop.Floating==0&&Pop.Maximum<=1.0; + GatePass=GatePass&&PopPass; + std::printf("[ScreenSpaceSwap] lod=%d_to_%d samples=%llu normal_px_p50=%.4f p99=%.4f max=%.4f vertical_px_p50=%.4f p99=%.4f max=%.4f max_normal_m=%.6f max_xyz_m=(%.3f,%.3f,%.3f) max_slope_deg=%.2f cracks=%llu floating=%llu status=%s\n", + Lod,Lod-1,static_cast(Pop.Samples), + Pop.P50,Pop.P99,Pop.Maximum,Pop.VerticalP50,Pop.VerticalP99,Pop.VerticalMaximum, + Pop.MaximumNormalDistanceMeters,Pop.MaximumX,Pop.MaximumY, + Pop.MaximumDisplayedZ, + Pop.MaximumSlopeDegrees,static_cast(Pop.Cracks), + static_cast(Pop.Floating), + PopPass?"PASS":"REJECT"); + PrintSlopeTail("swap",Lod,Pop.SlopeSamples,Pop.SlopeP99,Pop.SlopeMaximum); + + const FHeightGrid MissingFineGrid(CoarseGrid.Side,CoarseGrid.MinimumX, + CoarseGrid.MaximumX,CoarseGrid.MinimumY,CoarseGrid.MaximumY); + const FPopRow MissingFineError=ComparePop(CoarseGrid,MissingFineGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); + const bool CrackRejected=MissingFineError.Cracks>0&&MissingFineError.Maximum>1.0; + FaultControlsPass=FaultControlsPass&&CrackRejected; + GatePass=GatePass&&FaultControlsPass; + std::printf("[ScreenSpaceFaultInjection] lod=%d case=missing-fine-ring-surface cracks=%llu max_px=%.4f status=%s\n", + Lod,static_cast(MissingFineError.Cracks), + MissingFineError.Maximum,CrackRejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); + + bool NegativeControlsPass=true; + const auto CheckNegative=[&](const char* Name,std::uint32_t Drop,std::uint32_t Stride) + { + FStandaloneCpuTile Bad=Streamer.GenerateDiagnostic( + CoarseTile.Key,Drop,Stride,true); + const FStandaloneCpuTile* BadPointer=&Bad; + const std::span BadSpan(&BadPointer,1); + const FHeightGrid BadGrid=Rasterize(BadSpan,Bad.Key,VoxelMeters); + const FErrorRow BadError=CompareToReference(BadGrid,ExactGrid, + CoarseTile.Key,VoxelMeters,Budgets[Lod],Assembly,View); + const bool Rejected=BadError.Missing>0||BadError.Floating>0 + ||BadError.Samples<16||BadError.Maximum>1.0; + NegativeControlsPass=NegativeControlsPass&&Rejected; + std::printf("[ScreenSpaceNegative] lod=%d case=%s p50_px=%.4f p99_px=%.4f max_px=%.4f missing=%llu floating=%llu status=%s\n", + Lod,Name,BadError.P50,BadError.P99,BadError.Maximum, + static_cast(BadError.Missing), + static_cast(BadError.Floating), + Rejected?"REJECTED_AS_REQUIRED":"GATE_FALSE_NEGATIVE"); + }; + CheckNegative("s13-global-octave-drop",1,1); + CheckNegative("stride-2-column-interpolation",0,2); + GatePass=GatePass&&NegativeControlsPass; + } + std::printf("[ScreenSpaceGateResult] status=%s\n",GatePass?"PASS":"REJECT"); + return GatePass; + } +} + diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 6934a00..ad0ad16 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -37,6 +37,7 @@ namespace bool ProfileColumnOverlap=false; bool ForceGenerate=false; bool MeshCoverage=false; + bool ScreenSpaceGate=false; bool HasSeedOverride=false; int SeedOverride=0; bool HasRenderDistanceOverride=false; @@ -48,6 +49,9 @@ namespace bool HasCamera=false; bool AutoSurfaceCamera=false; FVector CameraMeters=FVector::ZeroVector; + float CameraYawDegrees=0.0f; + float CameraPitchDegrees=0.0f; + std::filesystem::path MeshExportPath; }; bool ParseInteger(std::string_view text,int& output) @@ -61,13 +65,15 @@ namespace return true; } - bool ParseCamera(std::string_view text,FVector& meters) + bool ParseCamera(std::string_view text,FVector& meters,float& yawDegrees,float& pitchDegrees) { 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}; + yawDegrees=yaw; + pitchDegrees=pitch; return true; } @@ -85,10 +91,12 @@ namespace std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n" " --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n" " --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n" + " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands\n" " --auto-surface-camera (place the 27-tile cube on this seed's surface)\n" " --mesh-coverage (report non-empty tiles and indexed triangles)\n" + " --screen-space-gate (measure display error and historical negative controls)\n" " --render-distance-chunks=N (0 disables the extra far ring)\n" " --tls-probe=N (Linux worker TLS/stack measurement; N=0..12)\n" " --tls-probe-touch=none|surface-world|floating-islands (first-use probe)\n" @@ -103,6 +111,7 @@ namespace else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true; else if(arg=="--force-generate")options.ForceGenerate=true; else if(arg=="--mesh-coverage")options.MeshCoverage=true; + else if(arg=="--screen-space-gate")options.ScreenSpaceGate=true; else if(arg=="--auto-surface-camera")options.AutoSurfaceCamera=true; else if(arg.starts_with("--recipe=")) { @@ -135,10 +144,17 @@ namespace } else if(arg.starts_with("--camera=")) { - if(!ParseCamera(arg.substr(9),options.CameraMeters)) + if(!ParseCamera(arg.substr(9),options.CameraMeters, + options.CameraYawDegrees,options.CameraPitchDegrees)) {std::fprintf(stderr,"Invalid --camera value: %.*s\n",static_cast(arg.size()),arg.data());return false;} options.HasCamera=true; } + else if(arg.starts_with("--mesh-export=")) + { + options.MeshExportPath=std::filesystem::path(arg.substr(14)); + if(options.MeshExportPath.empty()) + {std::fprintf(stderr,"--mesh-export needs a file path\n");return false;} + } else if(arg.starts_with("--workers=")) {if(!readInt("--workers=",options.WorkerCount))return false;} else if(arg.starts_with("--clip-radius=")) @@ -297,5 +313,6 @@ int main(int argc,char** argv) cameraVoxels,options.WorkerCount,options.MaxLevel,VoxelForgeCore::DetailVariant::coherent, scheduler,options.StreamHash,false,false,false,false,coldStart, options.ProfileColumnOverlap,options.FarHorizonLOD8InteriorStepVoxels, - options.MeshCoverage); + options.MeshCoverage,options.MeshExportPath,options.CameraYawDegrees, + options.CameraPitchDegrees,options.ScreenSpaceGate); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index a3e8344..8388ea8 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -2,6 +2,7 @@ #include "OwnerWorldLoader.h" #include "StreamingWorld.h" +#include "LinuxScreenSpaceGate.h" #include #include @@ -9,6 +10,8 @@ #include #include #include +#include +#include #include #include #include @@ -17,6 +20,76 @@ namespace StandaloneStreamBenchmark { + struct FSceneExportWriter + { + std::ofstream File; + bool bGood=true; + std::uint32_t TileCount=0; + + template void Write(const T& Value) + { + File.write(reinterpret_cast(&Value),sizeof(Value)); + bGood=bGood&&static_cast(File); + } + + bool Open(const std::filesystem::path& Path,std::uint32_t VoxelSizeMicrons, + const FVector& CameraVoxels,float YawDegrees,float PitchDegrees, + std::uint32_t ExpectedTiles) + { + std::error_code Error; + if(!Path.parent_path().empty()) + std::filesystem::create_directories(Path.parent_path(),Error); + if(Error)return false; + File.open(Path,std::ios::binary|std::ios::trunc); + if(!File)return false; + constexpr char Magic[8]={'V','F','M','S','H','0','0','1'}; + File.write(Magic,sizeof(Magic)); + const std::uint32_t Version=1; + Write(Version);Write(VoxelSizeMicrons);Write(ExpectedTiles); + const double MicronsToMeters=static_cast(VoxelSizeMicrons)/1000000.0; + const double Camera[3]={CameraVoxels.X*MicronsToMeters, + CameraVoxels.Y*MicronsToMeters,CameraVoxels.Z*MicronsToMeters}; + for(const double Value:Camera)Write(Value); + const double Yaw=YawDegrees,Pitch=PitchDegrees; + Write(Yaw);Write(Pitch); + return bGood; + } + + void Add(const FStandaloneCpuTile& Tile) + { + const std::int32_t Key[4]={Tile.Key.Coord.X,Tile.Key.Coord.Y, + Tile.Key.Coord.Z,Tile.Key.Level}; + for(const auto Value:Key)Write(Value); + const std::uint32_t SurfaceVertexCount=Tile.SurfaceVertexCount; + const std::uint32_t SkirtTriangleCount=Tile.SkirtTriangleCount; + const std::uint32_t VertexCount=static_cast(Tile.Vertices.size()); + const std::uint32_t IndexCount=static_cast(Tile.Indices.size()); + const std::uint32_t MaskCount=static_cast(Tile.TriangleSkirtFaceMasks.size()); + Write(SurfaceVertexCount);Write(SkirtTriangleCount); + Write(VertexCount);Write(IndexCount);Write(MaskCount); + for(const FStandaloneCpuVertex& Vertex:Tile.Vertices) + for(const float Value:Vertex.Position)Write(Value); + for(const FStandaloneCpuVertex& Vertex:Tile.Vertices) + for(const float Value:Vertex.Normal)Write(Value); + if(IndexCount) + File.write(reinterpret_cast(Tile.Indices.data()), + static_cast(sizeof(std::uint32_t)*Tile.Indices.size())); + if(MaskCount) + File.write(reinterpret_cast(Tile.TriangleSkirtFaceMasks.data()), + static_cast(Tile.TriangleSkirtFaceMasks.size())); + bGood=bGood&&static_cast(File); + ++TileCount; + } + + bool Close(std::uint32_t ExpectedTiles) + { + File.flush(); + const bool bComplete=bGood&&static_cast(File)&&TileCount==ExpectedTiles; + File.close(); + return bComplete; + } + }; + inline void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8) { for(int32 I=0;I ClosestDisplayDistances{}; + std::array PixelBudgetsSubvoxels{}; + if(bScreenSpaceGate) + { + ClosestDisplayDistances=StandaloneScreenSpaceGate::ClosestDisplayedDistancesMeters( + World,CameraPositionVoxels,World.CoreVoxelSizeMicrons); + const double VoxelMeters=static_cast(World.CoreVoxelSizeMicrons)/1000000.0; + for(int32 Lod=3;Lod<=World.MaxClipLevel&&Lod<9;++Lod) + PixelBudgetsSubvoxels[static_cast(Lod)]=static_cast(std::floor( + ClosestDisplayDistances[static_cast(Lod)] + *StandaloneScreenSpaceGate::OnePixelRadians/VoxelMeters + *VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)); + } const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels; const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls; VoxelForgeCore::World Shape(CoreSnapshot); @@ -154,6 +243,7 @@ namespace StandaloneStreamBenchmark FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels); + if(bScreenSpaceGate)Streamer.SetMesherStageProfilingEnabled(true); const auto StreamStart=Clock::now(); const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired); if(Update.DesiredTiles!=static_cast(Desired.Tiles.size())) @@ -162,6 +252,22 @@ namespace StandaloneStreamBenchmark std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n", Desired.Tiles.size(),Update.DesiredTiles);return 7; } + FSceneExportWriter SceneExport; + const bool bExportRequested=!MeshExportPath.empty(); + bool bSceneExportPassing=true; + if(bExportRequested) + { + bSceneExportPassing=SceneExport.Open(MeshExportPath,World.CoreVoxelSizeMicrons, + CameraPositionVoxels,CameraYawDegrees,CameraPitchDegrees, + static_cast(Update.DesiredTiles)); + if(!bSceneExportPassing) + { + Streamer.Stop(); + std::fprintf(stderr,"[MeshSceneExport] FAIL: could not open %s\n", + MeshExportPath.string().c_str()); + return 8; + } + } std::vector DensityRows,MeshRows,PayloadRows,TopologyRows; int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0; @@ -196,6 +302,8 @@ namespace StandaloneStreamBenchmark std::array ColumnEvalMsByLOD{},MarchingExclusiveMsByLOD{}; std::array GroundRefineExclusiveMsByLOD{},CapRefineExclusiveMsByLOD{}; std::array MeshAssemblyMsByLOD{}; + std::array SkirtMsByLOD{}; + std::array SkirtTrianglesByLOD{}; std::array TileCountByLOD{}; for(const FVoxelTileKey& Tile:Desired.Tiles) if(Tile.Level>=0&&Tile.Level(ExpectedTilesByLOD.size())) @@ -214,6 +322,9 @@ namespace StandaloneStreamBenchmark double NativeMeshingMilliseconds=0.0; for(const FVoxelTileKey& Tile:Desired.Tiles) if(Tile.Level,9> GateSampleTiles{}; + std::array GateSampleDistances{}; + GateSampleDistances.fill(std::numeric_limits::infinity()); Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{}; bool bNeighborhoodReported=false,bFullDetailReported=false; while(Finished=3&&Tile.Key.Level<9 + &&!Tile.Vertices.empty() + &&StandaloneScreenSpaceGate::TileHasNativeSurface(Tile.Key,*Assembly, + static_cast(World.CoreVoxelSizeMicrons)/1000000.0)) + { + const double Distance=StandaloneScreenSpaceGate::TileAabbDistanceMeters( + Tile.Key,CameraPositionVoxels, + static_cast(World.CoreVoxelSizeMicrons)/1000000.0); + const bool bCloser=DistanceKey); + if(bCloser||(bTied&&bKeyLess)) + {GateSampleDistances[LOD]=Distance;GateSampleTiles[LOD]=Tile;} + } } if(bProfileColumnOverlap&&Tile.Key.Level>=0 &&Tile.Key.Level(ColumnCaptureTilesByLOD.size())) @@ -321,6 +450,28 @@ namespace StandaloneStreamBenchmark } } Streamer.Stop(); + bool bScreenSpaceGatePassing=true; + if(bScreenSpaceGate) + { + if(Assembly) + bScreenSpaceGatePassing=StandaloneScreenSpaceGate::Run(GateSampleTiles, + Streamer,*Assembly,ClosestDisplayDistances,World.CoreVoxelSizeMicrons, + World.MaxClipLevel,PixelBudgetsSubvoxels,CameraYawDegrees, + CameraPitchDegrees); + else + {std::fprintf(stderr,"[ScreenSpaceGateResult] no-world-assembly status=REJECT\n");bScreenSpaceGatePassing=false;} + } + if(bExportRequested) + { + bSceneExportPassing=SceneExport.Close(static_cast(Update.DesiredTiles)); + std::error_code FileSizeError; + const std::uintmax_t ExportBytes=std::filesystem::file_size(MeshExportPath,FileSizeError); + std::printf("[MeshSceneExport] tiles=%u expected=%d bytes=%llu status=%s path=%s\n", + SceneExport.TileCount,Update.DesiredTiles, + static_cast(FileSizeError?0:ExportBytes), + bSceneExportPassing?"PASS":"FAIL",MeshExportPath.string().c_str()); + bSceneExportPassing=bSceneExportPassing&&!FileSizeError; + } bool bColumnOverlapCapturePassing=true; if(bProfileColumnOverlap) { @@ -341,10 +492,12 @@ namespace StandaloneStreamBenchmark } for(std::size_t LOD=0;LOD(TileCountByLOD[LOD]),WorkerSecondsByLOD[LOD], ColumnEvalMsByLOD[LOD],MarchingExclusiveMsByLOD[LOD], - GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD],MeshAssemblyMsByLOD[LOD]); + GroundRefineExclusiveMsByLOD[LOD],CapRefineExclusiveMsByLOD[LOD], + SkirtMsByLOD[LOD],static_cast(SkirtTrianglesByLOD[LOD]), + MeshAssemblyMsByLOD[LOD]); } const double NeighborhoodSeconds=std::chrono::duration(NeighborhoodReady-ColdStart).count(); const double NeighborhoodMeshSeconds=std::chrono::duration(NeighborhoodReady-ShapeReady).count(); @@ -393,9 +546,59 @@ namespace StandaloneStreamBenchmark static_cast(HashTileRows(MeshRows)), static_cast(HashTileRows(PayloadRows)), static_cast(HashTileRows(TopologyRows))); + for(std::size_t LOD=0;LOD& Rows) + { + std::vector Selected; + for(const FHashedTile& Row:Rows) + if(Row.Key.Level==static_cast(LOD))Selected.push_back(Row); + return Selected; + }; + auto LodDensity=FilterLOD(DensityRows),LodMesh=FilterLOD(MeshRows); + auto LodPayload=FilterLOD(PayloadRows),LodTopology=FilterLOD(TopologyRows); + std::printf("[PipelineLODHash] lod=%zu tiles=%llu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n", + LOD,static_cast(TileCountByLOD[LOD]), + static_cast(HashTileRows(LodDensity)), + static_cast(HashTileRows(LodMesh)), + static_cast(HashTileRows(LodPayload)), + static_cast(HashTileRows(LodTopology))); + } + const std::array QueryPoints={{ + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z}, + {CameraPositionVoxels.X+8.0,CameraPositionVoxels.Y,CameraPositionVoxels.Z}, + {CameraPositionVoxels.X-8.0,CameraPositionVoxels.Y,CameraPositionVoxels.Z}, + {CameraPositionVoxels.X,CameraPositionVoxels.Y+8.0,CameraPositionVoxels.Z}, + {CameraPositionVoxels.X,CameraPositionVoxels.Y-8.0,CameraPositionVoxels.Z}, + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z+8.0}, + {CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z-8.0}, + {CameraPositionVoxels.X+64.0,CameraPositionVoxels.Y-32.0,CameraPositionVoxels.Z+16.0}, + {CameraPositionVoxels.X-128.0,CameraPositionVoxels.Y+96.0,CameraPositionVoxels.Z-64.0}}}; + uint64 ExactDensityHash=14695981039346656037ull; + uint64 ExactUnclampedHash=14695981039346656037ull; + uint64 ExactDetailHash=14695981039346656037ull; + uint64 ExactCollisionHash=14695981039346656037ull; + for(const auto& Point:QueryPoints) + { + const float Density=Shape.densityAt(Point); + const float Unclamped=Shape.unclampedDensityAt(Point); + const auto Detail=Shape.detailSampleAt(Point); + HashWord(ExactDensityHash,std::bit_cast(Density),4); + HashWord(ExactUnclampedHash,std::bit_cast(Unclamped),4); + HashWord(ExactDetailHash,std::bit_cast(Detail.displacement),4); + HashWord(ExactDetailHash,std::bit_cast(Detail.caveInfluence),4); + HashWord(ExactDetailHash,std::bit_cast(Detail.floorInfluence),4); + HashWord(ExactCollisionHash,Shape.isSolidAt(Point)?1u:0u,1); + } + std::printf("[PipelineExactQueryHash] probes=%zu density=0x%016llx unclamped=0x%016llx detail=0x%016llx collision=0x%016llx path=direct-Core-queries\n", + QueryPoints.size(),static_cast(ExactDensityHash), + static_cast(ExactUnclampedHash), + static_cast(ExactDetailHash), + static_cast(ExactCollisionHash)); } return NeighborhoodFinished==static_cast(Neighborhood.size()) &&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing - &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing)?0:6; + &&(!bProfileColumnOverlap||bColumnOverlapCapturePassing) + &&(!bExportRequested||bSceneExportPassing)&&bScreenSpaceGatePassing?0:6; } } // namespace StandaloneStreamBenchmark diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 5c1b4eb..0391016 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -646,6 +646,16 @@ public: int32 GetWorkerCount()const{return WorkerCount;} uint64 GetCacheHits()const{return CacheHitCount.load(std::memory_order_relaxed);} + void SetMesherStageProfilingEnabled(bool Enabled) + {bProfileMesherStages=Enabled;} + FStandaloneCpuTile GenerateDiagnostic(const FVoxelTileKey& Key, + std::uint32_t GlobalOctaveDrop=0,std::uint32_t CellStride=1, + bool ForceGenerate=false) + { + FStandaloneTileJob Job;Job.Key=Key;Job.StartedAt=FClock::now();Job.QueuedAt=Job.StartedAt; + return Generate(Job,GlobalOctaveDrop,CellStride,ForceGenerate); + } + private: FStandaloneStreamingUpdate UpdateDesiredSet(FStandaloneDesiredTileSet NewDesired, const std::vector& ResidentKeys,uint64 CrossingIndex) @@ -761,12 +771,14 @@ private: Wake.notify_all(); return Stats; } - FStandaloneCpuTile Generate(const FStandaloneTileJob& Job) + FStandaloneCpuTile Generate(const FStandaloneTileJob& Job, + std::uint32_t DiagnosticGlobalOctaveDrop=0,std::uint32_t DiagnosticCellStride=1, + bool ForceGenerate=false) { const auto StartTime=Job.StartedAt; FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing; Out.QueuedAt=Job.QueuedAt;Out.StartedAt=Job.StartedAt; - if(MeshCache) + if(MeshCache&&!ForceGenerate) { if(const FStandaloneCpuTile* Cached=MeshCache->Find(Job.Key)) { @@ -805,6 +817,11 @@ private: Options.skipMaterialGroups=bSkipMaterialGroups;Options.skipUv=bSkipUV;Options.skipColors=bSkipColors; Options.captureColumnCoordinates=bCaptureColumnCoordinates; Options.lod8HorizonInteriorStepVoxels=Lod8HorizonInteriorStepVoxels; + Options.profileMesherStages=bProfileMesherStages&&!ForceGenerate; + Options.diagnosticGlobalOctaveDrop=DiagnosticGlobalOctaveDrop; + Options.diagnosticCellStride=std::max(1,DiagnosticCellStride); + Options.diagnosticColumnInterpolationStride= + std::max(1,DiagnosticCellStride); std::string Error; auto Result=CoreWorld->generateTile( {{Job.Key.Coord.X,Job.Key.Coord.Y,Job.Key.Coord.Z},Job.Key.Level},Options,Error); @@ -977,6 +994,7 @@ private: bool bSkipColors=false; bool bCaptureColumnCoordinates=false; int32 Lod8HorizonInteriorStepVoxels=0; + bool bProfileMesherStages=false; std::atomic CacheHitCount{0}; mutable std::mutex Mutex; std::condition_variable Wake; diff --git a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp index 936ef02..30b7a44 100644 --- a/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp +++ b/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp @@ -1768,6 +1768,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, const int32 GroundSurfaceTriangleCount=GroundTris.Num()/3; const int32 CapSurfaceTriangleCount=CapTris.Num()/3; TArray GroundSkirtFaceMasks,CapSkirtFaceMasks; + const uint64 SkirtStageStartCycles=OutStageTimings?FPlatformTime::Cycles64():0; if (bGenerateSkirts && (GroundTris.Num() + CapTris.Num()) > 0) { const float ExtentCm = (float)(CellsPerAxis * Step) * GeometryVoxelSize; @@ -2007,6 +2008,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, EmitSkirts(GroundTris,GroundSkirtFaceMasks); EmitSkirts(CapTris,CapSkirtFaceMasks); } + if(OutStageTimings) + OutStageTimings->SkirtMilliseconds=static_cast( + FPlatformTime::Cycles64()-SkirtStageStartCycles) + *FPlatformTime::GetSecondsPerCycle64()*1000.0; // F17 — concatène sol PUIS sky-cap : un run d'indices contigu par polygroup (voir // FVoxelMeshData::NumCeilingTriangles). Mêmes triangles qu'avant, seul l'ordre change. @@ -2059,6 +2064,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, AuditInterpolationCycles,AuditInterpolationSamples,SurfaceCellCalls); OutStageTimings->VertexDedupStreamMilliseconds=ScaleSampledCycles( AuditDedupStreamCycles,AuditDedupStreamSamples,SurfaceCellCalls); + OutStageTimings->SkirtMilliseconds=std::max(0.0, + OutStageTimings->SkirtMilliseconds); const double TotalMilliseconds=static_cast( FPlatformTime::Cycles64()-StageMesherStartCycles)*CycleToMs; OutStageTimings->OtherMilliseconds=FMath::Max(0.0,TotalMilliseconds @@ -2066,7 +2073,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels, -OutStageTimings->CellClassificationMilliseconds -OutStageTimings->GradientNormalsMilliseconds -OutStageTimings->EdgeInterpolationMilliseconds - -OutStageTimings->VertexDedupStreamMilliseconds); + -OutStageTimings->VertexDedupStreamMilliseconds + -OutStageTimings->SkirtMilliseconds); } return MeshData; diff --git a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h index 056dcac..1fb81d6 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h +++ b/Standalone/Source/VoxelForge/Public/VoxelMarchingCubesMesher.h @@ -50,6 +50,7 @@ struct FVoxelMarchingCubesStageTimings double GradientNormalsMilliseconds = 0.0; double EdgeInterpolationMilliseconds = 0.0; double VertexDedupStreamMilliseconds = 0.0; + double SkirtMilliseconds = 0.0; double OtherMilliseconds = 0.0; uint64 GradientFallbackDensityCalls = 0; uint64 EdgeMidpointDensityCalls = 0;