From 2c8a26c0d03318bd435beee93ccfa5f887917bed Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Tue, 27 Oct 2026 20:17:35 +0100 Subject: [PATCH] streaming: far LODs keep a share while near work is pending (L79); no more horizon starvation L78's strict near-first order starved far LODs at 30 m/s (100% far holes, never completed). Unreal policy 2: once the collision ring is ready, LOD0-2 keep their reserve and far tiles are admitted as low-priority Horizon work; headless gives far work 1 of every 16 requests. Moving camera, far holes while moving: 1.4 m/s L77 8% / L78 29% / L79 31%; 30 m/s L77 8% / L78 100% / L79 72% (completes on dwell). Near readiness while moving at 30 m/s: 52 / 51 / 67%. Still worse than L77 for fast travel -> speed-adaptive policy next. World unchanged (v213). Fast tier PASS; Linux UE host builds. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- Source/VoxelForge/Private/VoxelWorld.cpp | 81 ++++++-- .../WorldAssemblerM3/Linux79/REPORT.md | 67 +++++++ .../Linux79/RunL79HeadlessMovingCamera.py | 124 ++++++++++++ .../Linux79/RunL79MovingCamera.py | 187 ++++++++++++++++++ .../Linux79/RunL79TimingPairs.py | 128 ++++++++++++ ...RunL79HeadlessMovingCamera.cpython-313.pyc | Bin 0 -> 9475 bytes .../RunL79MovingCamera.cpython-313.pyc | Bin 0 -> 15249 bytes .../RunL79TimingPairs.cpython-313.pyc | Bin 0 -> 9430 bytes Standalone/Source/StreamBenchmarkHeadless.cpp | 47 ++++- Standalone/Source/StreamBenchmarkPipeline.h | 151 +++++++++++++- Standalone/Source/StreamingWorld.h | 78 +++++++- 11 files changed, 833 insertions(+), 30 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/RunL79HeadlessMovingCamera.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/RunL79MovingCamera.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/RunL79TimingPairs.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79HeadlessMovingCamera.cpython-313.pyc create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79MovingCamera.cpython-313.pyc create mode 100644 Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79TimingPairs.cpython-313.pyc diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 8c329b7..fe50717 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -208,6 +208,12 @@ namespace GVoxelForgeStreamingCoarseFirst, TEXT("Prioritize walkable tiles, then finish LOD0-2 before far clipmap work; controls far-level order.")); + int32 GVoxelForgeStreamingSchedulerPolicy = 2; + FAutoConsoleVariableRef CVarVoxelForgeStreamingSchedulerPolicy( + TEXT("voxel.StreamingSchedulerPolicy"), + GVoxelForgeStreamingSchedulerPolicy, + TEXT("Streaming order: 0=L77 coarse-first, 1=L78 strict near-first, 2=L79 fair far share.")); + int32 GVoxelForgeStreamingDynamicNearReserve = 1; FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve( TEXT("voxel.StreamingDynamicNearReserve"), @@ -11949,6 +11955,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw const int32 NearAdmissionCap = bPawnMoving ? MaxTasks : TotalAdmissionCap; const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve) + ReadyQueueDepth; constexpr int32 FirstFarLevel = 3; + const int32 SchedulerPolicy = FMath::Clamp(GVoxelForgeStreamingSchedulerPolicy, 0, 2); + const int32 FirstCoarseLevel = FMath::Max( + 2, Settings ? Settings->FullResClipLevels : 2); const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0 && bWalkableRingReadyForReserve; int32 SchedulerSubmittedByLevel[9] = {}; @@ -12449,34 +12458,60 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw return true; }; - // The support and collision-critical set drains first. Then LOD0, LOD1, and LOD2 - // use the near lane. Far tiles are not admitted until every requested near tile is ready. - if (AreCriticalTilesReady() && bWalkableRingReadyForReserve) - { - for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level) - { - const EVoxelTileWorkPriority Priority = Level == 0 - ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; - SubmitLevel(Level, Priority, NearAdmissionCap, true); - } - } - - if (AreNearLODTilesReady()) + // Policy 0 reconstructs L77's coarse-first admission order for the moving-camera control. + if (SchedulerPolicy == 0) { if (bUseCoarseFirstOrder) { - for (int32 Level = MaxDesiredLevel; Level >= FirstFarLevel; --Level) + for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, CoarseTaskCap, true); } + for (int32 Level = 0; Level < FirstCoarseLevel && Level <= MaxDesiredLevel; ++Level) + { + const EVoxelTileWorkPriority Priority = Level == 0 + ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; + SubmitLevel(Level, Priority, + Priority == EVoxelTileWorkPriority::LOD0 ? NearAdmissionCap : CoarseTaskCap, + true); + } } else + { + SubmitLevel(0, EVoxelTileWorkPriority::LOD0, NearAdmissionCap, false); + for (int32 Level = 1; Level <= MaxDesiredLevel; ++Level) + { + SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, + CoarseTaskCap, false); + if (GetGenerationAdmissionCount() >= CoarseTaskCap) break; + } + } + } + else + { + const bool bNearAdmissionReady = AreCriticalTilesReady() + && bWalkableRingReadyForReserve; + if (bNearAdmissionReady) + { + for (int32 Level = 0; Level <= 2 && Level <= MaxDesiredLevel; ++Level) + { + const EVoxelTileWorkPriority Priority = Level == 0 + ? EVoxelTileWorkPriority::LOD0 : EVoxelTileWorkPriority::Horizon; + SubmitLevel(Level, Priority, NearAdmissionCap, true); + } + } + + // L78 waits for all requested near tiles. L79 keeps a one-task far lane open + // after critical requests; CoarseTaskCap reserves the remaining admissions for near work. + const bool bFarAdmissionReady = SchedulerPolicy == 1 + ? AreNearLODTilesReady() : true; + if (bFarAdmissionReady) { for (int32 Level = FirstFarLevel; Level <= MaxDesiredLevel; ++Level) { SubmitLevel(Level, EVoxelTileWorkPriority::Horizon, - CoarseTaskCap, false); + CoarseTaskCap, bUseCoarseFirstOrder); if (GetGenerationAdmissionCount() >= CoarseTaskCap) { if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; } @@ -12643,7 +12678,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw > ResultBacklogCapStopsAtUpdateEntry; for (int32 Level = 0; Level < 9; ++Level) { - AdmissionCapByLevel[Level] = Level <= 2 ? NearAdmissionCap : CoarseTaskCap; + if (SchedulerPolicy == 0) + { + AdmissionCapByLevel[Level] = bUseCoarseFirstOrder + ? (Level >= FirstCoarseLevel || Level > 0 ? CoarseTaskCap : NearAdmissionCap) + : (Level == 0 ? NearAdmissionCap : CoarseTaskCap); + } + else + { + AdmissionCapByLevel[Level] = Level <= 2 ? NearAdmissionCap : CoarseTaskCap; + } if (DesiredMissingByLevel[Level] <= 0) continue; if (bAllChunksLoaded) @@ -12730,7 +12774,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw "\"core_region_concurrent_admission\":%d," "\"max_tasks\":%d,\"ready_queue_depth\":%d," "\"total_admission_cap\":%d,\"near_admission_cap\":%d," - "\"near_reserve\":%d," + "\"near_reserve\":%d,\"scheduler_policy\":%d," "\"coarse_cap\":%d,\"first_far_level\":%d," "\"submission_stop_level\":%d,\"submission_stop_cap\":%d," "\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d," @@ -12754,7 +12798,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve, GVoxelForgeStreamingLegacyPendingResultCap, GVoxelForgeCoreRegionConcurrentAdmission, MaxTasks, ReadyQueueDepth, - TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, CoarseTaskCap, FirstFarLevel, + TotalAdmissionCap, NearAdmissionCap, NearTaskReserve, SchedulerPolicy, + CoarseTaskCap, FirstFarLevel, SchedulerOrderStopLevel, SchedulerOrderStopCap, PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard, ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate, diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux79/REPORT.md new file mode 100644 index 0000000..5d9da71 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux79/REPORT.md @@ -0,0 +1,67 @@ +# Linux79 report + +## Moving-camera starvation + +The UE scheduler now defaults to policy 2: once the collision-critical ring is ready, LOD0–2 keep their reserved admission budget and far tiles are admitted as low-priority `Horizon` work. It no longer waits for every near tile before admitting far work. The headless policy uses a recurring far share (one far request per 16 near requests). Generation stays on workers. + +The headless trace moved for 12 s, then dwelled for 5 s at 1.4 m/s and 30 m/s. Each run had 69 samples, 8 workers, and zero generation failures. Headless processes were pinned to CPUs 0–11, address-space limited to 6 GiB, and capped at 1,740 s. “Far holes” counts desired far tiles with neither their LOD nor a resident coarser parent. Near readiness is the moving-sample mean; dwell ended with near readiness at 100% in every run. + +| Speed | Order | All far ready | Moving far holes, mean | Max far stale age | Moving near ready, mean | Dwell far holes | +|---|---|---:|---:|---:|---:|---:| +| 1.4 m/s | L77 coarse-first | 1.50 s | 8.25% | 1.25 s | 73.73% | 0% | +| 1.4 m/s | L78 strict near-first | 4.00 s | 29.20% | 3.75 s | 85.03% | 0% | +| 1.4 m/s | L79 fair share | 4.25 s | 30.79% | 4.00 s | 82.90% | 0% | +| 30 m/s | L77 coarse-first | 1.75 s | 8.16% | 1.25 s | 52.14% | 0% | +| 30 m/s | L78 strict near-first | Never | 100.00% | 17.00 s | 51.40% | 100% | +| 30 m/s | L79 fair share | 9.75 s | 71.89% | 9.50 s | 66.67% | 0% | + +Verdict: L78 starves the horizon at 30 m/s, including through the dwell. L79 prevents indefinite starvation and finishes far coverage while still moving, but far coverage remains sparse during fast travel. L77 has much better far coverage and faster far completion, while L79 improves near readiness at 30 m/s. At walking speed L79 is close to L78. L79 is the near-priority/far-progress compromise; it does not provide L77-level moving coverage. + +Trace summary and samples: `Saved/Linux/l79/movement/headless/summary.json` and `samples.csv`; per-policy logs are alongside them. + +## Passage-cost decision + +I tested a point-bound filter candidate against the same page route on 32 tiles (8 per strate: four LOD0 and four LOD1). Across 42,875 density samples per tile it changed zero samples and zero signs. Maximum and mean surface displacement were both 0 cm; solid/air membership did not change, and there were no changed samples at mouths or within 4 m of walls. There were consequently no changed tiles to classify as mouth or wall losses. + +The candidate did not reduce passage work: candidate/base median tile-time ratios were SW 1.002, FI 1.014, TN 0.990, and VS 0.993. All inspected pairs also reported zero baseline and candidate segment visits, so this sample set is a negative control and does not validate bounds on an active passage-wall sample. I rejected the candidate: it showed no useful performance gain, and it is not accepted as proof that the larger passage cuts are safe. No field change or evaluator-version bump was made; the passage cost remains open. + +The broad L76 cut remains rejected (612 density mismatches; mesh, payload, and topology changed on 24 tiles). Its retained artifacts contain aggregate hash counts, not per-sample deltas, so this round does not claim to have inspected those individual changes. The L79 candidate’s per-tile evidence is in `Saved/Linux/l79/passage-bounds/{0-SurfaceWorld,1-FloatingIslands,2-TunnelNetwork,3-VerticalShafts}.log`. + +No new mouth or mid-tunnel capture set was made because no passage geometry change was accepted. The final fast tier’s existing passage audits passed, including support, capsule/walkability, grade/floor, ripple, naturalness, route binding, and mouth-profile gates. + +## Readiness, W8, three interleaved pairs, median seconds + +| Strate | L78 LOD0–2 | L79 LOD0–2 | L78 whole view | L79 whole view | +|---|---:|---:|---:|---:| +| SurfaceWorld | 1.232 | 1.365 | 2.762 | 2.611 | +| FloatingIslands | 3.391 | 3.503 | 4.881 | 4.763 | +| TunnelNetwork | 3.363 | 3.737 | 4.621 | 4.792 | +| VerticalShafts | 1.417 | 1.683 | 2.615 | 2.603 | + +L79 trades a small near-readiness regression for guaranteed far admission. The L77 sampler census from the L78 before/after pair set is retained here; B/A are before/after the exact composer trims. “Near” means LOD0–2. + +| Strate | Near grid-fill closure B/A | Whole closure B/A | Passage CPU share B/A | Support CPU share B/A | +|---|---:|---:|---:|---:| +| SurfaceWorld | 99.1% / 99.9% | 100.1% / 99.6% | 45.1% / 47.6% | 15.0% / 12.8% | +| FloatingIslands | 99.9% / 99.5% | 99.8% / 99.8% | 64.6% / 63.0% | 14.1% / 13.8% | +| TunnelNetwork | 99.9% / 100.1% | 99.8% / 100.2% | 65.9% / 63.7% | 13.2% / 13.5% | +| VerticalShafts | 100.0% / 100.2% | 99.9% / 100.9% | 45.9% / 42.9% | 5.9% / 6.6% | + +Timing data: `Saved/Linux/l79/timing/summary.json`. Sampler source data: `Saved/Linux/l78/{sampler-census,closure-census,route-census}.csv`; sampler handler cost stayed below 0.05% of profiled thread CPU. + +## Version, hashes, and verification + +- `WorldAssemblyEvaluatorVersion=213`; unchanged. The passage candidate was removed, so no geometry or world-hash update was accepted. +- `Standalone/data/owner_world.json` SHA256: `38b3d3085f2a0711a7c8dbe8883aaaf2510a9acbfb529ebece6de6d8c2dbfde`. +- Final source stamp: `084e7beed3039198b633d0075793f14796fdd7628b307406f6fdda7aedab641a`. +- Fast tier: PASS, 274 checks, 332.3 s. Passage audits and Clang W1/W8/W12 determinism checks passed. Clang and GCC binaries built at the final source stamp; no L79 GCC W1/W8/W12 matrix or new passage captures were run because no geometry change was accepted. Clang headless SHA256 `f67c58b3b60bf41bcdb630f82afee5b87a57c3b991dad3dbb4e93523032c9229`; GCC `faf139e63a7eff5c27f898bef508240ae602b63d9e509bc512dec71118fe4fe3`. +- Linux `VFHostEditor` build after timing: PASS in 81.49 s, `taskset -c 0-11`, `-MaxParallelActions=4`, no address-space limit. Three existing RealtimeMesh deprecation warnings. The only changed `Source/` file is `Source/VoxelForge/Private/VoxelWorld.cpp`; UBT compiled it and the VFCore amalgam. Log: `Saved/Linux/l79/build/ue-host-build-final.log`. + +## Failed runs + +- Three Unreal null-RHI moving-trace attempts exited during asset load at the 6 GiB address-space cap (`Ran out of memory allocating 5,096,960 bytes`). The permitted headless moving-camera traces supplied the comparison instead. +- The first headless movement diagnostic segfaulted after generator release because it updated desired tiles without the camera overload. The runner was corrected to pass the camera; all six final traces passed. +- The first fast-tier launch rejected a stale expected binary stamp. Clang/GCC binaries were rebuilt; the final 274-check run passed. +- The broad route-parity diagnostic also failed on both the L79 baseline and unmodified L78 reference: FI had 3/8 base/page mismatches and 5,500 exact-query mismatches. This is pre-existing cross-route diagnostic behavior; the same-route point-bound inspection completed, and the final fast tier passed. + +No commit, push, or stash was made. The branch remains `assembler/world` at `ec20994`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/RunL79HeadlessMovingCamera.py b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79HeadlessMovingCamera.py new file mode 100644 index 0000000..c262bef --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79HeadlessMovingCamera.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Measure streaming coverage while the standalone queue follows a moving camera.""" +from __future__ import annotations + +import csv +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l79/movement/headless" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARY = ROOT / "Saved/Linux/l79/bin/VoxelForgeCoreHeadless-l79-final" +CAMERA_M = (19.166015625, -14.2001953125, -753.3740234375) +POLICIES = {0: "L77", 1: "L78", 2: "L79"} +SPEEDS = {"walk": 1.4, "fast": 30.0} +SAMPLE = re.compile(r"^\[L79MovingSample\] (.*)$", re.M) +SUMMARY = re.compile(r"^\[L79MovingSummary\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") + + +def run(policy: int, speed_name: str) -> tuple[dict, list[dict]]: + label = f"{speed_name}-{POLICIES[policy]}" + log = OUT / f"{label}.log" + OUT.mkdir(parents=True, exist_ok=True) + camera = ",".join(f"{value:.12f}" for value in CAMERA_M) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARY), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", + f"--stream-scheduler-policy={policy}", + f"--moving-camera-speed-mps={SPEEDS[speed_name]:.1f}", + "--moving-camera-duration-s=12", "--moving-camera-dwell-s=5", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + samples = [dict(FIELDS.findall(match.group(1))) for match in SAMPLE.finditer(content)] + summary_match = SUMMARY.search(content) + engine_summary = dict(FIELDS.findall(summary_match.group(1))) if summary_match else {} + if result.returncode != 0 or not samples or not engine_summary: + raise SystemExit(f"moving-camera run failed or trace missing: {log}; exit={result.returncode}") + for row in samples: + for name in ("policy", "speed_mps", "t_s", "x_m", "near_ready_pct", "far_missing_pct", + "far_hole_pct", + "far_stale_mean_s", "far_stale_max_s", "generation_failures", "queue_update_ms"): + row[name] = float(row[name]) + row["label"] = label + if int(float(engine_summary.get("generation_failures", "-1"))) != 0: + raise SystemExit(f"tile generation failure: {log}") + run_info = { + "label": label, "policy": POLICIES[policy], "policy_id": policy, + "speed": speed_name, "speed_mps": SPEEDS[speed_name], + "exit_code": result.returncode, "elapsed_s": round(time.monotonic() - started, 3), + "samples": len(samples), "engine_summary": engine_summary, + "log": str(log.relative_to(ROOT)), + } + print("[L79HeadlessMoveRun] " + json.dumps(run_info, sort_keys=True), flush=True) + return run_info, samples + + +def main() -> int: + if not DATA.is_file() or not BINARY.is_file(): + raise SystemExit(f"missing input: {DATA} or {BINARY}") + results: list[dict] = [] + samples: list[dict] = [] + schedule = [(policy, speed) for speed in ("walk", "fast") + for policy in (0, 1, 2)] + for policy, speed in schedule: + result, rows = run(policy, speed) + results.append(result) + samples.extend(rows) + (OUT / "runs-partial.json").write_text( + json.dumps(results, indent=2) + "\n", encoding="utf-8") + summary = [] + for speed_name, speed in SPEEDS.items(): + for policy in POLICIES: + rows = [row for row in samples + if row["speed_mps"] == speed and row["policy"] == policy] + moving = [row for row in rows if row.get("phase") == "move"] + dwell = [row for row in rows if row.get("phase") == "dwell"] + summary.append({ + "speed": speed_name, "speed_mps": speed, "policy": POLICIES[policy], + "moving_samples": len(moving), "dwell_samples": len(dwell), + "far_missing_moving_mean_pct": round( + sum(row["far_missing_pct"] for row in moving) / max(1, len(moving)), 3), + "far_missing_moving_max_pct": max( + (row["far_missing_pct"] for row in moving), default=None), + "far_missing_dwell_last_pct": dwell[-1]["far_missing_pct"] if dwell else None, + "far_hole_moving_mean_pct": round( + sum(row["far_hole_pct"] for row in moving) / max(1, len(moving)), 3), + "far_hole_moving_max_pct": max( + (row["far_hole_pct"] for row in moving), default=None), + "far_hole_dwell_last_pct": dwell[-1]["far_hole_pct"] if dwell else None, + "far_stale_moving_max_s": max( + (row["far_stale_max_s"] for row in moving), default=None), + "far_stale_dwell_max_s": max( + (row["far_stale_max_s"] for row in dwell), default=None), + "near_ready_moving_mean_pct": round( + sum(row["near_ready_pct"] for row in moving) / max(1, len(moving)), 3), + "near_ready_moving_min_pct": min( + (row["near_ready_pct"] for row in moving), default=None), + "near_ready_dwell_last_pct": dwell[-1]["near_ready_pct"] if dwell else None, + "queue_update_max_ms": max((row["queue_update_ms"] for row in rows), default=None), + }) + (OUT / "summary.json").write_text( + json.dumps({"runs": results, "summary": summary}, indent=2) + "\n", encoding="utf-8") + if samples: + with (OUT / "samples.csv").open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=list(samples[0])) + writer.writeheader() + writer.writerows(samples) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/RunL79MovingCamera.py b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79MovingCamera.py new file mode 100644 index 0000000..eae8b35 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79MovingCamera.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Compare L77, L78, and L79 production streaming orders while the pawn moves.""" +from __future__ import annotations + +import csv +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +HOST = Path("/home/jahni/VFHost") +PLUGIN_SAVED = ROOT / "Saved/Linux/l79" +HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l79" +HOST_SAVED = HOST / "Saved/Linux/l79" +EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd") +SPAWN_M = (19.166015625, -14.2001953125, -753.3740234375) # owner TunnelNetwork +SPEEDS = {"walk": 140.0, "fast": 3000.0} # cm/s: 1.4 m/s and 30 m/s +POLICY_NAMES = {0: "L77", 1: "L78", 2: "L79"} +TRACE_DIR = PLUGIN_SAVED / "movement/traces" +MOVE_START_S = 6.0 +EXIT_S = 18.0 + + +def parse_trace(path: Path, run: dict) -> tuple[list[dict], dict]: + events = [] + if path.is_file(): + for line in path.read_text(encoding="utf-8", errors="replace").splitlines(): + try: + events.append(json.loads(line)) + except json.JSONDecodeError: + pass + samples = [event for event in events if event.get("type") == "streaming_sample" + and float(event.get("speed_cm_s", 0.0)) >= 50.0] + scheduler = [event for event in events if event.get("type") == "scheduler_admission" + and float(event.get("moving", 0)) > 0] + rows = [] + for event in samples: + desired = event.get("desired_by_lod", [0] * 9) + resident = event.get("resident_by_lod", [0] * 9) + pending = event.get("pending_by_lod", [0] * 9) + far_desired = sum(desired[3:]) + far_resident = sum(resident[3:]) + near_desired = sum(desired[:3]) + near_resident = sum(resident[:3]) + rows.append({ + "run": run["label"], "policy": run["policy_name"], "speed": run["speed"], + "t_s": event.get("t_s"), "position_cm": event.get("position_cm"), + "speed_cm_s": event.get("speed_cm_s"), + "far_desired": far_desired, "far_resident": far_resident, + "far_pending": sum(pending[3:]), + "far_missing_pct": round(100.0 * (far_desired - far_resident) + / max(1, far_desired), 3), + "near_desired": near_desired, "near_resident": near_resident, + "near_ready_pct": round(100.0 * near_resident / max(1, near_desired), 3), + "walkable_ready": event.get("walkable_ready"), + **{f"lod{i}_desired": desired[i] for i in range(9)}, + **{f"lod{i}_resident": resident[i] for i in range(9)}, + }) + submitted_far = sum(sum(event.get("submitted_by_lod", [0] * 9)[3:]) + for event in scheduler) + submitted_near = sum(sum(event.get("submitted_by_lod", [0] * 9)[:3]) + for event in scheduler) + far_missing = [row["far_missing_pct"] for row in rows] + near_ready = [row["near_ready_pct"] for row in rows] + abslog_lines = (run["abslog_path"].read_text(encoding="utf-8", errors="replace").splitlines() + if run["abslog_path"].is_file() else []) + summary = { + "moving_samples": len(rows), + "moving_duration_s": round(max((float(row["t_s"]) for row in rows), default=0.0) + - min((float(row["t_s"]) for row in rows), default=0.0), 3), + "far_missing_pct_mean": round(sum(far_missing) / max(1, len(far_missing)), 3), + "far_missing_pct_max": max(far_missing, default=None), + "far_missing_pct_first": far_missing[0] if far_missing else None, + "far_missing_pct_last": far_missing[-1] if far_missing else None, + "near_ready_pct_mean": round(sum(near_ready) / max(1, len(near_ready)), 3), + "near_ready_pct_min": min(near_ready, default=None), + "near_ready_pct_last": near_ready[-1] if near_ready else None, + "walkable_ready_samples": sum(row["walkable_ready"] == 1 for row in rows), + "submitted_near_tiles": submitted_near, + "submitted_far_tiles": submitted_far, + "lod0_ring_ready_events": sum(event.get("type") == "lod0_ring_ready" for event in events), + "visual_gap_result": next((line.split("result=")[1].split()[0] + for line in abslog_lines + if "[VoxelForgeVisualGapAudit] result=" in line), "not-run"), + "visual_staleness_result": next((line.split("result=")[1].split()[0] + for line in abslog_lines + if "[VoxelForgeVisualStaleness] result=" in line), "not-run"), + } + return rows, summary + + +def run(policy: int, speed_name: str, replicate: int, audit: bool = False) -> tuple[dict, list[dict]]: + label = f"{speed_name}-{POLICY_NAMES[policy]}-r{replicate}" + ("-audit" if audit else "") + trace = TRACE_DIR / f"{label}.trace.jsonl" + abslog = TRACE_DIR / f"{label}.log" + stdout = TRACE_DIR / 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 (TRACE_DIR, output, userdir, zendata, ddc): + directory.mkdir(parents=True, exist_ok=True) + spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWN_M) + 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", "-slot=0", "-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", "-voxel.TestMove=1", + f"-voxel.TestMoveSpeedCmPerSecond={SPEEDS[speed_name]:.1f}", + f"-voxel.TestMoveStartSeconds={MOVE_START_S:.1f}", + f"-voxel.TestExitSeconds={EXIT_S:.1f}", "-voxel.TestMoveDirection=1,0,0", + f"-voxel.StreamingSchedulerPolicy={policy}", "-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}", + ] + if audit: + command.extend(("-voxel.TestVisualStaleness=2", "-voxel.TestVisualGapAudit=1")) + wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "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) + run_info = { + "label": label, "policy": policy, "policy_name": POLICY_NAMES[policy], + "speed": speed_name, "speed_cm_s": SPEEDS[speed_name], "replicate": replicate, + "audit": audit, "exit_code": result.returncode, + "elapsed_s": round(time.monotonic() - started, 3), + "trace": str(trace.relative_to(ROOT)), "abslog": str(abslog.relative_to(ROOT)), + "stdout": str(stdout.relative_to(ROOT)), "abslog_path": abslog, + } + samples, summary = parse_trace(trace, run_info) + run_info.update(summary) + del run_info["abslog_path"] + print("[L79MoveRun] " + json.dumps(run_info, sort_keys=True), flush=True) + return run_info, samples + + +def main() -> int: + if not EDITOR.is_file(): + raise SystemExit(f"missing Unreal commandlet executable: {EDITOR}") + TRACE_DIR.mkdir(parents=True, exist_ok=True) + results: list[dict] = [] + sample_rows: list[dict] = [] + schedule = [] + for replicate, policies in ((1, (0, 1, 2)), (2, (2, 1, 0))): + speeds = ("walk", "fast") if replicate == 1 else ("fast", "walk") + for policy in policies: + for speed in speeds: + schedule.append((policy, speed, replicate, False)) + schedule.append((2, "fast", 3, True)) + for policy, speed, replicate, audit in schedule: + row, samples = run(policy, speed, replicate, audit) + results.append(row) + sample_rows.extend(samples) + (PLUGIN_SAVED / "movement/runs-partial.json").write_text( + json.dumps(results, indent=2) + "\n", encoding="utf-8") + (PLUGIN_SAVED / "movement/summary.json").write_text( + json.dumps({"runs": results}, indent=2) + "\n", encoding="utf-8") + sample_path = PLUGIN_SAVED / "movement/samples.csv" + if sample_rows: + with sample_path.open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=list(sample_rows[0])) + writer.writeheader() + writer.writerows(sample_rows) + return 0 if all(row["exit_code"] == 0 and row["moving_samples"] > 0 + for row in results) else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/RunL79TimingPairs.py b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79TimingPairs.py new file mode 100644 index 0000000..5338446 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux79/RunL79TimingPairs.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Interleave L78/L79 owner-world readiness timings for all four strates.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l79/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L78": ROOT / "Saved/Linux/l78/bin/VoxelForgeCoreHeadless-l78-clang-profile", + "L79": ROOT / "Saved/Linux/l79/bin/VoxelForgeCoreHeadless-l79-clang-profile", +} +CAMERAS = { + 0: (8.0, 16.0, -372.2626953125), + 1: (25.8369140625, 3.19921875, -737.0), + 2: (19.166015625, -14.2001953125, -753.3740234375), + 3: (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +LOD_READY = re.compile( + r"^\[WorldAssemblerL77LODReady\] lod=(\d+) tiles=(\d+) cold_ready_s=([0-9.]+)$", + re.M, +) +NEAR_READY = re.compile( + r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$", + re.M, +) +ORDERS = (("L78", "L79"), ("L79", "L78"), ("L78", "L79")) + + +def run(strate: int, pair: int, mode: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"strate{strate}-pair{pair}-{mode}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[mode]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-lod-work", "--linux77-profile", + ] + started = time.monotonic() + with log.open("w", encoding="utf-8") as output: + result = subprocess.run(command, cwd=ROOT, stdout=output, + stderr=subprocess.STDOUT, check=False, timeout=1740) + content = log.read_text(encoding="utf-8", errors="replace") + pipeline_match = PIPELINE.search(content) + ready = {int(lod): {"tiles": int(tiles), "seconds": float(seconds)} + for lod, tiles, seconds in LOD_READY.findall(content)} + near_match = NEAR_READY.search(content) + if (result.returncode != 0 or pipeline_match is None or near_match is None + or any(lod not in ready for lod in (0, 1, 2))): + raise SystemExit(f"readiness run failed or data missing: {log}; exit={result.returncode}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, + "pair": pair, + "mode": mode, + "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "lod02_ready_s": float(near_match.group(3)), + "lod_ready_s": {str(lod): ready[lod]["seconds"] for lod in sorted(ready)}, + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": int(pipeline["whole_tiles"].split("/")[0]), + "sampler": next((line for line in content.splitlines() + if line.startswith("[L77Sampler] action=stop")), None), + "log": str(log.relative_to(ROOT)), + } + print("[L79TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + missing = [str(path) for path in (DATA, *BINARIES.values()) if not path.is_file()] + if missing: + raise SystemExit("missing input: " + ", ".join(missing)) + rows = [] + for strate in range(4): + for pair, order in enumerate(ORDERS, 1): + paired = {} + for mode in order: + row = run(strate, pair, mode) + rows.append(row) + paired[mode] = row + (OUT / "runs-partial.json").write_text( + json.dumps(rows, indent=2) + "\n", encoding="utf-8") + print("[L79TimingPair] " + json.dumps({ + "strate": strate, "pair": pair, + "L78_lod02_s": paired["L78"]["lod02_ready_s"], + "L79_lod02_s": paired["L79"]["lod02_ready_s"], + "L78_whole_s": paired["L78"]["whole_view_ready_s"], + "L79_whole_s": paired["L79"]["whole_view_ready_s"], + }, sort_keys=True), flush=True) + summary = [] + for strate in range(4): + group = [row for row in rows if row["strate"] == strate] + summary.append({ + "strate": strate, + "pairs": 3, + "L78_lod02_s": [row["lod02_ready_s"] for row in group if row["mode"] == "L78"], + "L79_lod02_s": [row["lod02_ready_s"] for row in group if row["mode"] == "L79"], + "L78_lod02_median_s": statistics.median( + row["lod02_ready_s"] for row in group if row["mode"] == "L78"), + "L79_lod02_median_s": statistics.median( + row["lod02_ready_s"] for row in group if row["mode"] == "L79"), + "L78_whole_s": [row["whole_view_ready_s"] for row in group if row["mode"] == "L78"], + "L79_whole_s": [row["whole_view_ready_s"] for row in group if row["mode"] == "L79"], + "L78_whole_median_s": statistics.median( + row["whole_view_ready_s"] for row in group if row["mode"] == "L78"), + "L79_whole_median_s": statistics.median( + row["whole_view_ready_s"] for row in group if row["mode"] == "L79"), + }) + (OUT / "summary.json").write_text( + json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79HeadlessMovingCamera.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79HeadlessMovingCamera.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..24b217c626517df9a2352e720d9a1289263747b5 GIT binary patch literal 9475 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79MovingCamera.cpython-313.pyc b/Standalone/Research/WorldAssemblerM3/Linux79/__pycache__/RunL79MovingCamera.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f209d3c5ebe92cfd8de50e0667b3a02b45185540 GIT binary patch literal 15249 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 G(arg.size()),arg.data());return false;} options.HasCameraStep=true; } + else if(arg.starts_with("--stream-scheduler-policy=")) + { + if(!readInt("--stream-scheduler-policy=",options.StreamingSchedulerPolicy) + ||options.StreamingSchedulerPolicy<0||options.StreamingSchedulerPolicy>2) + {std::fprintf(stderr,"Invalid --stream-scheduler-policy (expected 0..2).\n");return false;} + } + else if(arg.starts_with("--moving-camera-speed-mps=")) + { + const std::string value(arg.substr(std::string_view("--moving-camera-speed-mps=").size())); + char* end=nullptr;options.MovingCameraSpeedMetersPerSecond=std::strtod(value.c_str(),&end); + if(end==value.c_str()||*end!='\0'||!std::isfinite(options.MovingCameraSpeedMetersPerSecond) + ||options.MovingCameraSpeedMetersPerSecond<=0.0) + {std::fprintf(stderr,"Invalid --moving-camera-speed-mps (expected positive finite value).\n");return false;} + options.MovingCameraTrace=true; + } + else if(arg.starts_with("--moving-camera-duration-s=")) + { + const std::string value(arg.substr(std::string_view("--moving-camera-duration-s=").size())); + char* end=nullptr;options.MovingCameraDurationSeconds=std::strtod(value.c_str(),&end); + if(end==value.c_str()||*end!='\0'||!std::isfinite(options.MovingCameraDurationSeconds) + ||options.MovingCameraDurationSeconds<=0.0) + {std::fprintf(stderr,"Invalid --moving-camera-duration-s (expected positive finite value).\n");return false;} + } + else if(arg.starts_with("--moving-camera-dwell-s=")) + { + const std::string value(arg.substr(std::string_view("--moving-camera-dwell-s=").size())); + char* end=nullptr;options.MovingCameraDwellSeconds=std::strtod(value.c_str(),&end); + if(end==value.c_str()||*end!='\0'||!std::isfinite(options.MovingCameraDwellSeconds) + ||options.MovingCameraDwellSeconds<0.0) + {std::fprintf(stderr,"Invalid --moving-camera-dwell-s (expected nonnegative finite value).\n");return false;} + } else if(arg.starts_with("--column-cache=")) { const std::string_view mode=arg.substr(std::string_view("--column-cache=").size()); @@ -1635,6 +1673,10 @@ namespace {std::fprintf(stderr,"--tls-probe-touch requires --tls-probe=N\n");return false;} if(options.RegionCrossing&&!options.HasCameraStep) {std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;} + if(options.MovingCameraTrace&&(!options.StreamPipeline||!options.WorldAssemblerPrototype)) + {std::fprintf(stderr,"--moving-camera-speed-mps requires --world-assembler-prototype --stream-pipeline\n");return false;} + if(options.MovingCameraTrace&&options.StreamingSchedulerPolicy<0) + {std::fprintf(stderr,"--moving-camera-speed-mps requires --stream-scheduler-policy=0|1|2\n");return false;} if(options.LandmarkL45RampControl&&!options.LandmarkChamberDemo) {std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;} if(options.AuditOreVeinYield&&!options.OreVeinsDemo) @@ -2228,5 +2270,8 @@ int main(int argc,char** argv) ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe, options.AuditNativeLattice,options.LatticeOrphanControl, options.CaptureSkipWalkability,options.ProfilePassageComposer, - options.StreamHashAllowUniformSkips,options.Linux77Profile); + options.StreamHashAllowUniformSkips,options.Linux77Profile, + options.StreamingSchedulerPolicy,options.MovingCameraTrace, + options.MovingCameraSpeedMetersPerSecond,options.MovingCameraDurationSeconds, + options.MovingCameraDwellSeconds); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 01b2e03..bc2f745 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1874,7 +1874,9 @@ namespace StandaloneStreamBenchmark bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false, bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false, bool bProfilePassageComposer=false,bool bAllowUniformSkipsWhenHashing=false, - bool bLinux77Profile=false) + bool bLinux77Profile=false,int32 SchedulerPolicy=-1, + bool bMovingCameraTrace=false,double MovingCameraSpeedMetersPerSecond=0.0, + double MovingCameraDurationSeconds=12.0,double MovingCameraDwellSeconds=5.0) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -2038,6 +2040,7 @@ namespace StandaloneStreamBenchmark bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache, bAllowUniformSkipsWhenHashing); + Streamer.SetSchedulerPolicy(SchedulerPolicy); Streamer.SetUniformBlockProofAuditEnabled(bAuditUniformBlockProofs); Streamer.SetTileDrainInstrumentationEnabled(bInstrumentTileDrain); Streamer.SetLinux77ProfileEnabled(bLinux77Profile); @@ -4975,6 +4978,152 @@ namespace StandaloneStreamBenchmark std::fprintf(stderr,"[StreamPipeline] expected %zu tiles after shape publication but queued %d.\n", Desired.Tiles.size(),Update.DesiredTiles);return 7; } + if(bMovingCameraTrace) + { + constexpr double SamplePeriodSeconds=0.25; + using FTileSet=std::unordered_set; + FTileSet Resident; + std::unordered_map FarMissingSince; + const double VoxelMetres=static_cast(World.CoreVoxelSizeMicrons)/1000000.0; + const auto TotalSeconds=MovingCameraDurationSeconds+MovingCameraDwellSeconds; + const auto TraceStart=Clock::now(); + int32 SampleIndex=0; + int32 GenerationFailures=0; + double FirstAllFarReadySeconds=-1.0; + int32 Samples=0; + std::printf("[L79MovingCheckpoint] stage=start desired=%zu policy=%d speed_mps=%.3f\n", + Desired.Tiles.size(),SchedulerPolicy,MovingCameraSpeedMetersPerSecond); + std::fflush(stdout); + auto DrainReady=[&]() + { + std::vector Ready=Streamer.TakeReady(); + for(const FStandaloneCpuTile& Tile:Ready) + { + if(Tile.bGenerationFailed)++GenerationFailures; + else Resident.insert(Tile.Key); + } + }; + for(;;++SampleIndex) + { + const double ScheduledSeconds=std::min(TotalSeconds, + static_cast(SampleIndex)*SamplePeriodSeconds); + std::this_thread::sleep_until(TraceStart+std::chrono::duration(ScheduledSeconds)); + const auto SampleAt=Clock::now(); + const double ElapsedSeconds=std::min(TotalSeconds, + std::chrono::duration(SampleAt-TraceStart).count()); + const double MovingSeconds=std::min(MovingCameraDurationSeconds,ElapsedSeconds); + FVector Camera=CameraPositionVoxels; + Camera.X+=static_cast(MovingCameraSpeedMetersPerSecond*MovingSeconds/VoxelMetres); + DrainReady(); + if(SampleIndex==0) + { + std::printf("[L79MovingCheckpoint] stage=drained resident=%zu failures=%d\n", + Resident.size(),GenerationFailures); + std::fflush(stdout); + } + FStandaloneDesiredTileSet CurrentDesired=Desired; + CurrentDesired.CenterChunk=FIntVector( + static_cast(std::floor(Camera.X/CHUNK_SIZE)), + static_cast(std::floor(Camera.Y/CHUNK_SIZE)), + static_cast(std::floor(Camera.Z/CHUNK_SIZE))); + const auto CoreDesired=Shape.desiredTiles({Camera.X,Camera.Y,Camera.Z}); + CurrentDesired.Tiles.clear();CurrentDesired.Tiles.reserve(CoreDesired.size()); + for(const auto& Key:CoreDesired) + { + if(MaxLevel>=0&&Key.lod>MaxLevel)continue; + CurrentDesired.Tiles.emplace_back( + FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); + } + const auto BeforeUpdate=Clock::now(); + const std::vector ResidentKeys(Resident.begin(),Resident.end()); + const FStandaloneStreamingUpdate MovingUpdate=Streamer.UpdateDesiredWithSet( + CurrentDesired,ResidentKeys,static_cast(SampleIndex),Camera); + if(SampleIndex==0) + { + std::printf("[L79MovingCheckpoint] stage=updated desired=%d new_jobs=%d\n", + MovingUpdate.DesiredTiles,MovingUpdate.NewJobs); + std::fflush(stdout); + } + const double UpdateMilliseconds=std::chrono::duration( + Clock::now()-BeforeUpdate).count(); + if(MovingUpdate.DesiredTiles!=static_cast(CurrentDesired.Tiles.size())) + { + Streamer.Stop(); + std::fprintf(stderr,"[MovingCameraTrace] desired-set mismatch expected=%zu actual=%d\n", + CurrentDesired.Tiles.size(),MovingUpdate.DesiredTiles); + return 9; + } + std::size_t NearDesired=0,NearResident=0,FarDesired=0,FarResident=0,FarCovered=0; + double FarStaleAgeSum=0.0,FarStaleAgeMax=0.0; + const auto FloorDiv=[](int32 Value,int32 Divisor) + { + const int32 Quotient=Value/Divisor, Remainder=Value%Divisor; + return Quotient-((Remainder!=0&&Remainder<0)?1:0); + }; + std::unordered_map CurrentFarMissing; + for(const FVoxelTileKey& Key:CurrentDesired.Tiles) + { + const bool bResident=Resident.contains(Key); + if(Key.Level<=2) + { + ++NearDesired; + NearResident+=bResident?1u:0u; + } + else + { + ++FarDesired; + FarResident+=bResident?1u:0u; + bool bFarCovered=bResident; + for(int32 ParentLOD=Key.Level+1; + !bFarCovered&&ParentLOD<=World.MaxClipLevel;++ParentLOD) + { + const int32 Scale=1<<(ParentLOD-Key.Level); + const FVoxelTileKey Parent(FIntVector( + FloorDiv(Key.Coord.X,Scale),FloorDiv(Key.Coord.Y,Scale), + FloorDiv(Key.Coord.Z,Scale)),ParentLOD); + bFarCovered=Resident.contains(Parent); + } + FarCovered+=bFarCovered?1u:0u; + if(!bResident) + { + auto [It,bInserted]=FarMissingSince.try_emplace(Key,SampleAt); + (void)bInserted; + const double Age=std::chrono::duration(SampleAt-It->second).count(); + FarStaleAgeSum+=Age; + FarStaleAgeMax=std::max(FarStaleAgeMax,Age); + CurrentFarMissing.emplace(Key,It->second); + } + } + } + FarMissingSince.swap(CurrentFarMissing); + const double NearReadyPercent=100.0*static_cast(NearResident) + /static_cast(std::max(1,NearDesired)); + const double FarMissingPercent=100.0*static_cast(FarDesired-FarResident) + /static_cast(std::max(1,FarDesired)); + const double FarHolePercent=100.0*static_cast(FarDesired-FarCovered) + /static_cast(std::max(1,FarDesired)); + const double FarStaleAgeMean=FarDesired>FarResident + ?FarStaleAgeSum/static_cast(FarDesired-FarResident):0.0; + if(FarDesired>0&&FarDesired==FarResident&&FirstAllFarReadySeconds<0.0) + FirstAllFarReadySeconds=ElapsedSeconds; + const char* Phase=ElapsedSeconds=TotalSeconds)break; + } + std::printf("[L79MovingSummary] policy=%d speed_mps=%.3f move_s=%.3f dwell_s=%.3f samples=%d resident_tiles=%zu first_all_far_ready_s=%.3f generation_failures=%d priority=background_workers_only status=%s\n", + SchedulerPolicy,MovingCameraSpeedMetersPerSecond,MovingCameraDurationSeconds, + MovingCameraDwellSeconds,Samples,Resident.size(),FirstAllFarReadySeconds, + GenerationFailures,GenerationFailures==0?"PASS":"FAIL"); + Streamer.Stop(); + return GenerationFailures==0?0:9; + } FSceneExportWriter SceneExport; const bool bExportRequested=!MeshExportPath.empty(); bool bSceneExportPassing=true; diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index 1953fad..357d4ee 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -551,6 +551,9 @@ public: bUseTileFilter=true; } + void SetSchedulerPolicy(int32 Policy) + {SchedulerPolicy=std::clamp(Policy,-1,2);} + bool QueuePriorityTasks(std::vector> Tasks) { if(Tasks.empty())return true; @@ -623,6 +626,29 @@ public: return UpdateDesiredSet(std::move(NewDesired),ResidentKeys,CrossingIndex); } + FStandaloneStreamingUpdate UpdateDesiredWithSet( + FStandaloneDesiredTileSet NewDesired,const std::vector& ResidentKeys, + uint64 CrossingIndex,const FVector& CameraPositionVoxels) + { + if(bHasLastCameraPosition) + { + const FVector Delta=CameraPositionVoxels-LastCameraPositionVoxels; + const double LengthSquared=static_cast(Delta.X)*Delta.X + +static_cast(Delta.Y)*Delta.Y; + if(LengthSquared>1.0e-12) + { + const double InverseLength=1.0/std::sqrt(LengthSquared); + TravelDirection=FVector(static_cast(Delta.X*InverseLength), + static_cast(Delta.Y*InverseLength),0.0f); + } + else TravelDirection=FVector::ZeroVector; + } + else TravelDirection=FVector::ZeroVector; + LastCameraPositionVoxels=CameraPositionVoxels; + bHasLastCameraPosition=true; + return UpdateDesiredSet(std::move(NewDesired),ResidentKeys,CrossingIndex); + } + std::vector TakeReady() { std::vector Out; @@ -774,15 +800,18 @@ private: for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair); std::vector Prioritized=NewDesired.Tiles; - if(IsCoarseFirstSchedulingEnabled()) + const int32 ActiveSchedulerPolicy=SchedulerPolicy<0 + ?(IsCoarseFirstSchedulingEnabled()?2:3):SchedulerPolicy; + if(ActiveSchedulerPolicy<=2) { - // Finish the local walkable ring, then all near LODs, before admitting far work. - // Terminer l'anneau praticable, puis tous les LOD proches avant le champ lointain. + // Policy 0 retains L77 coarse-first ordering after the local support ring. + // Policies 1 and 2 prioritize all near LODs before horizon work. auto Tier=[&](const FVoxelTileKey& Key) { if(Key.Level==0&&std::abs(Key.Coord.X-NewDesired.CenterChunk.X)<=1 &&std::abs(Key.Coord.Y-NewDesired.CenterChunk.Y)<=1 &&std::abs(Key.Coord.Z-NewDesired.CenterChunk.Z)<=1)return 0; + if(ActiveSchedulerPolicy==0)return Key.Level>=3?1:2; return Key.Level<=2?1:2; }; auto IsAhead=[&](const FVoxelTileKey& Key) @@ -798,8 +827,10 @@ private: { const int32 TierA=Tier(A),TierB=Tier(B); if(TierA!=TierB)return TierAB.Level; + if(TierA==1&&A.Level!=B.Level) + return ActiveSchedulerPolicy==0?A.Level>B.Level:A.LevelB.Level; const bool bAheadA=IsAhead(A),bAheadB=IsAhead(B); if(bAheadA!=bAheadB)return bAheadA; const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A,NewDesired.CenterChunk); @@ -810,6 +841,29 @@ private: if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y NearTiles,FarTiles,FairOrder; + NearTiles.reserve(Prioritized.size()); + FarTiles.reserve(Prioritized.size()); + FairOrder.reserve(Prioritized.size()); + for(const FVoxelTileKey& Key:Prioritized) + (Key.Level<=2?NearTiles:FarTiles).push_back(Key); + constexpr size_t NearBurstAndShareStride=16; + size_t NearIndex=0,FarIndex=0; + while(NearIndex Lock(Mutex); - Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty() - ||(!Queue.empty()&&(NearOutstandingJobs==0||Queue.front().Key.Level<=2));}); + const auto HasEligibleTile=[&]() + { + return !Queue.empty()&&(SchedulerPolicy!=1||NearOutstandingJobs==0 + ||Queue.front().Key.Level<=2); + }; + Wake.wait(Lock,[&](){return bStop||!PriorityTasks.empty()||HasEligibleTile();}); if(bStop)return; - const bool bNearTileEligible=!Queue.empty() - &&(NearOutstandingJobs==0||Queue.front().Key.Level<=2); + const bool bTileEligible=HasEligibleTile(); const bool bPreferPriority=!PriorityTasks.empty() - &&(!bNearTileEligible||ConsecutivePriorityTasks