diff --git a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp index 6b3bc9b..d275a7b 100644 --- a/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp +++ b/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp @@ -470,12 +470,61 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe, Candidate.CenterY = CenterY; Candidate.Direction = DirectionIndex; Candidate.HalfLength = HalfLength; + const auto EndpointCoordinates = [&](std::int32_t Index) + { + const std::int64_t Along = (Index - 16) * HalfLength / 16; + return std::array{ + CenterX + WorldAssemblyRoundRatio( + Along * Directions[DirectionIndex].X, 65536), + CenterY + WorldAssemblyRoundRatio( + Along * Directions[DirectionIndex].Y, 65536)}; + }; + const auto ArrivalEndpoint = EndpointCoordinates(0); + const auto DepartureEndpoint = EndpointCoordinates(32); + const std::int64_t ArrivalXQ = WorldAssemblyFixed( + static_cast(ArrivalEndpoint[0])); + const std::int64_t ArrivalYQ = WorldAssemblyFixed( + static_cast(ArrivalEndpoint[1])); + const std::int64_t DepartureXQ = WorldAssemblyFixed( + static_cast(DepartureEndpoint[0])); + const std::int64_t DepartureYQ = WorldAssemblyFixed( + static_cast(DepartureEndpoint[1])); + const auto InRadius = [](std::int64_t XValue, std::int64_t YValue, + std::int64_t Radius) + { + return Radius <= 0 || XValue * XValue + YValue * YValue <= Radius * Radius; + }; + if ((ArrivalRadiusQ > 0 + && !InRadius(ArrivalXQ, ArrivalYQ, ArrivalRadiusQ)) + || (DepartureRadiusQ > 0 + && !InRadius(DepartureXQ, DepartureYQ, DepartureRadiusQ))) + { + // Mouth placement is independent of terrain, so reject it before + // allocating points or sampling the heightfield for grade. + ++FallbackPlacementRejected; + continue; + } Candidate.Points.reserve(33); + if (ArrivalRadiusQ > 0) + { + Candidate.EndpointProximityCost += static_cast( + ArrivalXQ * ArrivalXQ + ArrivalYQ * ArrivalYQ) * 16ull; + } + if (DepartureRadiusQ > 0) + { + Candidate.EndpointProximityCost += static_cast( + DepartureXQ * DepartureXQ + DepartureYQ * DepartureYQ); + // Approach the surface portal from the world-facing side. The + // resolved passage leaves the mouth along its outward tangent; + // a route that approaches from beyond the mouth overlaps that + // bore and turns its centerline into solid ground. + Candidate.OutwardProjection = DepartureXQ * Directions[DirectionIndex].X + + DepartureYQ * Directions[DirectionIndex].Y; + } std::int64_t PreviousGround = 0; std::int64_t PreviousX = 0; std::int64_t PreviousY = 0; bool bGradePass = true; - bool bPlacementPass = true; for (std::int32_t Index = 0; Index <= 32; ++Index) { const std::int64_t Along = (Index - 16) * HalfLength / 16; @@ -485,33 +534,6 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe, Along * Directions[DirectionIndex].Y, 65536); const std::int64_t XQ = WorldAssemblyFixed(static_cast(X)); const std::int64_t YQ = WorldAssemblyFixed(static_cast(Y)); - const auto InRadius = [](std::int64_t XValue, std::int64_t YValue, - std::int64_t Radius) - { - return Radius <= 0 || XValue * XValue + YValue * YValue <= Radius * Radius; - }; - if ((Index == 0 && ArrivalRadiusQ > 0 - && !InRadius(XQ, YQ, ArrivalRadiusQ)) - || (Index == 32 && DepartureRadiusQ > 0 - && !InRadius(XQ, YQ, DepartureRadiusQ))) - { - bPlacementPass = false; - } - if (Index == 0 && ArrivalRadiusQ > 0) - { - Candidate.EndpointProximityCost += static_cast(XQ * XQ + YQ * YQ) * 16ull; - } - if (Index == 32 && DepartureRadiusQ > 0) - { - Candidate.EndpointProximityCost += static_cast(XQ * XQ + YQ * YQ); - // Approach the surface portal from the world-facing side. The - // resolved passage leaves the mouth along its outward tangent; - // a route that approaches from beyond the mouth overlaps that - // bore and turns its centerline into solid ground. - const std::int64_t OutwardProjection = XQ * Directions[DirectionIndex].X - + YQ * Directions[DirectionIndex].Y; - Candidate.OutwardProjection = OutwardProjection; - } const auto Column = BaseField.point(WorldAssemblyFixed(static_cast(X)), WorldAssemblyFixed(static_cast(Y)), 0); const std::int64_t DeltaX = X - PreviousX; @@ -557,11 +579,10 @@ bool BuildFiniteSurfaceRoute(VoxelForgeCore::WorldAssemblyRecipeV1& Recipe, BestGradeCandidate.HalfLength = HalfLength; } } - if (bGradePass && bPlacementPass) Candidates.push_back(std::move(Candidate)); + if (bGradePass) Candidates.push_back(std::move(Candidate)); else { - if (!bGradePass) ++FallbackGradeRejected; - if (!bPlacementPass) ++FallbackPlacementRejected; + ++FallbackGradeRejected; } } UE_LOG(LogTemp, Display, diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 5b4e2a9..0a875eb 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -7812,6 +7812,7 @@ bool AVoxelWorld::InitializeVoxelForgeCoreWorld(FString& OutFailureReason) Request.workerLimit = static_cast(FMath::Clamp(GetMaxConcurrentTasks(), 1, 16)); Request.detail = VoxelForgeCore::DetailVariant::coherent; Request.includeFarWorld = false; + Request.parallelSignEdgeRepair = true; VoxelForgeCoreState = State; State->bSnapshotReady.store(true, std::memory_order_release); diff --git a/Standalone/Research/WorldAssemblerM3/Linux94/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux94/REPORT.md new file mode 100644 index 0000000..41d042d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux94/REPORT.md @@ -0,0 +1,45 @@ +# Linux 94 — Unreal startup + +## Before and after + +Medians are from L92's final serial-repair `after-direct-json` traces and L94's parallel-repair traces. Times start at `BeginPlay`, except export duration and `core_compile_start` → `bReady`. LOD0–2 means every currently desired key was settled before the scripted movement at 20 s. + +| Arrival | Measure | Before | After | Runs | +|---|---|---:|---:|---:| +| SW | `SerializeResolvedWorld` | 468.0 ms | 115.2 ms | 3 / 4 | +| SW | Core compile → `bReady` | 2.157 s | 0.198 s | 3 / 4 | +| SW | `bReady` | 2.738 s | 0.421 s | 3 / 4 | +| SW | First admission | 2.747 s | 0.434 s | 3 / 4 | +| SW | LOD0–2 settled | 5.198 s | 2.813 s | 3 / 3 of 4 | +| SW | `BeginPlay` game-thread block | 0.582 s | 0.224 s | 3 / 4 | +| FI | `SerializeResolvedWorld` | 465.6 ms | 115.4 ms | 3 / 3 | +| FI | Core compile → `bReady` | 0.184 s | 0.180 s | 3 / 3 | +| FI | `bReady` | 0.750 s | 0.408 s | 3 / 3 | +| FI | First admission | 0.762 s | 0.422 s | 3 / 3 | +| FI | LOD0–2 settled | 3.396 s | 3.119 s | 3 / 3 | +| FI | `BeginPlay` game-thread block | 0.570 s | 0.229 s | 3 / 3 | + +Export, compile, `bReady`, admission and residency values are direct trace measurements. The SW route candidate log window fell from a 451 ms median to 96 ms (timestamps rounded to 1 ms); the enclosing export timer fell by 353 ms. The route window is a boundary measurement, not an internal function timer. + +## Changes and hashes + +- Unreal now requests parallel sign-edge repair through its existing Unreal `ParallelFor` scheduler. The request remains capped at 10 workers on this 12-logical-core host; it creates no separate worker pool. Traces show compile launch queue delays of 0.04–0.18 ms. The first two seconds' sampled frame maximum was 28.7 ms in SW; FI had one 45.9 ms sample and two samples below 27.2 ms. The direct synchronous `BeginPlay` block is measured above. +- The SW fallback route planner checks its exact mouth-radius endpoint constraints before allocating route points or sampling terrain. It still visits 78,408 candidates, but skips heightfield work for the 58,480 placement rejects. Eligible candidates remain 35; the selected route remains center `(32,80)`, direction 6, half-length 16, lift 128, with three sweep attempts. +- Headless sign-edge repair now defaults to parallel, with `--serial-sign-edge-repair` retained for A/B runs. The passage composer was not changed. +- Unreal `world_assembly_hash=1ecc162755ab83ac`, `recipe_hash=52b010081ad4b550`, and `plan_hash=a06d76fd11d7a08e` match L92 in every FI/SW run. +- Unreal SW `sign_edge_distances` fell from 1,942.2 ms to 36.5 ms median; `ShapeStage` fell from 2,156.3 ms to 198.1 ms. FI stayed effectively level: 8.8 → 9.5 ms and 183.5 → 179.8 ms. +- A serial/parallel Core hash A/B at the SW and FI spawn coordinates produced identical per-key tile hashes, full-detail hashes, whole-view hashes and per-LOD hashes. SW matched for 1,408 tiles (whole-view density/mesh/payload/topology: `ba04e6da1010c52f / 93555a2b969b16a5 / 339eba0fad4f79a5 / c2edafbec2fa9305`); FI matched for 1,216 tiles (`3175bceb00bfc76c / d1dabfba70a95d8a / b23900f97cfbb2cf / 3967a02b7c1ae1c8`). This focused headless A/B used the current owner-export manifest `bcc0d0d33ea39435`; the separate Unreal transient manifest is the `1ecc…` hash above. +- In that headless A/B, SW `sign_edge_distances` was 1,753.9 ms serial and 86.4 ms parallel. The FI input had zero endpoint corrections in both modes. + +## Verification and failed runs + +- Fast tier: **PASS**, 274 checks (269 PASS, 5 diagnostics), 297.1 s; source stamp `03a8fce1a9067edb8a83461a836292686d74abd8ea84b8014d8006296da8428d`. Headless build and tier were pinned to CPUs 0–11 and capped at 6 GiB. Unreal Linux build succeeded with `-NoUBA -MaxParallelActions=4`, pinned to CPUs 0–11, without `--as`; it was not run during timing. The build emitted three existing RealtimeMesh deprecation warnings. +- SW repeat 3 exited Unreal successfully but missed three desired LOD0–2 tiles at the 20 s stationary boundary. They settled after movement began, at 25.36 s, so this run is excluded from the LOD median; repeat 4 completed the three-run residency sample. +- An auxiliary full `CoreWorldTest` without trace-only mode stopped before hashing: the host project's staged first-person Blueprint failed its `MaxWalkSpeed` pin preflight. The trace-only startup runs and the separate headless hash A/B completed. +- Logs and traces are under `Saved/Linux/l94/`; baseline traces are under `Saved/Linux/l92/unreal-arrival/`. `RunL94UnrealArrival.py` reproduces an individual Unreal trace run. No run exceeded 30 minutes. No commit, push or stash was made. + +## Source files + +- `Source/VoxelForge/Private/VoxelWorld.cpp` +- `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp` +- `Standalone/Source/StreamBenchmarkHeadless.cpp` diff --git a/Standalone/Research/WorldAssemblerM3/Linux94/RunL94UnrealArrival.py b/Standalone/Research/WorldAssemblerM3/Linux94/RunL94UnrealArrival.py new file mode 100644 index 0000000..cb1c724 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux94/RunL94UnrealArrival.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Matched null-RHI FI/SW startup traces for Linux 94.""" +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +HOST = Path("/home/jahni/VFHost") +HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l94" +HOST_SAVED = HOST / "Saved/Linux/l94" +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +OUT = ROOT / "Saved/Linux/l94/unreal-arrival" +SPAWNS_M = { + "FI": (25.8369140625, 3.19921875, -737.0), + "SW": (25.8369140625, 3.19921875, -200.0), +} +MOVE_START_S = 20.0 +MOVE_DURATION_S = 12.0 +DWELL_S = 5.0 +EXIT_S = MOVE_START_S + MOVE_DURATION_S + DWELL_S + + +def read_events(path: Path) -> list[dict]: + events: list[dict] = [] + if path.is_file(): + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + if line.startswith("{"): + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + pass + return events + + +def run(stratum: str, mode: str, repeat: int, phase: str) -> dict: + slot = 1 if stratum == "FI" else 0 + boost = 0 if mode == "off" else 1 + label = f"{phase}-{stratum.lower()}-{mode}-{repeat}" + trace = OUT / f"{label}.trace.jsonl" + abslog = OUT / f"{label}.log" + stdout = OUT / f"{label}.stdout.log" + output = HOST_PLUGIN_SAVED / "output" / label + userdir = HOST_SAVED / "user" / label + zendata = HOST_SAVED / "zen" / label + ddc = HOST_SAVED / "DDC" + for directory in (OUT, output, userdir, zendata, ddc): + directory.mkdir(parents=True, exist_ok=True) + trace.unlink(missing_ok=True) + spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWNS_M[stratum]) + command = [ + str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended", + "-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch", + "-notraceserver", "-nocrashreports", "-DisablePlugins=AndroidFileServer", + "-ddc=NoZenLocalFallback", f"-userdir={userdir}", f"-ZenDataPath={zendata}", + f"-LocalDataCachePath={ddc}", "-run=VoxelForgeExplore", "-seed=0", + "-archetype=TunnelNetwork", f"-slot={slot}", "-modes=export", "-gameconfig=1", + "-coreworldtest=1", "-ownerfourstrateplan=1", "-corevoxelsizecm=25", + "-coreislandtest=0", "-corefallbacktest=0", "-coreofftest=0", "-coreplantest=0", + "-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0", + f"-out={output}", "-voxel.StreamingCoarseFirst=1", + "-voxel.StreamingDynamicNearReserve=1", "-voxel.StreamingLegacyPendingResultCap=0", + "-voxel.CoreRegionConcurrentAdmission=1", "-voxel.StreamingResultBacklogCap=256", + "-voxel.StreamingReadyQueueDepth=8", "-voxel.StreamingBackgroundWorkerCap=1", + "-voxel.CoreColumnCacheDirectMapped=0", "-voxel.CoreMCGradientMemoization=1", + "-voxel.StreamingCollisionLookahead=1", f"-voxel.StreamingStartupApplyBoost={boost}", + "-voxel.TestMove=1", "-voxel.TestMoveSpeedCmPerSecond=140.0", + f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}", + f"-voxel.TestMoveDurationSeconds={MOVE_DURATION_S:.1f}", + f"-voxel.TestExitSeconds={EXIT_S:.1f}", "-voxel.TestMoveDirection=1,0,0", + "-voxel.StreamingSchedulerPolicy=3", "-voxel.TestExitOnSteady=0", + "-coreworldstreamtrace=1", f"-voxel.StartupTraceSpawnCm={spawn_cm}", + "-voxel.StartupTraceUntilEndPlay=1", "-voxel.StartupTraceThroughCrossing=1", + "-voxel.StartupTraceMaxTiles=16384", f"-voxel.StartupTraceFile={trace}", + f"-abslog={abslog}", + ] + wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command] + started = time.monotonic() + with stdout.open("w", encoding="utf-8") as stream: + result = subprocess.run(wrapped, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + + events = read_events(trace) + samples = [e for e in events if e.get("type") == "streaming_sample"] + arrival_samples = [e for e in samples if float(e.get("t_s", 0.0)) < MOVE_START_S] + ready = None + for event in arrival_samples: + desired = sum(event.get("desired_by_lod", [0] * 9)[:3]) + settled = sum(event.get("settled_by_lod", + event.get("resident_by_lod", [0] * 9))[:3]) + if desired and settled >= desired: + ready = event + break + first_admission = next((e for e in events if e.get("type") == "scheduler_admission" + and e.get("desired_rebuilt") == 1), None) + stage_ends = {e.get("name"): e for e in events if e.get("type") == "stage_end"} + core_events = {name: next((e for e in events if e.get("type") == name), None) + for name in ("core_snapshot_serialization", "core_snapshot_from_json", + "core_compile_launch", "core_compile_start", "core_bready")} + begin_play = stage_ends.get("BeginPlay", {}) + compile_start = core_events["core_compile_start"] + core_bready = core_events["core_bready"] + row = { + "label": label, "stratum": stratum, "slot": slot, "mode": mode, + "boost_cvar": boost, "repeat": repeat, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "sample_count": len(samples), "arrival_sample_count": len(arrival_samples), + "first_lod02_full_ready_s": ready.get("t_s") if ready else None, + "first_lod02_full_desired": sum(ready.get("desired_by_lod", [0] * 9)[:3]) + if ready else None, + "first_lod02_full_resident": sum(ready.get("resident_by_lod", [0] * 9)[:3]) + if ready else None, + "first_lod02_full_settled": sum(ready.get("settled_by_lod", + ready.get("resident_by_lod", [0] * 9))[:3]) + if ready else None, + "first_admission_t_s": first_admission.get("t_s") if first_admission else None, + "begin_play_end_t_s": begin_play.get("t_s"), + "begin_play_duration_s": begin_play.get("duration_s"), + "core_bready_t_s": core_bready.get("t_s") if core_bready else None, + "compile_start_to_bready_s": round( + core_bready["t_s"] - compile_start["t_s"], 6) + if core_bready and compile_start else None, + "startup_events": {key: value for key, value in core_events.items() if value}, + "snapshot_serialize_ms": core_events["core_snapshot_serialization"].get("duration_ms") + if core_events["core_snapshot_serialization"] else None, + "snapshot_from_json_ms": core_events["core_snapshot_from_json"].get("duration_ms") + if core_events["core_snapshot_from_json"] else None, + "trace": str(trace.relative_to(ROOT)), "abslog": str(abslog.relative_to(ROOT)), + "stdout": str(stdout.relative_to(ROOT)), + } + stdout_text = stdout.read_text(encoding="utf-8", errors="replace") + failure = re.search(r"\[VoxelForgeCoreWorldTest\] failed: ([^\r\n]+)", stdout_text) + if failure: + row["world_test_failure"] = failure.group(1) + apply_events = [e for e in events if e.get("type") == "startup_apply_budget"] + row["apply_budget_events"] = apply_events + with (OUT / "runs.jsonl").open("a", encoding="utf-8") as stream: + stream.write(json.dumps(row, sort_keys=True) + "\n") + print("[L94UnrealArrival] " + json.dumps(row, sort_keys=True), flush=True) + if result.returncode != 0: + raise SystemExit(f"Unreal commandlet failed: {stdout}; exit={result.returncode}") + if not core_bready: + raise SystemExit(f"Unreal startup trace has no bReady event: {trace}") + if not ready or len(arrival_samples) < 8: + raise SystemExit(f"Unreal arrival trace incomplete: {stdout}; exit={result.returncode}") + return row + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--stratum", choices=("FI", "SW"), required=True) + parser.add_argument("--mode", choices=("on", "off"), default="on") + parser.add_argument("--repeat", type=int, default=1) + parser.add_argument("--phase", choices=("before", "after", "after-settled", "after-hold20", + "after-direct-json"), + default="after") + args = parser.parse_args() + if not EDITOR.is_file(): + raise SystemExit(f"missing Unreal commandlet executable: {EDITOR}") + row = run(args.stratum, args.mode, args.repeat, args.phase) + (OUT / f"{row['label']}.summary.json").write_text( + json.dumps(row, indent=2) + "\n", encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 381cb17..741c94f 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -53,8 +53,7 @@ namespace bool Linux77Profile=false; bool ExactMidpointDensityMemo=false; bool ActiveLaneBatching=false; - bool ParallelSignEdgeRepair=false; - bool HasSignEdgeRepairOverride=false; + bool ParallelSignEdgeRepair=true; int ProofBlockCells=8; int ShapeHelperLimit=8; bool AuditBasePageEquivalence=false; @@ -1523,9 +1522,9 @@ namespace else if(arg=="--exact-midpoint-density-memo")options.ExactMidpointDensityMemo=true; else if(arg=="--active-lane-batching")options.ActiveLaneBatching=true; else if(arg=="--parallel-sign-edge-repair") - {options.ParallelSignEdgeRepair=true;options.HasSignEdgeRepairOverride=true;} + {options.ParallelSignEdgeRepair=true;} else if(arg=="--serial-sign-edge-repair") - {options.ParallelSignEdgeRepair=false;options.HasSignEdgeRepairOverride=true;} + {options.ParallelSignEdgeRepair=false;} else if(arg.starts_with("--proof-block-cells=")) { if(!readInt("--proof-block-cells=",options.ProofBlockCells) @@ -2275,8 +2274,7 @@ int main(int argc,char** argv) options.AuditOreVeinYield,options.AuditNativeCaves,options.InstrumentTileDrain, options.AuditMotifIntervals, options.ExactMidpointDensityMemo,options.ActiveLaneBatching, - options.ProofBlockCells,options.HasSignEdgeRepairOverride - ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe, + options.ProofBlockCells,options.ParallelSignEdgeRepair, options.AuditNativeLattice,options.LatticeOrphanControl, options.CaptureSkipWalkability,options.ProfilePassageComposer, options.StreamHashAllowUniformSkips,options.Linux77Profile,