Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Linux123/RunL123Pairs.py
T
Fr0zkaandClaude Opus 5.5 5e7e0eb1c1 research: region completion gate (L123, no production change)
The 18 ms gate is one game-thread tick, not compile cost (0.25 ms). MPSC handoff keeps ownership but still
polls per tick; velocity look-ahead didn't help (near -0.8 pt, p99 11.77 > 11.00). Both kept as a patch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
2026-10-30 09:55:45 +01:00

263 lines
14 KiB
Python

#!/usr/bin/env python3
"""Five interleaved 30 m/s UE pairs for the Core region lookahead experiment."""
from __future__ import annotations
import argparse
import json
import math
import statistics
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[4]
HOST = Path("/home/jahni/VFHost")
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
SPAWN_TN_M = (19.166015625, -14.2001953125, -753.3740234375)
SPAWN_FI_M = (25.8369140625, 3.19921875, -737.0)
def percentile(values: list[float], fraction: float) -> float | None:
if not values:
return None
ordered = sorted(values)
return ordered[max(0, min(len(ordered) - 1, math.ceil(len(ordered) * fraction) - 1))]
def read_events(path: Path) -> list[dict]:
rows = []
if path.is_file():
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if not line.startswith("{"):
continue
try:
rows.append(json.loads(line))
except json.JSONDecodeError:
pass
return rows
def run(scenario: str, condition: str, repetition: int, turn: bool = False) -> dict:
lookahead = int(condition in ("lookahead", "turn_lookahead"))
if scenario == "movement":
speed, start, duration, exit_s, slot, spawn = (
3000.0, 6.0, 12.0, 23.0, 0, SPAWN_TN_M)
else:
speed, start, duration, exit_s, slot, spawn = (
140.0, 20.0, 12.0, 37.0, 1, SPAWN_FI_M)
label = f"{scenario}-{condition}-r{repetition}"
output_group = "turn" if turn else scenario
out = ROOT / "Saved/Linux/l123" / output_group / condition / "unreal"
host_out = HOST / "Plugins/VoxelForge/Saved/Linux/l123" / output_group / condition
host_saved = HOST / "Saved/Linux/l123"
trace = out / f"{label}.trace.jsonl"
abslog = out / f"{label}.log"
stdout = out / f"{label}.stdout.log"
export = host_out / "output" / label
userdir = host_saved / "user" / label
zendata = host_saved / "zen" / label
ddc = host_saved / "DDC"
for path in (out, export, userdir, zendata, ddc):
path.mkdir(parents=True, exist_ok=True)
trace.unlink(missing_ok=True)
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in spawn)
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={export}", "-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.StreamingFarApplySupplement=1",
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
f"-voxel.CoreRegionLookahead={lookahead}", "-voxel.TestMove=1",
f"-voxel.TestMoveSpeedCmPerSecond={speed:.1f}",
f"-voxel.TestMoveStartSeconds={start:.1f}",
f"-voxel.TestMoveDurationSeconds={duration:.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}",
]
if turn:
command.extend(["-voxel.TestMoveTurnAfterSeconds=6.0",
"-voxel.TestMoveTurnDirection=0,1,0"])
started = time.monotonic()
wrapped = ["taskset", "-c", "0-11", "timeout", "1740s", *command]
with stdout.open("w", encoding="utf-8") as stream:
process = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
stderr=subprocess.STDOUT, check=False, timeout=1740)
events = read_events(trace)
samples = [event for event in events if event.get("type") == "streaming_sample"]
moving = [event for event in samples if float(event.get("speed_cm_s", 0.0)) >= 50.0]
stop_event = next((event for event in events
if event.get("type") == "streaming_test_move_stopped"), {})
stop_s = float(stop_event.get("t_s", 0.0))
move_start = min((float(event.get("t_s", 0.0)) for event in moving), default=float("inf"))
dwell = [event for event in samples if float(event.get("t_s", 0.0)) >= stop_s
and float(event.get("speed_cm_s", 0.0)) < 50.0]
backend = next((event for event in events if event.get("type") == "worker_backend_config"), {})
cap = next((event for event in events if event.get("type") == "streaming_worker_cap_readback"), {})
if process.returncode or len(moving) < 40 or len(dwell) < 16:
raise RuntimeError(f"incomplete UE run {label}: exit={process.returncode}, "
f"move={len(moving)}, dwell={len(dwell)}, trace={trace}")
if (int(backend.get("core_region_lookahead", -1)) != lookahead
or int(backend.get("streaming_far_apply_supplement", -1)) != 1
or int(cap.get("streaming_background_worker_cap", -1)) != 1
or int(backend.get("effective_tile_slots", -1)) != 8):
raise RuntimeError(f"configuration readback mismatch: {backend}; {cap}")
moving_rows = []
for event in moving:
desired = event.get("desired_by_lod", [0] * 9)
resident = event.get("resident_by_lod", [0] * 9)
covered = event.get("covered_by_lod", resident)
moving_rows.append({
"near_ready_pct": 100.0 * sum(resident[:3]) / max(1, sum(desired[:3])),
"lod0_ready_pct": 100.0 * resident[0] / max(1, desired[0]),
"lod1_ready_pct": 100.0 * resident[1] / max(1, desired[1]),
"lod2_ready_pct": 100.0 * resident[2] / max(1, desired[2]),
"far_hole_pct": 100.0 * (sum(desired[3:]) - sum(covered[3:]))
/ max(1, sum(desired[3:])),
})
work = [event for event in events if event.get("type") == "game_thread_work_frame"
and move_start <= float(event.get("t_s", 0.0)) < stop_s]
ticks = [float(event.get("tick_ms", 0.0)) for event in work]
updates = [float(event.get("update_chunks_ms", 0.0)) for event in work]
arrival = [event for event in samples if float(event.get("t_s", 0.0)) < start
and float(event.get("speed_cm_s", 0.0)) < 50.0]
def settled(event: dict, end: int) -> bool:
desired = event.get("desired_by_lod", [0] * 9)
actual = event.get("settled_by_lod", [0] * 9)
return sum(desired[:end]) > 0 and sum(actual[:end]) >= sum(desired[:end])
def far_holes(event: dict) -> float | None:
desired = event.get("desired_by_lod", [0] * 9)[3:]
covered = event.get("covered_by_lod", [0] * 9)[3:]
return 100.0 * (sum(desired) - sum(covered)) / sum(desired) if sum(desired) else None
near_settled = next((event for event in arrival if settled(event, 3)), None)
whole_settled = next((event for event in arrival if settled(event, 9)), None)
far_clear = next((event for event in arrival if far_holes(event) == 0.0), None)
compile_done = [event for event in events if event.get("type") == "core_region_compile_complete"
and move_start <= float(event.get("t_s", 0.0)) < stop_s]
compile_requests = [event for event in events
if event.get("type") == "core_region_compile_request"
and move_start <= float(event.get("t_s", 0.0)) < stop_s]
lookahead_turns = [event for event in events
if event.get("type") == "core_region_lookahead_turn"]
row = {
"scenario": scenario, "condition": condition, "repetition": repetition,
"speed_mps": speed / 100.0, "core_region_lookahead": lookahead,
"exit_code": process.returncode, "wall_s": round(time.monotonic() - started, 3),
"moving_samples": len(moving), "dwell_samples": len(dwell),
"near_ready_moving_mean_pct": statistics.mean(r["near_ready_pct"] for r in moving_rows),
"lod0_ready_moving_mean_pct": statistics.mean(r["lod0_ready_pct"] for r in moving_rows),
"lod1_ready_moving_mean_pct": statistics.mean(r["lod1_ready_pct"] for r in moving_rows),
"lod2_ready_moving_mean_pct": statistics.mean(r["lod2_ready_pct"] for r in moving_rows),
"far_holes_moving_mean_pct": statistics.mean(r["far_hole_pct"] for r in moving_rows),
"game_thread_tick_p99_ms": percentile(ticks, 0.99),
"game_thread_tick_max_ms": max(ticks, default=None),
"stream_update_p99_ms": percentile(updates, 0.99),
"stream_update_max_ms": max(updates, default=None),
"lod02_settled_t_s": float(near_settled["t_s"]) if near_settled else None,
"whole_view_settled_t_s": float(whole_settled["t_s"]) if whole_settled else None,
"far_clear_t_s": float(far_clear["t_s"]) if far_clear else None,
"far_holes_at_pre_move_pct": far_holes(arrival[-1]) if arrival else None,
"pending_gate_p50_ms": percentile(
[float(e.get("pending_gate_elapsed_ms", 0.0)) for e in compile_done], 0.50),
"compile_queue_p95_ms": percentile(
[float(e.get("compile_queue_ms", 0.0)) for e in compile_done], 0.95),
"compile_task_p95_ms": percentile(
[float(e.get("compile_task_ms", 0.0)) for e in compile_done], 0.95),
"compiled_tiles_total": sum(int(e.get("compiled_tiles", 0)) for e in compile_done),
"lookahead_tiles_peak": max((int(e.get("lookahead_region_tiles", 0))
for e in compile_requests), default=0),
"wasted_prefetch_tiles": sum(int(e.get("wasted_prefetch_tiles", 0))
for e in compile_done),
"lookahead_turn_events": lookahead_turns,
"compile_completion_count": len(compile_done),
"trace": str(trace.relative_to(ROOT)), "log": str(abslog.relative_to(ROOT)),
}
with (ROOT / "Saved/Linux/l123" / "runs.jsonl").open("a", encoding="utf-8") as stream:
stream.write(json.dumps(row, sort_keys=True) + "\n")
print("[L123Pair] " + json.dumps(row, sort_keys=True), flush=True)
return row
def pairs_for(runs: list[dict], scenario: str) -> list[dict]:
pairs = []
for repetition in range(1, 6):
baseline = next(row for row in runs if row["scenario"] == scenario
and row["condition"] == "baseline" and row["repetition"] == repetition)
candidate = next(row for row in runs if row["scenario"] == scenario
and row["condition"] == "lookahead" and row["repetition"] == repetition)
pairs.append({"repetition": repetition,
"near_ready_delta_pct": candidate["near_ready_moving_mean_pct"]
- baseline["near_ready_moving_mean_pct"],
"far_holes_delta_pct": candidate["far_holes_moving_mean_pct"]
- baseline["far_holes_moving_mean_pct"],
"tick_p99_delta_ms": candidate["game_thread_tick_p99_ms"]
- baseline["game_thread_tick_p99_ms"],
"tick_max_delta_ms": candidate["game_thread_tick_max_ms"]
- baseline["game_thread_tick_max_ms"]})
return pairs
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--scenario", choices=("movement", "arrival"), required=True)
parser.add_argument("--turn-diagnostic", action="store_true")
args = parser.parse_args()
if not EDITOR.is_file():
raise SystemExit(f"missing Unreal editor: {EDITOR}")
root_out = ROOT / "Saved/Linux/l123" / args.scenario
root_out.mkdir(parents=True, exist_ok=True)
(ROOT / "Saved/Linux/l123").mkdir(parents=True, exist_ok=True)
if args.turn_diagnostic:
row = run("movement", "turn_lookahead", 1, turn=True)
turn_out = ROOT / "Saved/Linux/l123/turn"
turn_out.mkdir(parents=True, exist_ok=True)
(turn_out / "summary.json").write_text(
json.dumps({"run": row}, indent=2) + "\n", encoding="utf-8")
return 0
runs = []
for repetition in range(1, 6):
order = ("baseline", "lookahead") if repetition % 2 else ("lookahead", "baseline")
for condition in order:
runs.append(run(args.scenario, condition, repetition))
(root_out / "runs-partial.json").write_text(
json.dumps(runs, indent=2) + "\n", encoding="utf-8")
pairs = pairs_for(runs, args.scenario)
summary = {
"design": "Compile/retain the current near region set plus one step along velocity; "
"return completion through an Epoch-tagged MPSC queue.",
"scenario": args.scenario, "runs": runs, "pairs": pairs,
"paired_median_near_ready_delta_pct": statistics.median(
pair["near_ready_delta_pct"] for pair in pairs),
"paired_median_far_holes_delta_pct": statistics.median(
pair["far_holes_delta_pct"] for pair in pairs),
"paired_median_tick_p99_delta_ms": statistics.median(
pair["tick_p99_delta_ms"] for pair in pairs),
"paired_median_tick_max_delta_ms": statistics.median(
pair["tick_max_delta_ms"] for pair in pairs),
}
(root_out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())