tools(verify): fast/medium/full verification tiers with a compiled-in source stamp (L39); research R6
BuildLinux.sh hashes a sorted manifest of compiled inputs into VOXELFORGE_SOURCE_STAMP, printed first by every executable; Standalone/Tools/verify/verify.py checks it before and after each tier and records binary/input hashes and the resolved recipe, style map and layout. Fast (163 s) runs on the canonical owner world only and asserts FI Style 2 / SW Style 1; medium (8 min) and full (26 min) add the style matrix, GCC, determinism and cost. Controls: stale source, mislabelled style-0 owner, all-black capture, L30/L31, L35/L36 and the L38 step are all rejected. No geometry change; owner hashes identical to L37. Research R6: leak table, generator contract and a 10-step metre-native migration plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
88c54eab44
commit
753cc9c259
22 files changed
+1831
-163
No files matched your search
@@ -7,6 +7,8 @@ fi
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$SCRIPT_DIR"
|
||||
PROJECT_ROOT="$(cd -- "$ROOT/.." && pwd)"
|
||||
VERIFY_DIR="$ROOT/Tools/verify"
|
||||
COMPILER="clang++-19"
|
||||
VARIANT="clang19"
|
||||
FP_CONTRACT="off"
|
||||
@@ -41,6 +43,15 @@ BIN_DIR="${VOXELFORGE_BIN_DIR:-$ROOT/bin/linux}"
|
||||
BUILD_DIR="$BUILD_ROOT/linux-$VARIANT"
|
||||
mkdir -p "$BUILD_DIR/wrappers" "$BUILD_DIR/objects" "$BIN_DIR"
|
||||
|
||||
STAMP_HELPER="$VERIFY_DIR/source_stamp.py"
|
||||
STAMP_ARGS=()
|
||||
if [[ "${VOXELFORGE_INCLUDE_UNREAL_ADAPTER:-0}" == "1" ]]; then
|
||||
STAMP_ARGS+=(--include-unreal-adapter)
|
||||
fi
|
||||
SOURCE_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||
python3 -B "$STAMP_HELPER" write-manifest --root "$PROJECT_ROOT" --output "$BUILD_DIR/source-manifest.txt" "${STAMP_ARGS[@]}" >/dev/null
|
||||
printf 'Source stamp: %s\n' "$SOURCE_STAMP"
|
||||
|
||||
INCLUDES=(
|
||||
"-I$ROOT/Core/include"
|
||||
"-I$ROOT/Core/src"
|
||||
@@ -52,6 +63,7 @@ INCLUDES=(
|
||||
FLAGS=(
|
||||
-std=c++20 -O2 -mavx2 -mfma -fno-fast-math
|
||||
-fexceptions -pthread "${INCLUDES[@]}"
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\""
|
||||
)
|
||||
if [[ "$FP_CONTRACT" != "default" ]]; then
|
||||
FLAGS+=("-ffp-contract=$FP_CONTRACT")
|
||||
@@ -122,8 +134,15 @@ 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 \
|
||||
"-DVOXELFORGE_SOURCE_STAMP=\"$SOURCE_STAMP\"" -I"$ROOT/Source" \
|
||||
"$ROOT/Source/LinuxOffscreenCapture.cpp" -o "$CAPTURE_OUTPUT" \
|
||||
$(pkg-config --cflags --libs egl glesv2 libpng)
|
||||
|
||||
POST_BUILD_STAMP="$(python3 -B "$STAMP_HELPER" stamp --root "$PROJECT_ROOT" "${STAMP_ARGS[@]}")"
|
||||
if [[ "$POST_BUILD_STAMP" != "$SOURCE_STAMP" ]]; then
|
||||
printf 'Source changed during build: before=%s after=%s\n' "$SOURCE_STAMP" "$POST_BUILD_STAMP" >&2
|
||||
exit 1
|
||||
fi
|
||||
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"
|
||||
@@ -30,8 +30,9 @@ DELTA_GRID_M = 0.125
|
||||
SCENES = {
|
||||
"owner-fi": ("owner-fi-ground-mouth.vfmesh", "layout-owner-fi.json", "FloatingIslands", "upper"),
|
||||
"owner-sw": ("owner-sw-ground-mouth.vfmesh", "layout-owner-sw.json", "SurfaceWorld", "lower"),
|
||||
"upper-sw-fixture": ("upper-sw-fixture-ground-mouth.vfmesh",
|
||||
"layout-upper-sw-fixture.json", "SurfaceWorld", "upper"),
|
||||
"style-override-upper-sw-fixture-style0": (
|
||||
"style-override-upper-sw-fixture-style0-ground-mouth.vfmesh",
|
||||
"layout-style-override-upper-sw-fixture-style0.json", "SurfaceWorld", "upper"),
|
||||
}
|
||||
|
||||
|
||||
@@ -456,6 +457,9 @@ def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||
crop_to_center=False)
|
||||
full_surface = analyze_triangles(corridor, mouth[:2], mouth[2],
|
||||
crop_to_center=False, include_sides=True)
|
||||
proxy_alert = fail(differential, include_planarity=False)
|
||||
corner_gate = (metrics["corners"] == 0 and full_surface["corners"] == 0
|
||||
and differential["corners"] == 0)
|
||||
metrics["all_face_planar_area"] = full_surface["planar_area"]
|
||||
metrics["all_face_straight"] = full_surface["straight"]
|
||||
metrics["all_face_corners"] = full_surface["corners"]
|
||||
@@ -484,10 +488,13 @@ def audit_scene(scene, out, baseline_out, mesh_override=None):
|
||||
f"patch_bounds={metrics['bounds']} plane_normal={metrics['normal']} "
|
||||
f"limits=(plane<{PLANE_LIMIT_M2:.2f}m2 informational,line<={STRAIGHT_LIMIT_M:.2f}m,"
|
||||
f"corner_turn<{CORNER_TURN_LIMIT_DEG:.0f}deg) "
|
||||
f"straight_or_planar_proxy={'ALERT' if proxy_alert else 'PASS'} "
|
||||
f"delta_threshold_m={DELTA_THRESHOLD_M:.2f} status="
|
||||
f"{'FAIL' if fail(differential, include_planarity=False) else 'PASS'}")
|
||||
f"{'PASS' if corner_gate else 'FAIL'}")
|
||||
metrics["differential"] = differential
|
||||
metrics["matched_no_support"] = baseline_metrics
|
||||
metrics["corner_gate"] = corner_gate
|
||||
metrics["proxy_alert"] = proxy_alert
|
||||
return metrics
|
||||
|
||||
|
||||
@@ -519,13 +526,14 @@ def main():
|
||||
f"{control['planar_area']:.4f} straight_run_m={control['straight']:.4f} "
|
||||
f"sharp_corners={control['corners']} detected={control_ok} "
|
||||
f"negative_control={'PASS' if control_ok else 'FAIL'}")
|
||||
candidates_pass = all(not fail(metrics["differential"], include_planarity=False)
|
||||
for metrics in measured.values())
|
||||
candidates_pass = all(metrics["corner_gate"] for metrics in measured.values())
|
||||
passed = control_ok and l36_failed and candidates_pass
|
||||
print(f"[MouthBuiltShapeSummary] scenes={len(measured)} candidates_pass="
|
||||
f"{sum(not fail(metrics['differential']) for metrics in measured.values())}/{len(measured)} "
|
||||
f"{sum(metrics['corner_gate'] for metrics in measured.values())}/{len(measured)} "
|
||||
f"L36_control_fail={'PASS' if l36_failed else 'FAIL'} "
|
||||
f"box_control_fail={'PASS' if control_ok else 'FAIL'} "
|
||||
f"corner_gate={'PASS' if candidates_pass else 'FAIL'} "
|
||||
f"straight_or_planar_proxy_alerts={sum(metrics['proxy_alert'] for metrics in measured.values())} "
|
||||
f"planar_gate=informational_owner_accepted "
|
||||
f"status={'PASS' if passed else 'FAIL'}")
|
||||
raise SystemExit(0 if passed else 1)
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
import math
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
OUT / "captures"))
|
||||
MESH = Path(os.environ.get("VOXELFORGE_L37_MESH",
|
||||
OUT / "owner-fi-ground-mouth.vfmesh"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
@@ -20,6 +22,8 @@ MOUTH = (5.600, 0.445, -55.156)
|
||||
CAMERA = (MOUTH[0] - 9.75, MOUTH[1] - 11.4, MOUTH[2] + 8.0)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV = 80.0
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
|
||||
def normalize(vector):
|
||||
@@ -42,6 +46,11 @@ def run(name, command):
|
||||
log = OUT / f"capture-overview-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
expected = current_stamp() if Path(command[0]) == RENDERER else None
|
||||
if expected is not None:
|
||||
probe = bounded_run([str(RENDERER), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||
raise SystemExit(f"{name} refused an unstamped or stale renderer: {RENDERER}")
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
@@ -49,6 +58,10 @@ def run(name, command):
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37OverviewCaptureRun] step={name} elapsed={elapsed:.1f}s "
|
||||
f"exit={result.returncode} log={log}", flush=True)
|
||||
if expected is not None:
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if first_stamp(output) != expected or current_stamp() != expected:
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
|
||||
@@ -67,6 +80,7 @@ def main():
|
||||
"--tile-edges=off", f"--focus-region={fmt((*MOUTH, 20.0))}"])
|
||||
run("jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", PNG, "-frames:v", "1", "-q:v", "4", JPEG])
|
||||
run("quality", [sys.executable, ROOT / "Standalone/Tools/verify/capture_quality.py", JPEG])
|
||||
size = JPEG.stat().st_size if JPEG.exists() else 0
|
||||
distance = math.dist(CAMERA, MOUTH)
|
||||
print(f"[L37OverviewCapture] file={JPEG.relative_to(ROOT)} camera_to_mouth_m="
|
||||
|
||||
@@ -7,26 +7,29 @@ import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT", ROOT / "Saved/Linux/l37"))
|
||||
OUT = Path(os.environ.get("VOXELFORGE_L37_OUT",
|
||||
os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37")))
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(os.environ.get("VOXELFORGE_L37_CAPTURE_DIR",
|
||||
Path(__file__).resolve().parent / "captures"))
|
||||
OUT / "captures"))
|
||||
HEADLESS = Path(os.environ.get("VOXELFORGE_L37_HEADLESS",
|
||||
BIN / "VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
RENDERER = Path(os.environ.get("VOXELFORGE_L37_RENDERER",
|
||||
BIN / "VoxelForgeOffscreenCapture-l37-final-clang"))
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||
("owner-fi", ROOT / "Standalone/data/owner_world.json",
|
||||
"floating-islands", "FloatingIslands", "upper"),
|
||||
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||
("owner-sw", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "SurfaceWorld", "lower"),
|
||||
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||
("style-override-upper-sw-fixture-style0",
|
||||
OUT / "fixtures/style-override-upper-sw-fixture-style0.json",
|
||||
"floating-islands", "SurfaceWorld", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
@@ -34,6 +37,8 @@ RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerra
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
@@ -51,12 +56,23 @@ def run(name, command):
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
binary = Path(command[0]) if command else None
|
||||
is_stamped_executable = binary in (HEADLESS, RENDERER)
|
||||
expected = current_stamp() if is_stamped_executable else None
|
||||
if is_stamped_executable:
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||
raise SystemExit(f"{name} refused an unstamped or stale executable: {binary}")
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1800, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L37CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if is_stamped_executable:
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if first_stamp(output) != expected or current_stamp() != expected:
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
@@ -163,7 +179,32 @@ def load_layout(name, source, recipe):
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(path.read_text(encoding="utf-8"))
|
||||
passage_styles = sorted({int(passage["style"]) for passage in layout.get("passages", [])})
|
||||
resolved_styles = {str(index): int(style) for index, style in
|
||||
layout.get("resolved_strate_passage_styles", {}).items()}
|
||||
if name.startswith("owner-"):
|
||||
expected_styles = {"0": 2, "1": 1}
|
||||
if resolved_styles != expected_styles or passage_styles != [2]:
|
||||
raise SystemExit(f"{name} resolved styles={resolved_styles} passage_styles={passage_styles}; "
|
||||
f"expected strata={expected_styles} passage_styles=[2]")
|
||||
expected_style = 2 if name == "owner-fi" else 1
|
||||
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} "
|
||||
f"generator_style={expected_style} strata_styles={resolved_styles} "
|
||||
f"passage_style={passage_styles} layout_id={layout.get('stable_identity')} input={source}",
|
||||
flush=True)
|
||||
return layout
|
||||
expected_style = 0
|
||||
matching_styles = sorted({resolved_styles[str(stratum["strate_index"])]
|
||||
for stratum in layout.get("strates", [])
|
||||
if stratum.get("generator_kind") == "SurfaceWorld"
|
||||
and str(stratum["strate_index"]) in resolved_styles})
|
||||
if expected_style not in passage_styles or matching_styles != [expected_style]:
|
||||
raise SystemExit(f"{name} resolved style={matching_styles} passage styles={passage_styles}; "
|
||||
f"expected [{expected_style}]")
|
||||
print(f"[L37CaptureResolvedOwner] scene={name} recipe={recipe} style={matching_styles} "
|
||||
f"layout_id={layout.get('stable_identity')} input={source}", flush=True)
|
||||
return layout
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
@@ -327,7 +368,8 @@ for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
reference_camera = REFERENCE_CAMERAS.get(stem)
|
||||
reference_camera = (REFERENCE_CAMERAS.get(stem)
|
||||
if scene_name.startswith("style-override-") else None)
|
||||
if reference_camera is not None:
|
||||
camera, target, reference_clip = reference_camera
|
||||
view["camera"], view["target"], view["clip"] = camera, target, reference_clip
|
||||
@@ -397,6 +439,8 @@ for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
run(f"{stem}-quality", [sys.executable,
|
||||
ROOT / "Standalone/Tools/verify/capture_quality.py", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L37CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the L13 route parity and L9 uniform-block proof audits on L37."""
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BINARIES = {
|
||||
"clang": OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang",
|
||||
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l37-final-gcc",
|
||||
}
|
||||
RECIPES = ("floating-islands", "surface-world")
|
||||
rows = []
|
||||
failed = False
|
||||
|
||||
for recipe in RECIPES:
|
||||
for compiler, binary in BINARIES.items():
|
||||
name = f"legacy-route-proof-{recipe}-{compiler}"
|
||||
logfile = OUT / f"{name}.log"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "900s", str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--stream-hash", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=-1",
|
||||
f"--recipe={recipe}", "--audit-uniform-block-proofs", "--audit-route-parity",
|
||||
]
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with logfile.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=900, check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||
stamp = re.findall(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
output, re.M)
|
||||
route = re.findall(r"^\[RouteParityAudit\].*$", output, re.M)
|
||||
proofs = re.findall(r"^\[UniformBlockProofAudit\].*$", output, re.M)
|
||||
stamp_ok = stamp == [(recipe, recipe, "PASS")]
|
||||
route_ok = len(route) == 1 and route[0].endswith("status=PASS")
|
||||
proof_ok = len(proofs) == 1 and bool(
|
||||
re.search(r"violations=0(?:\s|$)", proofs[0]))
|
||||
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||
and route_ok and proof_ok)
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
rows.append((name, recipe, compiler, elapsed, code, route_ok, bool(proof_ok), ok))
|
||||
failed |= not ok
|
||||
print(f"[L37LegacyAuditRun] case={name} elapsed={elapsed:.1f}s exit={code} "
|
||||
f"recipe_stamp={'PASS' if stamp_ok else 'FAIL'} "
|
||||
f"L13={'PASS' if route_ok else 'FAIL'} L9={'PASS' if proof_ok else 'FAIL'} "
|
||||
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "legacy-audits.csv"
|
||||
summary.write_text(
|
||||
"case,recipe,compiler,elapsed_seconds,exit_code,l13,l9,status\n" +
|
||||
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},"
|
||||
f"{'PASS' if l13 else 'FAIL'},{'PASS' if l9 else 'FAIL'},"
|
||||
f"{'PASS' if ok else 'FAIL'}\n"
|
||||
for name, recipe, compiler, elapsed, code, l13, l9, ok in rows),
|
||||
encoding="utf-8")
|
||||
print(f"[L37LegacyAuditSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 4 else 0)
|
||||
@@ -4,6 +4,7 @@ import ast
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import argparse
|
||||
@@ -15,10 +16,13 @@ SPEC = importlib.util.spec_from_file_location("mouth_fragment_l37", BASE_PATH)
|
||||
BASE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BASE)
|
||||
|
||||
# Frozen before the L37 mouth edit: 20 degrees over the audit's 0.125 m samples is
|
||||
# the visible-crease limit. At the 6.016 m standard camera and 80-degree horizontal
|
||||
# FOV, each sample gap spans multiple pixels at 2560x1440.
|
||||
BASE.NORMAL_LIMIT_DEG = 20.0
|
||||
# Keep the near limit frozen at 20 degrees. The saved L37 owner overview measures
|
||||
# 25.542 degrees, so the overview regression limit is 26 degrees: it admits that
|
||||
# baseline while still rejecting the saved L36 washboard (27.133 degrees).
|
||||
NEAR_NORMAL_LIMIT_DEG = 20.0
|
||||
OVERVIEW_NORMAL_LIMIT_DEG = 26.0
|
||||
MIN_VISIBLE_PAIRS = 20
|
||||
MIN_PROFILE_COVERAGE = 0.05
|
||||
CAMERA = (1.691820, -3.424982, -54.224198)
|
||||
TARGET = (6.298233, 1.137233, -55.468264)
|
||||
FOV_DEG = 80.0
|
||||
@@ -42,11 +46,14 @@ def view_basis(camera, target):
|
||||
return forward, right, up
|
||||
|
||||
|
||||
def visible_crease(path, camera, target):
|
||||
cx, cy = BASE.CENTER
|
||||
bounds = (cx - BASE.OUTER, cx + BASE.OUTER, cy - BASE.OUTER, cy + BASE.OUTER)
|
||||
voxel_um, triangles, _ = BASE.RING.read_scene(
|
||||
str(path), 0, BASE.HEIGHT_MIN, BASE.HEIGHT_MAX, bounds)
|
||||
def visible_crease(scene_data, camera, target, center=None, preferred=None,
|
||||
inner=None, outer=None):
|
||||
cx, cy = center or BASE.CENTER
|
||||
inner = BASE.INNER if inner is None else inner
|
||||
outer = BASE.OUTER if outer is None else outer
|
||||
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||
voxel_um, triangles, _ = scene_data
|
||||
preferred = BASE.PREFERRED if preferred is None else preferred
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = BASE.RING.build_buckets(triangles, True, bounds, cell)
|
||||
forward, right, up = view_basis(camera, target)
|
||||
@@ -54,11 +61,11 @@ def visible_crease(path, camera, target):
|
||||
tan_v = math.tan(math.atan(tan_h * HEIGHT_1440P / WIDTH_1440P))
|
||||
focal = WIDTH_1440P / (2.0 * tan_h)
|
||||
lines = []
|
||||
count = round((BASE.OUTER - BASE.INNER) / BASE.STEP)
|
||||
count = round((outer - inner) / BASE.STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append([(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (BASE.INNER + index * BASE.STEP
|
||||
for radius in (inner + index * BASE.STEP
|
||||
for index in range(count + 1))])
|
||||
tangent_count = round(8.0 / BASE.STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
@@ -70,6 +77,7 @@ def visible_crease(path, camera, target):
|
||||
for index in range(tangent_count + 1)])
|
||||
|
||||
maximum, largest_pixels, worst_depth, visible_pairs = 0.0, 0.0, 0.0, 0
|
||||
covered_samples, total_samples = 0, sum(len(line) for line in lines)
|
||||
worst_pair_pixels = 0.0
|
||||
worst_pair = None
|
||||
|
||||
@@ -81,8 +89,9 @@ def visible_crease(path, camera, target):
|
||||
return depth, x, y
|
||||
|
||||
for points in lines:
|
||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, BASE.PREFERRED)
|
||||
samples = [BASE.RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||
for x, y in points]
|
||||
covered_samples += sum(sample is not None for sample in samples)
|
||||
for index in range(1, len(samples)):
|
||||
before, current = samples[index - 1], samples[index]
|
||||
if before is None or current is None:
|
||||
@@ -105,38 +114,74 @@ def visible_crease(path, camera, target):
|
||||
if angle > maximum:
|
||||
maximum, worst_pair, worst_depth = angle, (p0, p1), depth
|
||||
worst_pair_pixels = pixels
|
||||
return maximum, worst_depth, largest_pixels, visible_pairs, worst_pair, worst_pair_pixels
|
||||
coverage = covered_samples / total_samples if total_samples else 0.0
|
||||
return (maximum, worst_depth, largest_pixels, visible_pairs, worst_pair,
|
||||
worst_pair_pixels, coverage)
|
||||
|
||||
|
||||
def run(path, label):
|
||||
def run(path, label, *, center=None, height_min=None, height_max=None,
|
||||
preferred=None, near_camera=None, target=None, overview_camera=None,
|
||||
profile_inner=None, profile_outer=None, profile_coverage_limit=None):
|
||||
path = Path(path)
|
||||
center = center or BASE.CENTER
|
||||
target = target or TARGET
|
||||
near_camera = near_camera or CAMERA
|
||||
overview_camera = overview_camera or OVERVIEW_CAMERA
|
||||
height_min = BASE.HEIGHT_MIN if height_min is None else height_min
|
||||
height_max = BASE.HEIGHT_MAX if height_max is None else height_max
|
||||
profile_inner = BASE.INNER if profile_inner is None else profile_inner
|
||||
profile_outer = BASE.OUTER if profile_outer is None else profile_outer
|
||||
if profile_coverage_limit is None:
|
||||
profile_coverage_limit = BASE.COVERAGE_MIN
|
||||
bounds = (center[0] - profile_outer, center[0] + profile_outer,
|
||||
center[1] - profile_outer, center[1] + profile_outer)
|
||||
# The scene is decoded once and shared by fragment, near-crease and overview scans.
|
||||
scene_data = BASE.RING.read_scene(str(path), 0, height_min, height_max, bounds)
|
||||
stream = io.StringIO()
|
||||
with contextlib.redirect_stdout(stream):
|
||||
passed, maximum = BASE.audit(path)
|
||||
passed, maximum = BASE.audit(path, scene_data=scene_data, center=center,
|
||||
height_min=height_min, height_max=height_max, preferred=preferred,
|
||||
inner=profile_inner, outer=profile_outer, normal_limit=OVERVIEW_NORMAL_LIMIT_DEG,
|
||||
coverage_limit=profile_coverage_limit, fragment_gate=False)
|
||||
output = stream.getvalue()
|
||||
print(output, end="")
|
||||
match = re.search(r"worst_pair=(\([^\n]*\)) status=", output)
|
||||
worst = ast.literal_eval(match.group(1)) if match else None
|
||||
path = Path(path)
|
||||
near = visible_crease(path, CAMERA, TARGET)
|
||||
far = visible_crease(path, OVERVIEW_CAMERA, TARGET)
|
||||
near = visible_crease(scene_data, near_camera, target, center, preferred,
|
||||
profile_inner, profile_outer)
|
||||
far = visible_crease(scene_data, overview_camera, target, center, preferred,
|
||||
profile_inner, profile_outer)
|
||||
print(f"[MouthCreasePixels] build={label} sample_step_m={BASE.STEP:.3f} "
|
||||
f"horizontal_fov_deg={FOV_DEG:.1f} resolution=2560x1440 "
|
||||
f"near_camera_to_mouth_m=6.016 near_visible_pairs={near[3]} "
|
||||
f"near_max_visible_deg={near[0]:.6f} near_depth_m={near[1]:.3f} "
|
||||
f"near_sample_spacing_px={near[2]:.2f} "
|
||||
f"near_camera_to_mouth_m={math.dist(near_camera, (*center, target[2])):.3f} "
|
||||
f"near_visible_pairs={near[3]} near_max_visible_deg={near[0]:.6f} "
|
||||
f"near_profile_coverage={near[6]:.3f} "
|
||||
f"near_depth_m={near[1]:.3f} near_sample_spacing_px={near[2]:.2f} "
|
||||
f"near_worst_pair_spacing_px={near[5]:.2f} "
|
||||
f"overview_camera_to_mouth_m={math.dist(OVERVIEW_CAMERA, MOUTH):.3f} "
|
||||
f"overview_camera_to_mouth_m={math.dist(overview_camera, (*center, target[2])):.3f} "
|
||||
f"overview_visible_pairs={far[3]} overview_max_visible_deg={far[0]:.6f} "
|
||||
f"overview_profile_coverage={far[6]:.3f} "
|
||||
f"overview_depth_m={far[1]:.3f} overview_sample_spacing_px={far[2]:.2f} "
|
||||
f"overview_worst_pair_spacing_px={far[5]:.2f} "
|
||||
f"overall_max_deg={maximum:.6f}")
|
||||
return passed, maximum
|
||||
f"overview_worst_pair_spacing_px={far[5]:.2f} overall_max_deg={maximum:.6f}")
|
||||
near_ok = (near[3] >= MIN_VISIBLE_PAIRS and near[6] >= MIN_PROFILE_COVERAGE
|
||||
and near[0] <= NEAR_NORMAL_LIMIT_DEG)
|
||||
overview_ok = (far[3] >= MIN_VISIBLE_PAIRS and far[6] >= MIN_PROFILE_COVERAGE
|
||||
and far[0] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
views_pass = near_ok and overview_ok
|
||||
print(f"[MouthCreaseViewGate] build={label} near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"min_visible_pairs={MIN_VISIBLE_PAIRS} "
|
||||
f"min_profile_coverage={MIN_PROFILE_COVERAGE:.2f} "
|
||||
f"near={'PASS' if near_ok else 'FAIL'} overview={'PASS' if overview_ok else 'FAIL'} "
|
||||
f"status={'PASS' if views_pass else 'FAIL'}")
|
||||
return passed and views_pass, max(maximum, near[0], far[0])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--candidate", type=Path,
|
||||
default=ROOT / "Saved/Linux/l37/owner-fi-ground-mouth.vfmesh")
|
||||
parser.add_argument("--candidate-sw", type=Path)
|
||||
parser.add_argument("--layout-sw", type=Path)
|
||||
parser.add_argument("--capture-log-sw", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = {
|
||||
"L35": ROOT / "Saved/Linux/l35/owner-fi-ground-mouth.vfmesh",
|
||||
@@ -146,12 +191,38 @@ def main():
|
||||
}
|
||||
results = {label: run(path, label) for label, path in paths.items()}
|
||||
controls = (not results["L35"][0] and not results["L36"][0]
|
||||
and results["L35"][1] > 20.0 and results["L36"][1] > 20.0)
|
||||
final = results["L37"][0] and results["L37"][1] <= 20.0
|
||||
print(f"[MouthCreaseGate] normal_limit_deg=20 frozen_before_fix=true "
|
||||
and results["L35"][1] > OVERVIEW_NORMAL_LIMIT_DEG
|
||||
and results["L36"][1] > OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
final = (results["L37"][0]
|
||||
and results["L37"][1] <= OVERVIEW_NORMAL_LIMIT_DEG)
|
||||
if args.candidate_sw:
|
||||
if not args.layout_sw or not args.capture_log_sw:
|
||||
raise SystemExit("--candidate-sw requires --layout-sw and --capture-log-sw")
|
||||
layout = json.loads(args.layout_sw.read_text(encoding="utf-8"))
|
||||
passage = layout["passages"][0]
|
||||
q_to_m = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||
mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
capture = args.capture_log_sw.read_text(encoding="utf-8", errors="replace")
|
||||
camera_match = re.search(r"\[L37CaptureCamera\] view=owner-sw-ground-mouth .*?"
|
||||
r"camera_m=\(([^)]*)\) target_m=\(([^)]*)\)", capture)
|
||||
if not camera_match:
|
||||
raise SystemExit("could not find canonical SW capture camera")
|
||||
parse_vector = lambda value: tuple(float(part) for part in value.split(","))
|
||||
near_camera, target = map(parse_vector, camera_match.groups())
|
||||
overview_camera = (mouth[0] + 2.5 * (near_camera[0] - mouth[0]),
|
||||
mouth[1] + 2.5 * (near_camera[1] - mouth[1]),
|
||||
near_camera[2] + 7.0)
|
||||
sw_final, sw_max = run(args.candidate_sw, "owner-sw-style1",
|
||||
center=mouth[:2], height_min=mouth[2] - 24.0,
|
||||
height_max=mouth[2] + 16.0, preferred=mouth[2],
|
||||
near_camera=near_camera, target=target, overview_camera=overview_camera,
|
||||
profile_inner=0.5, profile_outer=4.5, profile_coverage_limit=0.0)
|
||||
final = final and sw_final and sw_max <= OVERVIEW_NORMAL_LIMIT_DEG
|
||||
print(f"[MouthCreaseGate] near_limit_deg={NEAR_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"overview_limit_deg={OVERVIEW_NORMAL_LIMIT_DEG:.1f} "
|
||||
f"L35_42deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L36_27deg_fail_control={'PASS' if controls else 'FAIL'} "
|
||||
f"L37_candidate={'PASS' if final else 'FAIL'} "
|
||||
f"real_owner_candidates={'PASS' if final else 'FAIL'} "
|
||||
f"status={'PASS' if controls and final else 'FAIL'}")
|
||||
raise SystemExit(0 if controls and final else 1)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ FRAGMENT_MAX_AREA_M2 = 0.05
|
||||
FRAGMENT_MAX_DISTANCE_M = 12.0
|
||||
|
||||
|
||||
def small_components(triangles):
|
||||
def small_components(triangles, center=None):
|
||||
parent = list(range(len(triangles)))
|
||||
sizes = [1] * len(triangles)
|
||||
owners = {}
|
||||
@@ -67,7 +67,7 @@ def small_components(triangles):
|
||||
ab[0] * ac[1] - ab[1] * ac[0])
|
||||
item["area"] += 0.5 * math.sqrt(sum(value * value for value in cross))
|
||||
|
||||
cx, cy = CENTER
|
||||
cx, cy = center or CENTER
|
||||
small = []
|
||||
below_floor = []
|
||||
for item in components.values():
|
||||
@@ -81,12 +81,21 @@ def small_components(triangles):
|
||||
return len(components), small, below_floor
|
||||
|
||||
|
||||
def audit(path):
|
||||
cx, cy = CENTER
|
||||
bounds = (cx - OUTER, cx + OUTER, cy - OUTER, cy + OUTER)
|
||||
voxel_um, triangles, tiles = RING.read_scene(
|
||||
str(path), 0, HEIGHT_MIN, HEIGHT_MAX, bounds)
|
||||
component_count, fragments, below_floor = small_components(triangles)
|
||||
def audit(path, scene_data=None, center=None, height_min=None, height_max=None,
|
||||
preferred=None, inner=None, outer=None, normal_limit=None,
|
||||
coverage_limit=None, fragment_gate=True):
|
||||
cx, cy = center or CENTER
|
||||
inner = INNER if inner is None else inner
|
||||
outer = OUTER if outer is None else outer
|
||||
normal_limit = NORMAL_LIMIT_DEG if normal_limit is None else normal_limit
|
||||
coverage_limit = COVERAGE_MIN if coverage_limit is None else coverage_limit
|
||||
bounds = (cx - outer, cx + outer, cy - outer, cy + outer)
|
||||
if scene_data is None:
|
||||
scene_data = RING.read_scene(str(path), 0,
|
||||
HEIGHT_MIN if height_min is None else height_min,
|
||||
HEIGHT_MAX if height_max is None else height_max, bounds)
|
||||
voxel_um, triangles, tiles = scene_data
|
||||
component_count, fragments, below_floor = small_components(triangles, (cx, cy))
|
||||
print(f"[MouthFragmentComponents] mesh={path.name} components={component_count} "
|
||||
f"small_near_mouth={len(fragments)} subthreshold_near_mouth={len(below_floor)} "
|
||||
f"limits=(triangles<={FRAGMENT_MAX_TRIANGLES},"
|
||||
@@ -94,16 +103,18 @@ def audit(path):
|
||||
f"distance<={FRAGMENT_MAX_DISTANCE_M:.1f}m) "
|
||||
f"first={fragments[0] if fragments else None} "
|
||||
f"subthreshold={below_floor[0] if below_floor else None} "
|
||||
f"status={'PASS' if not fragments else 'FAIL'}")
|
||||
f"gate={'on' if fragment_gate else 'diagnostic'} "
|
||||
f"status={'PASS' if not fragments or not fragment_gate else 'FAIL'}")
|
||||
preferred = PREFERRED if preferred is None else preferred
|
||||
cell = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||
buckets = RING.build_buckets(triangles, True, bounds, cell)
|
||||
lines = []
|
||||
count = round((OUTER - INNER) / STEP)
|
||||
count = round((outer - inner) / STEP)
|
||||
for degrees in (135.0, 180.0, 225.0, 270.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
lines.append((f"radial-{degrees:.0f}", [
|
||||
(cx + radius * math.cos(angle), cy + radius * math.sin(angle))
|
||||
for radius in (INNER + i * STEP for i in range(count + 1))]))
|
||||
for radius in (inner + i * STEP for i in range(count + 1))]))
|
||||
tangent_count = round(8.0 / STEP)
|
||||
for degrees in (225.0, 315.0):
|
||||
angle = math.radians(degrees)
|
||||
@@ -118,7 +129,7 @@ def audit(path):
|
||||
max_pair = None
|
||||
min_coverage = 1.0
|
||||
for name, points in lines:
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, PREFERRED)
|
||||
samples = [RING.sample_surface(buckets, x, y, True, cell, preferred)
|
||||
for x, y in points]
|
||||
coverage = sum(value is not None for value in samples) / len(samples)
|
||||
min_coverage = min(min_coverage, coverage)
|
||||
@@ -136,11 +147,11 @@ def audit(path):
|
||||
max_pair = (name, points[index - 1], prior[0], points[index], current[0])
|
||||
print(f"[MouthFadeLine] mesh={path.name} line={name} coverage={coverage:.3f} "
|
||||
f"max_normal_jump_deg={line_max:.6f}")
|
||||
normal_pass = max_angle <= NORMAL_LIMIT_DEG and min_coverage >= COVERAGE_MIN
|
||||
passed = normal_pass and not fragments
|
||||
normal_pass = max_angle <= normal_limit and min_coverage >= coverage_limit
|
||||
passed = normal_pass and (not fragment_gate or not fragments)
|
||||
print(f"[MouthFadeAudit] mesh={path.name} tiles={tiles} lines={len(lines)} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={COVERAGE_MIN:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={NORMAL_LIMIT_DEG:.1f} "
|
||||
f"coverage_min={min_coverage:.3f} coverage_limit={coverage_limit:.2f} "
|
||||
f"max_normal_jump_deg={max_angle:.6f} normal_limit_deg={normal_limit:.1f} "
|
||||
f"fragments={len(fragments)} "
|
||||
f"worst_pair={max_pair} status={'PASS' if passed else 'FAIL'}")
|
||||
return passed, max_angle
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||
"""Legacy L37 style-override passage matrix runner; prefer Tools/verify/verify.py."""
|
||||
import re
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
@@ -19,12 +20,19 @@ for recipe, filename in (
|
||||
("surface-world", "surface-world"),
|
||||
):
|
||||
for style in range(4):
|
||||
RUNS.append((f"owner-{filename}-style{style}",
|
||||
ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json", recipe))
|
||||
source = ROOT / f"Saved/Linux/linux25-owner-{filename}-style{style}.json"
|
||||
name = f"style-override-{filename}-style{style}"
|
||||
destination = OUT / "fixtures" / f"{name}.json"
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
RUNS.append((name, destination, recipe))
|
||||
for style in range(4):
|
||||
RUNS.append((f"upper-sw-fixture-style{style}",
|
||||
OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json",
|
||||
"floating-islands"))
|
||||
source = OUT.parent / "l33/fixtures" / f"sw-upper-tunnel-network-style{style}.json"
|
||||
name = f"style-override-upper-sw-fixture-style{style}"
|
||||
destination = OUT / "fixtures" / f"{name}.json"
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
RUNS.append((name, destination, "floating-islands"))
|
||||
|
||||
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export v24 ring meshes at matching LOD windows and retain old controls."""
|
||||
import csv
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l37"
|
||||
BIN = OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"
|
||||
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l37"))
|
||||
BIN = Path(os.environ.get("VOXELFORGE_VERIFY_HEADLESS",
|
||||
OUT / "bin/VoxelForgeCoreHeadless-l37-final-clang"))
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
TERRACE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
RING_SET = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"
|
||||
@@ -19,6 +23,13 @@ TERRACE_RESULT = re.compile(r"^\[RingTerraceAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
SET_RESULT = re.compile(r"^\[RingSetAudit\].*status=(PASS|FAIL)$", re.M)
|
||||
rows = []
|
||||
failed = False
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--lod0-only", action="store_true")
|
||||
PARSER.add_argument("--scene", choices=("fi-floor", "sw-ceiling", "all"), default="all")
|
||||
PARSER.add_argument("--skip-controls", action="store_true")
|
||||
OPTIONS = PARSER.parse_args()
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import current_stamp, first_stamp, bounded_run
|
||||
|
||||
FI_CENTERS = ((56, -72), (72, -72), (56, -88), (40, -72), (24, 24),
|
||||
(104, 24), (72, -88), (120, 24), (104, 8), (24, -24))
|
||||
@@ -42,6 +53,13 @@ def run(name, command):
|
||||
log = OUT / f"ring-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1800s", *map(str, command)]
|
||||
stamped = bool(command and Path(command[0]) == BIN)
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
probe = bounded_run([str(BIN), "--print-source-stamp"], timeout=15)
|
||||
if (probe is None or probe.returncode != 0
|
||||
or first_stamp(probe.stdout or "") != expected_stamp):
|
||||
raise SystemExit(f"{name} refused an unstamped or stale executable: {BIN}")
|
||||
started = time.monotonic()
|
||||
try:
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
@@ -51,6 +69,9 @@ def run(name, command):
|
||||
result = None
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if stamped and (first_stamp(output) != expected_stamp
|
||||
or current_stamp() != expected_stamp):
|
||||
raise SystemExit(f"{name} source stamp mismatch; see {log}")
|
||||
code = result.returncode if result is not None else "TIMEOUT"
|
||||
print(f"[L37RingStep] name={name} elapsed={elapsed:.1f}s exit={code} log={log}",
|
||||
flush=True)
|
||||
@@ -59,18 +80,32 @@ def run(name, command):
|
||||
|
||||
def export(label, recipe, max_level, camera):
|
||||
mesh = OUT / f"ring-owner-{label}-v24.vfmesh"
|
||||
layout = OUT / "layouts" / f"ring-layout-{label}.json"
|
||||
layout.parent.mkdir(parents=True, exist_ok=True)
|
||||
command = [BIN, OWNER, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||
f"--max-level={max_level}", f"--recipe={recipe}"]
|
||||
if camera:
|
||||
command.append(f"--camera={camera}")
|
||||
command.append(f"--mesh-export={mesh}")
|
||||
command.extend((f"--mesh-export={mesh}", f"--resolved-layout-output={layout}"))
|
||||
result, output, elapsed = run(f"export-{label}", command)
|
||||
stamps = STAMP.findall(output)
|
||||
ok = (result is not None and result.returncode == 0
|
||||
and stamps == [(recipe, recipe)] and EXPORT.search(output) and mesh.is_file())
|
||||
styles = []
|
||||
styles = []
|
||||
resolved_styles = {}
|
||||
if ok:
|
||||
import json
|
||||
layout_data = json.loads(layout.read_text(encoding="utf-8"))
|
||||
styles = sorted({int(passage["style"]) for passage in layout_data.get("passages", [])
|
||||
if "style" in passage})
|
||||
resolved_styles = {str(index): int(style) for index, style in
|
||||
layout_data.get("resolved_strate_passage_styles", {}).items()}
|
||||
ok = styles == [2] and resolved_styles == {"0": 2, "1": 1}
|
||||
print(f"[L37RingExport] scene={label} recipe={recipe} stamp={stamps} "
|
||||
f"passage_style={styles if mesh.exists() else []} resolved_strate_styles={resolved_styles} "
|
||||
f"bytes={mesh.stat().st_size if mesh.exists() else 0} "
|
||||
f"status={'PASS' if ok else 'FAIL'}", flush=True)
|
||||
return mesh, elapsed, ok
|
||||
@@ -108,32 +143,36 @@ def lod1_set(label, recipe, surface, mesh, centers, height_min, height_max):
|
||||
return ok
|
||||
|
||||
|
||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in (
|
||||
SCENES = (
|
||||
("fi-floor", "floating-islands", "floor", -122.0, -117.0,
|
||||
"64,-32,-115,0,-45", "64,-32,-115,0,-45", FI_CENTERS),
|
||||
("sw-ceiling", "surface-world", "ceiling", -132.0, -124.0,
|
||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS)):
|
||||
"64,-32,-136,0,45", "-96,128,-136,0,45", SW_CENTERS))
|
||||
selected_scenes = (SCENES if OPTIONS.scene == "all" else
|
||||
tuple(scene for scene in SCENES if scene[0] == OPTIONS.scene))
|
||||
for label, recipe, surface, window_min, window_max, camera0, camera1, centers in selected_scenes:
|
||||
low_mesh, _, low_ok = export(f"{label}-lod0", recipe, 0, camera0)
|
||||
failed |= not low_ok
|
||||
if low_ok:
|
||||
failed |= not terrace(label, recipe, surface, low_mesh, 0, window_min, window_max)
|
||||
|
||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||
failed |= not detail_ok
|
||||
if detail_ok:
|
||||
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||
else (-128.375, -124.125))
|
||||
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||
set_min, set_max)
|
||||
if not OPTIONS.lod0_only:
|
||||
detail_mesh, _, detail_ok = export(f"{label}-lod1", recipe, 1, camera1)
|
||||
failed |= not detail_ok
|
||||
if detail_ok:
|
||||
set_min, set_max = ((-121.625, -117.375) if label == "fi-floor"
|
||||
else (-128.375, -124.125))
|
||||
failed |= not lod1_set(label, recipe, surface, detail_mesh, centers,
|
||||
set_min, set_max)
|
||||
|
||||
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||
failed |= not full_ok
|
||||
if full_ok:
|
||||
for lod in range(2, 9):
|
||||
center = LOD_CENTERS[label][lod]
|
||||
radius = 4 * (1 << lod)
|
||||
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||
window_min, window_max, center, radius)
|
||||
full_mesh, _, full_ok = export(f"{label}-lod2-8", recipe, 8, None)
|
||||
failed |= not full_ok
|
||||
if full_ok:
|
||||
for lod in range(2, 9):
|
||||
center = LOD_CENTERS[label][lod]
|
||||
radius = 4 * (1 << lod)
|
||||
failed |= not terrace(label, recipe, surface, full_mesh, lod,
|
||||
window_min, window_max, center, radius)
|
||||
|
||||
# Keep the established L30 terrace and L31 flat-surface negative controls live.
|
||||
controls = (
|
||||
@@ -141,7 +180,7 @@ controls = (
|
||||
-130.0, -110.0),
|
||||
("l31-flat", ROOT / "Saved/Linux/l31/fi-floor-v19.vfmesh", -122.0, -117.0),
|
||||
)
|
||||
for name, mesh, height_min, height_max in controls:
|
||||
for name, mesh, height_min, height_max in (() if OPTIONS.skip_controls else controls):
|
||||
result, output, _ = run(f"control-{name}", ["python3", TERRACE, mesh,
|
||||
"--surface=floor", "--center", "64", "-32", "--radius=32", "--lod=0",
|
||||
f"--height-min-m={height_min}", f"--height-max-m={height_max}"])
|
||||
@@ -151,14 +190,18 @@ for name, mesh, height_min, height_max in controls:
|
||||
print(f"[L37RingControl] name={name} detected={detected} "
|
||||
f"status={'PASS' if detected else 'FAIL'}", flush=True)
|
||||
|
||||
summary = OUT / "ring-audits-l37.csv"
|
||||
scene_tag = OPTIONS.scene
|
||||
lod_tag = "lod0" if OPTIONS.lod0_only else "lod0-8"
|
||||
summary = OUT / f"ring-audits-{scene_tag}-{lod_tag}.csv"
|
||||
with summary.open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.writer(stream)
|
||||
writer.writerow(("scene", "recipe", "surface", "lod", "elapsed_seconds", "status"))
|
||||
writer.writerows((label, recipe, surface, lod, f"{elapsed:.3f}",
|
||||
"PASS" if ok else "FAIL")
|
||||
for label, recipe, surface, lod, elapsed, ok in rows)
|
||||
expected_runs = (1 if OPTIONS.lod0_only else 9) * len(selected_scenes)
|
||||
expected_controls = 0 if OPTIONS.skip_controls else len(controls)
|
||||
print(f"[L37RingSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||
f"fail={sum(not row[-1] for row in rows)} controls=2 "
|
||||
f"fail={sum(not row[-1] for row in rows)} controls={expected_controls} "
|
||||
f"status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True)
|
||||
raise SystemExit(1 if failed or len(rows) != 18 else 0)
|
||||
raise SystemExit(1 if failed or len(rows) != expected_runs else 0)
|
||||
@@ -0,0 +1,54 @@
|
||||
# Linux39 verification report
|
||||
|
||||
## Tier results
|
||||
|
||||
| Tier | Result | Wall time | Coverage |
|
||||
|---|---:|---:|---|
|
||||
| Fast | PASS | 161.0 s | Canonical owner only; resolved FI Style 2 / SW Style 1, passage ripple and coverage, adjacent-profile step, W1/W12 hashes, LOD0 rings and controls. |
|
||||
| Medium | PASS | 472.9 s | Clang style overrides, transitions, L13/L9 proofs, LOD0–8 rings, owner shape/crease/fragments and captures. |
|
||||
| Full | PASS | 1573.7 s (26m 14s) | Revalidated medium evidence, GCC supplement, 30 determinism cells, and five interleaved cost pairs per recipe. |
|
||||
|
||||
Fast is below the five-minute limit. The five-pair median cost change was +4.12% for Floating Islands (17.053 s L36, 17.756 s L39) and +0.94% for SurfaceWorld (1.800 s L36, 1.817 s L39). All commands used the CPU 0–11 / 6 GiB limits; the full tier had a 29-minute timeout.
|
||||
|
||||
## Source stamp and owner identity
|
||||
|
||||
`BuildLinux.sh` hashes a sorted manifest covering Core inputs, standalone pipeline sources and generated wrappers, build files, and the source-stamp helper. It includes the Unreal adapter when that adapter is enabled for compilation. The manifest SHA-256 is compiled as `VOXELFORGE_SOURCE_STAMP`; headless and capture executables print it as their first runtime line. The runner compares the expected stamp before and after execution, probes every available binary at both ends, and stops on a missing or mismatched stamp. Summaries record the stamp, binary SHA-256, input JSON SHA-256, resolved recipe, style and layout ID.
|
||||
|
||||
Current stamp: `644cd7ed510034b1855f249cc1dba338da2336c4071069792663c88810b59b90`. The canonical owner JSON SHA-256 is `7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183`, unchanged from L37. FI and SW each produced 12 matching W1/W12 hashes, and both signatures match the saved L37 controls. No Core geometry source changed; C++ edits add build stamps and resolved-layout evidence.
|
||||
|
||||
The instrumented L36 cost baseline was exported from commit `8be1c4d` and carries its own recorded stamp: `28c74aac57643360cb563e07bae278e2e14eceda0b63a3a50cf5c9876003e765`.
|
||||
|
||||
## Negative controls
|
||||
|
||||
| Control | Evidence |
|
||||
|---|---|
|
||||
| Core edit after build | Runner refused the stale binary; original source bytes were restored. |
|
||||
| Style-0 fixture named `owner_world.json` | Resolved snapshot exposed FI Style 0; the owner assertion requiring Style 2 rejected it. |
|
||||
| All-black JPEG | Rejected at stddev 0 and coverage 0 (floors: 2.0 and 0.001). |
|
||||
| L30 terrace rings | Rejected: 3.125 m flat run followed by a 0.243 m step. |
|
||||
| L31 flattened floor | Rejected: amplitude range 0.000252 m. |
|
||||
| L35 wall ribs / L36 floor washboard | Detected amplitudes 0.0918 m / 0.0943 m against 0.035 m / 0.015 m limits. |
|
||||
| L38 owner step | Detected 0.2443 m against the 0.100 m adjacent-profile limit. Fitted period 7.875 m is diagnostic only. |
|
||||
|
||||
## Cleanup and remaining diagnostic
|
||||
|
||||
The duplicate L13/L9 runner was removed. The crease/fragment audit now decodes each owner mesh once for fragment, near-crease and overview scans. Style-override artifacts use `style-override-*`; `owner` runs use the canonical input with asserted resolved styles. Capture quality decodes pixels and enforces variance and coverage floors, including on JPEGs.
|
||||
|
||||
The current FI owner has six small fragments. That count remains diagnostic because a strict zero-fragment gate fails on the L37 owner; a baseline-backed fragment threshold remains open. The merged audit still reports the count alongside near and overview crease results.
|
||||
|
||||
## Failed attempts
|
||||
|
||||
- `full-final` stopped after determinism with a missing `VerificationRunner.bounded_run` method reference; switched to the shared bounded runner.
|
||||
- `full-final2` could not find the historical temp tree's `Tools/verify` directory; created it before copying the stamp helper and computed the stamp from the temp repository root.
|
||||
- `full-final3` was interrupted during an expected long FI W1 cell after the log went quiet; rerun allowed the cell to finish.
|
||||
- `full-final4` passed all 30 determinism cells, then exposed an expected-name typo for the L36 binary (`...-stamped-clang` vs `...-stamped`). The corrected full run passed the baseline build and all cost pairs.
|
||||
|
||||
How-to instructions are in [`Standalone/Tools/verify/README.md`](../../../Tools/verify/README.md). Logs and JSON summaries are in `Saved/Linux/l39/`.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted. I re-ran `verify.py fast` myself on the final tree: PASS in 162.7 s, the stamp matched, owner hashes match
|
||||
L37 for both recipes at W1/W12, and the resolved style map asserted {FI: 2, SW: 1}. Note for readers: the
|
||||
`style=[2]` on SurfaceWorld rows is the style of the single FI→SW passage, which belongs to the upper (FI)
|
||||
strate. The owner world has no SW-owned passage, so "SW Style 1" never shapes a tunnel today. Earlier rounds'
|
||||
"owner-sw Style 1" captures show the lower end of the Style 2 passage.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Research R6 — director, generators, and mesher in metres
|
||||
|
||||
Read-only review of the current checkout and L20/L21/L22/L29, R2, and R5 notes. No builds or tests were run.
|
||||
|
||||
**R2’s layout-chunk split is already resolved in the checked source.** `AttachValidatedWorldAssembly` computes one physical `LayoutChunkWidthSubvoxels` from `StrateLayoutChunkSizeMeters`, passes it to both `BuildSurfaceStratumPlan` and `BuildFloatingIslandStratumPlan`, and writes the same manager/dispatch width into the resolved snapshot (L22). Do not apply the FI owner/Core ratio to layout bounds again. FI still uses that ratio to convert its *generator parameters* and passage dimensions from owner voxels; that separate authoring-unit leak remains.
|
||||
|
||||
## 1. Current leaks
|
||||
|
||||
The closest current match to the three layers is: director work in `VoxelStrateManager.cpp::Initialize` and `VoxelForgeCoreAdapter.cpp::AttachValidatedWorldAssembly`; field evaluators in `Heightfield2D::point`, `FloatingIslandFieldV1::densitySubvoxels`, and legacy `UVoxelGenerator::GetDensityAt`; shared composition in `FStandaloneShapeWorld::ApplyResolvedStrateComposition`; extraction in `World::generateTile` and `UVoxelMarchingCubesMesher::GenerateMesh`. `VoxelWorld::BuildDesiredTiles` is the scheduling side of the director. The table lists where these boundaries still cross.
|
||||
|
||||
| Leak | Current location and consequence |
|
||||
|---|---|
|
||||
| Director lookup inside field evaluation | `Standalone/Core/src/ShapeWorldInternal.h::FStandaloneShapeWorld::DensityAt`, `NativeSurfaceDensityAt`, `NativeFloatingIslandDensityAt`, and `ClassifyBoxDetailed` query `UVoxelStrateManager` and select native SurfaceWorld, native FloatingIslands, or legacy evaluation while workers sample. `Source/VoxelForge/Private/VoxelGenerator.cpp::UVoxelGenerator::GetDensityAt` also routes each sample through manager type/params. The resolved layout exists, but is not yet the sole dispatch input. |
|
||||
| No common generator API/format | `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h::Heightfield2D` and `FloatingIslandFieldV1` have distinct recipe/evaluator APIs; the legacy path in `VoxelGenerator.cpp`, operator stacks, and morphology is a third. `FStandaloneShapeWorld` is their adapter and shared composition path at once. A new generator cannot currently implement one common contract. |
|
||||
| Units cross boundaries repeatedly | `VoxelStrateDefinition.h::StrateHeightInChunks`, `VoxelSettings.h::InterStrateGapChunks`, and many `VoxelStrateTypes.h` shape/passage values are authored in chunks or voxels. `VoxelStrateManager.cpp::Initialize` converts metre chunk width to owner voxels; `VoxelForgeCoreAdapter.cpp::WorldAssemblyFixed` packs values as 1/256 Core voxel; `BuildFloatingIslandStratumPlan` and `AttachValidatedWorldAssembly` use owner/Core ratios for FI dimensions and passages. Snapshot bounds are micrometres, but recipe anchors are still labelled 1/256 Core voxel, and `voxelSizeMicrometres` is currently filled with the same Core size for every strate. `VoxelStrateDefinition.h` still documents 16 chunks as 64 m at 4 m/chunk although the current 8 m setting makes that 128 m. |
|
||||
| Generator policy remains in shared composition | `ShapeWorldInternal.h::ApplyResolvedStrateBoundaryBand` branches on generator-kind strings for FI air-gap and surface/landing behavior; the density/classification functions above branch on generator types. These exceptions should become explicit resolved interface or generator capabilities, not string tests in the common evaluator. |
|
||||
| Route and passage logic is only partly shared | `VoxelForgeCoreAdapter.cpp::BuildFiniteSurfaceRoute` plans the SurfaceWorld route. `BuildFloatingIslandStratumPlan` embeds passage/spiral dimensions into the FI anchor root. `ResolvedLayoutInternal.h::PlanResolvedLayoutPassagesV1` still tests SurfaceWorld and FloatingIslands to place mouths and handle the FI root; `ShapeWorldInternal.h::ApplyResolvedStratePassageCarves` applies the shared carve but also contains route/support policy. Legacy passage behavior remains in `UVoxelGenerator::GetDensityAt`. A new generator should not need a route implementation. |
|
||||
| Tile builder knows generator recipes | `Standalone/Core/src/VoxelForgeCore.cpp::World::generateTile` scans SurfaceWorld feature IDs 2/4, refines their resolution, limits a native SurfaceWorld density band, and selects/skips far-horizon repair based on SurfaceWorld/FloatingIslands plan presence. The generic mesher should receive bounds, surface hints, and a sampling request, not recipe IDs. |
|
||||
|
||||
The shared ceiling/content/bedrock records and Hard/Gradient/Interleaved evaluation are real foundations (`ResolvedLayoutSnapshotV3`, `ApplyResolvedStrateBoundaryBand`, L29). Remaining work is to make their resolved physical records authoritative and remove generator-name exceptions. The mesh-side boundary floor/ceiling patches in `VoxelForgeCore.cpp`/`FarShellsInternal.h` still duplicate part of that surface policy.
|
||||
|
||||
## 2. Generator contract
|
||||
|
||||
The director resolves an immutable world snapshot before jobs are queued. A generator is constructed once for a resolved strate and receives:
|
||||
|
||||
- Stable world seed, strate ID, generator/recipe/schema/evaluator versions, and that strate’s immutable parameters.
|
||||
- Physical half-open bounds `[bottom, top)` and lattice origin in integer micrometres; the resolved ceiling/content/bedrock and adjacent-interface records that touch the strate.
|
||||
- Its own `voxelSizeMicrometres` (25 cm by default), independent of the 8 m layout chunk and of the tile key. The request gives the physical query bounds plus the mesher’s exact halo. LOD may guide work, but the same physical coordinate and recipe must return the same field value at every LOD.
|
||||
- The shared deterministic library: versioned noise, fixed-point conversion, common distance/shape helpers, and conservative bounds utilities. Resolved passage descriptors and style are consumed by shared composition; the generator need not plan or carve the inter-strate route.
|
||||
|
||||
Concrete evaluator shape: `densityAt(xUm, yUm, zUm) -> signed field value`, with one documented convention (recommended: negative is solid, zero is the surface, positive is air). Keep magnitude units stable and documented; metre-scaled signed distance is the target for shared band composition. Provide a conservative support/influence AABB or “unbounded/unknown”; an interval/classification for a requested AABB (`solid`, `air`, `mixed`, or `unknown`); and an optional detail envelope (smallest safe sample spacing). A SurfaceWorld-like generator may also return semantic ground/cap surfaces in metres. Caches and feature tile lists may accelerate these queries, but are not part of output identity.
|
||||
|
||||
**A generator must never** query the manager, UObject state, chunk index, season, or a neighbour while evaluating; choose strate bounds, gaps, transitions, bedrock/ceiling ownership, or route geometry; mutate input state; generate triangles; or vary a field by tile order, worker, or LOD. Unknown proof means evaluate, never skip. The director selects the evaluator once; shared composition applies the interface bands, passages and edits; the mesher consumes the composed field and generic hints. This is enough for TunnelNetwork or a cavern generator to be added without edits to the director’s sampling loop or mesher.
|
||||
|
||||
## 3. Ordered migration
|
||||
|
||||
“No” means existing-world density/mesh/output hashes stay identical. A new schema may change manifest identity only when deliberately versioned; do not hide such a change behind a claimed pure refactor. Size estimates are one engineer’s rough implementation effort: S 1–2 days, M 3–5 days, L 1–2 weeks.
|
||||
|
||||
| Step | Change and verification tier | Changes output? | Main risk | Size |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | Freeze the current source stamp, canonical owner FI Style 2/SW Style 1 resolved snapshots, and existing hashes as the comparison set. Use R5 **fast** to establish the baseline; medium/full remain promotion gates. | No | Incorrect/stale binaries or mislabeled style invalidate all comparisons. | XS |
|
||||
| 2 | Add a common immutable field interface and wrap the current SurfaceWorld, FloatingIslands, and legacy evaluators without changing their arithmetic. Bind the evaluator once per resolved strate/tile, not via a manager lookup per density sample. Run **fast** hash parity; **medium** before merge. | No; hashes must match. | Virtual/function dispatch cost; accidentally changing float/fixed-point call order. | M |
|
||||
| 3 | Make the resolved metre layout the sole runtime authority: resolve height, gap, transition width, band bounds, seed, and passage settings before scheduling. Keep chunk indices only as a compatibility projection. Convert the existing 8 m chunk and L29 bands exactly. R2’s FI/SW split is handled here by the already-present common layout width; retain owner/Core ratios only in the legacy FI parameter adapter. Run **fast + medium** (layout, transition, passage checks). | No for equivalent existing layouts; hashes must match. | Negative-Z floor/rounding, old season snapshots, adapter and manager disagreeing about interval edges. | M |
|
||||
| 4 | Introduce metre-valued authored height/gap/transition/passage dimensions and a per-strate `voxelSizeMeters` default of 0.25. Add a versioned conversion from old chunk/voxel assets based on each legacy route’s established unit; do not assume the source unit is recorded on the asset or reinterpret old numeric values as metres. Keep 8 m layout chunk independent. **Fast + medium** for old assets; full when enabling non-default resolutions. | No for converted old assets. Yes for changed metre values or voxel size; bump layout/recipe/evaluator identity. | Asset migration and resolution-dependent detail; changing 25 cm to 12.5 cm can mean about 8× samples per physical volume. | L |
|
||||
| 5 | Route all new and existing passage requests through resolved per-strate configs and a shared route plan. First copy existing SW/FI route anchors exactly; replace FI-root and SurfaceWorld placement branches with explicit resolved anchor/surface capabilities in follow-up slices. Run **fast + medium** passage matrix and route parity. | No for exact route replay; hashes must match. New geometry/style semantics are Yes and need a generator/layout version bump. | Walkability/support, deterministic placement, route planning cost. | M |
|
||||
| 6 | Move remaining boundary exceptions into explicit interface records/capabilities. Keep ceiling → content → breakable bedrock per strate and the upper strate’s Hard/Gradient/Interleaved rule. Preserve L29 samples and passage-mouth fade exactly. Run **fast + medium** transition, hole, and passage gates. | No; hashes must match. | Interface edge ownership and a false solid/air shortcut can open a hole. | M |
|
||||
| 7 | Standardize culling outputs: conservative support bounds, interval certificates, unknown fallback, and detail envelope. Adapt existing field proofs first; replace type switches only when the new proof matches brute-force/classification controls. Run **fast**, then **medium** proof negatives and cost pairs. | No; hashes must match. | Unsound certificates skip density work; per-voxel bounds calls hurt performance. | M/L |
|
||||
| 8 | Make `World::generateTile` and the mesher consume generic feature boxes, semantic surfaces, field bounds, and sample-spacing hints. Move current SurfaceWorld refinements behind capabilities while keeping their algorithms byte-for-byte. Run **fast + medium** ring/shape checks. | No for dispatch-only refactor; hashes must match. | Cross-LOD seams, tile halos, mixed-resolution tile boundaries, Unreal mesher assumptions. | L |
|
||||
| 9 | Add an opt-in metre-native TunnelNetwork/cavern recipe through the contract and generic mesher interface. Run **medium** on the new recipe and R5 **full** before enabling it in owner layouts. | Yes for worlds selecting it; bump that generator’s recipe/evaluator version. | New density cost and output identity. | M/L |
|
||||
| 10 | Implement the L20 shared, LOD-stable surface/transition representation to remove measured ring ledges. Run **medium** ring/shape checks and R5 **full** on the final tree. | Yes; bump the mesher/layout version and establish new golden hashes. | Surface topology and cross-LOD transitions change; coarse sampling may erase thin bands. | L |
|
||||
|
||||
R5’s draft timings are about **3–4 min fast**, **8–10 min additional medium**, and **32–34 min full**, excluding builds. Run fast for every step: output-neutral slices must match existing hashes; output-changing slices must match their new versioned golden hashes. Medium is required before promoting layout, route, culling, or mesher changes. Full is final-source evidence after output-changing versions and golden hashes are reviewed. Include the actual owner configs and assert their resolved styles, as R5 requires; do not treat style-override fixtures as owner evidence.
|
||||
|
||||
### Cross-cutting risks
|
||||
|
||||
- **Determinism:** convert metres to integer micrometres once; use a specified tie rule and mathematical floor for negative coordinates; hash resolved parameters, voxel size, generator versions, and interface records in stable order. Keep floating-point contraction/platform checks for the compatibility evaluator.
|
||||
- **Per-voxel cost:** eliminate manager/recipe selection from the hot call, pre-resolve field objects and tile candidates, and keep unknown proofs conservative. Benchmark actual FI2 and SW1 with R5 cost pairs at medium/full. A finer voxel size is cubic work growth, not a cheap resolution toggle.
|
||||
- **Unreal adapter:** `VoxelForgeCoreAdapter.cpp` currently serializes UObject parameters into Core fixed units; new reflected metre fields need versioned asset migration and UHT/UBT validation. R2 also recorded that recent adapter changes had no Unreal build evidence. Static research cannot establish that compile boundary.
|
||||
|
||||
## Open question for the owner
|
||||
|
||||
Should existing voxel-authored assets retain their current physical dimensions through a per-generator compatibility conversion? Recommended: yes, then author new recipes in metres. Reinterpreting old values at 25 cm would rescale worlds and require broad output-version bumps.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Shared notes (researcher R6 <-> implementer L39), newest at bottom
|
||||
|
||||
|
||||
## R5 read-only architecture findings (2026-10-23)
|
||||
|
||||
Inspected `Standalone/BuildLinux.sh`, `Build.ps1`, `Core/core_sources.txt`, `Source/StreamBenchmarkHeadless.cpp`, `Source/LinuxOffscreenCapture.cpp`, `data/owner_world.json`, the L37 passage/invariant/legacy/ring/capture/crease/fragment runners, `Saved/Linux/l37/mouth-curves-run/layout-owner-fi.json`, and the R5/L37 reports. No source was changed and no binary was run.
|
||||
|
||||
- `BuildLinux.sh` reads `Core/core_sources.txt`, generates wrapper translation units under `Standalone/Build`, compiles the Core and compatibility objects, then links `VoxelForgeCoreHeadless-$VARIANT`. It separately compiles `LinuxOffscreenCapture.cpp` into `VoxelForgeOffscreenCapture-$VARIANT`. The generated wrappers/objects are build outputs; the stamp manifest should hash the listed original source paths, `core_sources.txt`, both standalone executable sources, and the build scripts. No stamp exists today. The script already enforces `taskset -c 0-11` plus `prlimit --as=6442450944` and rejects `--jobs` above 12.
|
||||
- There are two executable entry points to stamp. `StreamBenchmarkHeadless.cpp` parses options before its first current log (`[HeadlessConfig]`); `LinuxOffscreenCapture.cpp` checks `argc` before logging. Print the source stamp at the start of both `main`s, before parse/validation. A cheap stamp-only option on each binary would let the shared runner reject stale binaries before expensive work.
|
||||
- The canonical JSON contains FI `PassageConfig.Style=2` and SW `PassageConfig.Style=1`. The checked-in L37 capture and passage runners label `linux25-owner-*-style0.json` as `owner-*`; those are style overrides. In an existing resolved snapshot, each `strates[]` entry has `generator_kind`, `recipe_id`, and IDs, while `passages[]` has geometry, but neither exposes the effective style. The requested style assertion cannot be made from the current snapshot: add the resolved passage style per strate to its serialization, then assert FI=2/SW=1. `stable_identity` is the existing layout ID (`resolved-layout-v3-fnv64:…`); the recipe audit log reports recipe but not style.
|
||||
- The L37 runners each create their own subprocess wrappers and summaries. A shared helper can centralize the constrained invocation, stamp-only preflight, expected-stamp recomputation, binary/input SHA-256, resolved-layout extraction, and a common summary row (stamp, binary hash, input hash, actual recipe/style/layout ID, elapsed time). Recompute the manifest after the tier. Resolve the canonical layout once per owner case and use that same snapshot for all gates/captures.
|
||||
- `CapturePassageStandardSetL37.py` and `CaptureMouthOverviewL37.py` only check JPEG size. An all-black JPEG therefore passes. Add decoded-pixel variance/coverage validation (without PIL, which the earlier research could not import) and include a black-image negative control.
|
||||
- `RunMouthCreaseAuditL37.py` imports the fragment audit. For each candidate it reads the mesh once in `BASE.audit`, then `visible_crease` reads it again for the near view and again for overview. A combined analyzer can decode once, then share triangles/buckets for fragment, crease, and both views. Existing overview check matters: L37 passed near at 4.40° but measured 25.54° against 20° at overview.
|
||||
- `RunRingAuditsL37.py` already keeps the L30 terrace and L31 flat meshes as live controls; the full LOD0–8 work is separate from targeted LOD0 checks. The R5 split can reuse `AuditRingTerraceL32.py` directly for fast LOD0 plus those controls.
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "SourceStamp.h"
|
||||
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <png.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
@@ -810,6 +813,8 @@ void main()
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
VoxelForgePrintSourceStamp();
|
||||
if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0;
|
||||
if(argc<3)
|
||||
{
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]);
|
||||
@@ -1037,4 +1042,3 @@ int main(int argc,char** argv)
|
||||
HeightTint,Headlamp,TileEdges,
|
||||
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
#ifndef VOXELFORGE_SOURCE_STAMP
|
||||
#define VOXELFORGE_SOURCE_STAMP "UNSTAMPED"
|
||||
#endif
|
||||
|
||||
inline void VoxelForgePrintSourceStamp() noexcept
|
||||
{
|
||||
std::printf("[VoxelForgeSourceStamp] sha256=%s\n", VOXELFORGE_SOURCE_STAMP);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "SourceStamp.h"
|
||||
#include "OwnerWorldLoader.h"
|
||||
#include "StandaloneCoreScheduler.h"
|
||||
#include "StreamBenchmarkPipeline.h"
|
||||
@@ -316,6 +317,8 @@ namespace
|
||||
|
||||
int main(int argc,char** argv)
|
||||
{
|
||||
VoxelForgePrintSourceStamp();
|
||||
if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0;
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto coldStart=Clock::now();
|
||||
FOptions options;
|
||||
@@ -402,13 +405,33 @@ int main(int argc,char** argv)
|
||||
if(!roundTrip||roundTrip->ToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity)
|
||||
{if(error.empty())error="resolved-layout canonical serialization did not round-trip";
|
||||
std::fprintf(stderr,"Resolved-layout round trip failed: %s\n",error.c_str());return 2;}
|
||||
std::string resolvedStrateStyles="{";
|
||||
bool firstResolvedStrateStyle=true;
|
||||
for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition)continue;
|
||||
const auto resolvedStrate=std::find_if(resolvedLayout.strates.begin(),
|
||||
resolvedLayout.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==slot.StrateIndex;});
|
||||
if(resolvedStrate==resolvedLayout.strates.end())continue;
|
||||
const auto style=static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style);
|
||||
if(!firstResolvedStrateStyle)resolvedStrateStyles+=',';
|
||||
firstResolvedStrateStyle=false;
|
||||
resolvedStrateStyles.push_back('"');
|
||||
resolvedStrateStyles+=std::to_string(slot.StrateIndex);
|
||||
resolvedStrateStyles+="\":";
|
||||
resolvedStrateStyles+=std::to_string(style);
|
||||
}
|
||||
resolvedStrateStyles+='}';
|
||||
const std::string layoutOutputJson=layoutJson.substr(0,layoutJson.size()-1)
|
||||
+",\"resolved_strate_passage_styles\":"+resolvedStrateStyles+"}";
|
||||
if(!options.ResolvedLayoutOutputPath.empty())
|
||||
{
|
||||
if(!options.ResolvedLayoutOutputPath.parent_path().empty())
|
||||
std::filesystem::create_directories(options.ResolvedLayoutOutputPath.parent_path());
|
||||
std::ofstream output(options.ResolvedLayoutOutputPath,std::ios::binary);
|
||||
if(!output){std::fprintf(stderr,"Could not write resolved-layout snapshot: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
||||
output.write(layoutJson.data(),static_cast<std::streamsize>(layoutJson.size()));
|
||||
output.write(layoutOutputJson.data(),static_cast<std::streamsize>(layoutOutputJson.size()));
|
||||
if(!output){std::fprintf(stderr,"Resolved-layout snapshot write failed: %s\n",options.ResolvedLayoutOutputPath.c_str());return 2;}
|
||||
}
|
||||
const auto controls=VoxelForgeCore::AuditResolvedLayoutValidatorV3();
|
||||
@@ -423,6 +446,17 @@ int main(int argc,char** argv)
|
||||
VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError);
|
||||
std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n",
|
||||
resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size());
|
||||
for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
||||
{
|
||||
if(!slot.Definition)continue;
|
||||
const auto resolvedStrate=std::find_if(resolvedLayout.strates.begin(),
|
||||
resolvedLayout.strates.end(),[&](const auto& value)
|
||||
{return value.strateIndex==slot.StrateIndex;});
|
||||
if(resolvedStrate!=resolvedLayout.strates.end())
|
||||
std::printf("[ResolvedStratePassageStyle] strate_index=%d generator_kind=%s style=%u\n",
|
||||
slot.StrateIndex,resolvedStrate->generatorKind.c_str(),
|
||||
static_cast<std::uint32_t>(slot.Definition->PassageConfig.Style));
|
||||
}
|
||||
const auto requestedRecipe=options.FloatingIslandsRecipe
|
||||
?std::string_view("floating-islands"):std::string_view("surface-world");
|
||||
const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# VoxelForge verification
|
||||
|
||||
Build stamped Linux executables and run one of the three verification tiers from the repository root. Artifacts and logs go under `Saved/Linux/l39` by default.
|
||||
|
||||
```sh
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s env VOXELFORGE_BUILD_ROOT=Saved/Linux/l39/build VOXELFORGE_BIN_DIR=Saved/Linux/l39/bin Standalone/BuildLinux.sh --compiler=clang++-19 --variant=l39-clang --jobs=12
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s env VOXELFORGE_BUILD_ROOT=Saved/Linux/l39/build VOXELFORGE_BIN_DIR=Saved/Linux/l39/bin Standalone/BuildLinux.sh --compiler=g++ --variant=l39-gcc --jobs=12
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s python3 -B Standalone/Tools/verify/verify.py fast
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s python3 -B Standalone/Tools/verify/verify.py medium
|
||||
taskset -c 0-11 prlimit --as=6442450944 -- timeout 1740s python3 -B Standalone/Tools/verify/verify.py full
|
||||
```
|
||||
|
||||
`fast` checks only canonical `Standalone/data/owner_world.json`, asserts FI Style 2 and SW Style 1 from resolved snapshots, runs passage wall/floor ripple and coverage gates, the adjacent-profile step limit, W1/W12 hash comparisons, and LOD0 ring profiles with the L30/L31 controls. It also exercises stale-source, style-mislabel, black-capture, historical ripple, and L38 step controls.
|
||||
|
||||
`medium` adds the Clang style-override matrix, transitions, route/proof audits, LOD0–8 rings, and canonical-owner captures, shape, near/overview crease, and fragment checks. Run `medium` before `full`. Full reuses that evidence only when the source stamp, verification-tool hashes, binaries, inputs, logs, and audit-script hashes still match; otherwise it reruns the Clang medium work. It adds the owner/style/transition/proof matrix with GCC, the 30-cell determinism matrix, and five interleaved L36/L39 cost pairs per recipe. The final captures stay in the L39 output directory.
|
||||
|
||||
`BuildLinux.sh` hashes a sorted manifest of Core inputs, standalone sources and generated wrappers, build files, and optionally the Unreal adapter when `VOXELFORGE_INCLUDE_UNREAL_ADAPTER=1`. The manifest hash is compiled into the headless and offscreen binaries and printed before any other runtime log line. Each tier probes every available built executable at startup and re-probes it at completion, rejecting stale stamps or binary changes. Summaries store tool hashes, binary and JSON SHA-256 values, resolved recipe/style/layout identity, commands, elapsed time, and audit-script hashes.
|
||||
|
||||
Each tier stops at its first failed gate. Every process is limited to CPUs 0–11 and 6 GiB of address space; individual build and audit processes time out at 30 minutes. Summary files are `fast-summary.json`, `medium-summary.json`, and `full-summary.json`.
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reject empty and nearly black captures by decoding pixels and measuring coverage."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import math
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
|
||||
|
||||
def inspect(path: Path) -> tuple[bool, str]:
|
||||
probe = subprocess.run([*LIMIT, "timeout", "30s", "ffmpeg", "-nostdin",
|
||||
"-hide_banner", "-loglevel", "error", "-i", str(path), "-frames:v", "1",
|
||||
"-f", "rawvideo", "-pix_fmt", "rgb24", "pipe:1"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=30, check=False)
|
||||
if probe.returncode != 0 or not probe.stdout or len(probe.stdout) % 3:
|
||||
return False, f"decode_failed exit={probe.returncode} bytes={len(probe.stdout)}"
|
||||
raw = probe.stdout
|
||||
count = len(raw) // 3
|
||||
total = 0
|
||||
square_total = 0
|
||||
covered = 0
|
||||
for index in range(0, len(raw), 3):
|
||||
luminance = (77 * raw[index] + 150 * raw[index + 1] + 29 * raw[index + 2]) >> 8
|
||||
total += luminance
|
||||
square_total += luminance * luminance
|
||||
covered += luminance > 12
|
||||
mean = total / count
|
||||
variance = max(0.0, square_total / count - mean * mean)
|
||||
deviation = math.sqrt(variance)
|
||||
coverage = covered / count
|
||||
passed = deviation >= 2.0 and coverage >= 0.001
|
||||
return passed, (f"pixels={count} mean_luma={mean:.3f} stddev_luma={deviation:.3f} "
|
||||
f"coverage_luma_gt12={coverage:.5f} thresholds=(stddev>=2.0,coverage>=0.001)")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("image", type=Path)
|
||||
args = parser.parse_args()
|
||||
if not args.image.is_file():
|
||||
print(f"[CaptureQuality] image={args.image} status=FAIL reason=missing")
|
||||
return 1
|
||||
passed, detail = inspect(args.image)
|
||||
print(f"[CaptureQuality] image={args.image} {detail} status={'PASS' if passed else 'FAIL'}")
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply current ripple limits to retained L35 wall-rib and L36 washboard logs."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def records(path: Path) -> list[dict[str, float | str]]:
|
||||
result = []
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if "[PassageRippleAudit]" not in line:
|
||||
continue
|
||||
values = dict(re.findall(r"(?:^| )([a-z_]+)=([-0-9.]+)", line))
|
||||
def ratio(key: str) -> str:
|
||||
match = re.search(rf"(?:^| ){key}=([0-9]+)/([0-9]+)", line)
|
||||
return (str(int(match.group(1)) / max(1, int(match.group(2))))
|
||||
if match else "nan")
|
||||
try:
|
||||
item: dict[str, float | str] = {
|
||||
"local_peak_amplitude_m": float(values.get(
|
||||
"local_peak_amplitude_m", values.get("amplitude_m", "nan"))),
|
||||
"floor_amplitude_m": float(values.get("floor_amplitude_m", "nan")),
|
||||
"wall_coverage": float(ratio("coverage")),
|
||||
"floor_coverage": float(ratio("floor_coverage")),
|
||||
"status": "FAIL" if "status=FAIL" in line else "PASS",
|
||||
}
|
||||
except ValueError:
|
||||
continue
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("kind", choices=("wall-ribs", "floor-washboard"))
|
||||
parser.add_argument("log", type=Path)
|
||||
args = parser.parse_args()
|
||||
entries = records(args.log)
|
||||
if args.kind == "wall-ribs":
|
||||
candidates = [entry for entry in entries if
|
||||
float(entry["local_peak_amplitude_m"]) > 0.035
|
||||
or float(entry["wall_coverage"]) < 0.75]
|
||||
metric = max((float(entry["local_peak_amplitude_m"]) for entry in candidates),
|
||||
default=0.0)
|
||||
detail = f"max_wall_amplitude_m={metric:.4f} threshold_m=0.035"
|
||||
else:
|
||||
candidates = [entry for entry in entries if
|
||||
float(entry["floor_amplitude_m"]) > 0.015
|
||||
or float(entry["floor_coverage"]) < 0.75]
|
||||
metric = max((float(entry["floor_amplitude_m"]) for entry in candidates),
|
||||
default=0.0)
|
||||
detail = f"max_floor_amplitude_m={metric:.4f} threshold_m=0.015"
|
||||
detected = bool(candidates)
|
||||
print(f"[HistoricalRippleControl] kind={args.kind} log={args.log} "
|
||||
f"records={len(entries)} {detail} detected={str(detected).lower()} "
|
||||
f"status={'PASS' if detected else 'FAIL'}")
|
||||
return 0 if detected else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure adjacent route-relative mouth-profile steps; period fits are diagnostic."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
PROFILE = re.compile(
|
||||
r"\[PassageFloorRippleSample\] id=(\S+) distance_m=([-0-9.]+).*?"
|
||||
r"residual_m=([-0-9.]+)")
|
||||
RIPPLE = re.compile(r"\[PassageRippleAudit\] id=(\S+).*?period_m=([-0-9.]+)")
|
||||
STEP_LIMIT_M = 0.100
|
||||
MAX_ADJACENT_DISTANCE_M = 0.750
|
||||
|
||||
|
||||
def measure(text: str) -> dict[str, object]:
|
||||
profiles: dict[str, list[tuple[float, float]]] = defaultdict(list)
|
||||
for match in PROFILE.finditer(text):
|
||||
passage, distance, residual = match.groups()
|
||||
profiles[passage].append((float(distance), float(residual)))
|
||||
|
||||
adjacent: list[tuple[float, str, float, float]] = []
|
||||
group_counts: dict[str, int] = {}
|
||||
for key, samples in profiles.items():
|
||||
samples.sort()
|
||||
group_counts[key] = len(samples)
|
||||
for before, after in zip(samples, samples[1:]):
|
||||
delta_d = abs(after[0] - before[0])
|
||||
if delta_d <= MAX_ADJACENT_DISTANCE_M:
|
||||
adjacent.append((abs(after[1] - before[1]), key, before[0], after[0]))
|
||||
|
||||
periods = {passage: float(period) for passage, period in RIPPLE.findall(text)}
|
||||
maximum = max((item[0] for item in adjacent), default=0.0)
|
||||
return {
|
||||
"maximum_adjacent_step_m": maximum,
|
||||
"adjacent_pairs": len(adjacent),
|
||||
"profile_groups": len(profiles),
|
||||
"profile_group_counts": group_counts,
|
||||
"fitted_periods_m": periods,
|
||||
"passed": bool(adjacent) and maximum <= STEP_LIMIT_M,
|
||||
"worst": max(adjacent, default=None),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("log", type=Path)
|
||||
parser.add_argument("--expect-fail", action="store_true",
|
||||
help="pass only when the adjacent-step limit is exceeded")
|
||||
args = parser.parse_args()
|
||||
result = measure(args.log.read_text(encoding="utf-8", errors="replace"))
|
||||
detected = not bool(result["passed"])
|
||||
ok = detected if args.expect_fail else not detected
|
||||
print(f"[OwnerAdjacentProfileStep] log={args.log} pairs={result['adjacent_pairs']} "
|
||||
f"groups={result['profile_groups']} max_step_m={result['maximum_adjacent_step_m']:.6f} "
|
||||
f"limit_m={STEP_LIMIT_M:.3f} max_adjacent_distance_m={MAX_ADJACENT_DISTANCE_M:.3f} "
|
||||
f"fitted_periods_m={result['fitted_periods_m']} "
|
||||
"period_role=diagnostic "
|
||||
f"detected={str(detected).lower()} status={'PASS' if ok else 'FAIL'}")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,396 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared source-stamp, process, and per-run evidence helpers for verify.py."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from source_stamp import source_stamp
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
OUT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l39"))
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
RECIPE_RE = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||
re.M)
|
||||
STAMP_RE = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$")
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def script_hash(path: Path) -> str:
|
||||
return sha256_file(path) if path.is_file() else "MISSING"
|
||||
|
||||
|
||||
def verification_tool_hashes() -> dict[str, str]:
|
||||
return {str(path): sha256_file(path) for path in sorted(
|
||||
Path(__file__).resolve().parent.glob("*.py"))}
|
||||
|
||||
|
||||
def current_stamp() -> str:
|
||||
include_unreal = os.environ.get("VOXELFORGE_INCLUDE_UNREAL_ADAPTER", "0") == "1"
|
||||
return source_stamp(ROOT, include_unreal)
|
||||
|
||||
|
||||
def first_stamp(text: str) -> str | None:
|
||||
first = text.splitlines()[0] if text.splitlines() else ""
|
||||
match = STAMP_RE.fullmatch(first)
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def bounded_run(command: list[str], *, timeout: int = 1800,
|
||||
cwd: Path = ROOT, env: dict[str, str] | None = None,
|
||||
stdout: Any = subprocess.PIPE) -> subprocess.CompletedProcess[str] | None:
|
||||
try:
|
||||
return subprocess.run([*LIMIT, "timeout", f"{timeout}s", *map(str, command)],
|
||||
cwd=cwd, env=env, stdout=stdout, stderr=subprocess.STDOUT,
|
||||
text=True, encoding="utf-8", errors="replace", timeout=timeout,
|
||||
check=False)
|
||||
except subprocess.TimeoutExpired:
|
||||
return None
|
||||
|
||||
|
||||
class VerificationRunner:
|
||||
def __init__(self, tier: str):
|
||||
self.tier = tier
|
||||
self.started = time.monotonic()
|
||||
self.expected = current_stamp()
|
||||
self.tool_hashes = verification_tool_hashes()
|
||||
self.runs: list[dict[str, Any]] = []
|
||||
self.steps: list[dict[str, Any]] = []
|
||||
self.binary_hashes: dict[str, str] = {}
|
||||
self.binary_stamp_overrides: dict[str, str] = {}
|
||||
self.reused_tiers: list[dict[str, Any]] = []
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def assert_current_tree(self, point: str) -> None:
|
||||
now = current_stamp()
|
||||
if now != self.expected:
|
||||
raise RuntimeError(
|
||||
f"source changed {point}: expected {self.expected}, recomputed {now}")
|
||||
tools_now = verification_tool_hashes()
|
||||
if tools_now != self.tool_hashes:
|
||||
raise RuntimeError(f"verification tool changed {point}: "
|
||||
f"expected {self.tool_hashes}, recomputed {tools_now}")
|
||||
|
||||
def reuse_passed_tier(self, path: Path, tier: str) -> bool:
|
||||
"""Reuse a completed tier only while all its source and evidence inputs match."""
|
||||
self.assert_current_tree(f"before reusing {tier}")
|
||||
if not path.is_file():
|
||||
return False
|
||||
try:
|
||||
summary = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
if (summary.get("tier") != tier or summary.get("status") != "PASS"
|
||||
or summary.get("source_stamp") != self.expected
|
||||
or summary.get("verification_tools") != self.tool_hashes):
|
||||
return False
|
||||
if any(row.get("status") not in ("PASS", "EXPECTED_FAIL")
|
||||
or row.get("source_stamp") != self.expected
|
||||
for row in summary.get("runs", [])):
|
||||
return False
|
||||
if any(row.get("status") != "PASS" for row in summary.get("checks", [])):
|
||||
return False
|
||||
for binary, digest in summary.get("binary_sha256", {}).items():
|
||||
binary_path = Path(binary)
|
||||
if not binary_path.is_file() or sha256_file(binary_path) != digest:
|
||||
return False
|
||||
for row in summary.get("runs", []):
|
||||
input_path = row.get("input_json")
|
||||
if input_path:
|
||||
path_input = Path(input_path)
|
||||
if (not path_input.is_file()
|
||||
or sha256_file(path_input) != row.get("input_json_sha256")):
|
||||
return False
|
||||
log_path = row.get("log")
|
||||
if log_path and not Path(log_path).is_file():
|
||||
return False
|
||||
for script, digest in row.get("audit_scripts", {}).items():
|
||||
script_path = Path(script)
|
||||
if not script_path.is_file() or sha256_file(script_path) != digest:
|
||||
return False
|
||||
digest = sha256_file(path)
|
||||
self.runs.extend(summary.get("runs", []))
|
||||
self.steps.extend(summary.get("checks", []))
|
||||
self.binary_hashes.update(summary.get("binary_sha256", {}))
|
||||
self.reused_tiers.append({
|
||||
"tier": tier, "summary": str(path), "summary_sha256": digest,
|
||||
"elapsed_seconds": summary.get("elapsed_seconds"),
|
||||
"source_stamp": self.expected,
|
||||
})
|
||||
self.assert_current_tree(f"after reusing {tier}")
|
||||
print(f"[{self.tier}] reuse-{tier} PASS "
|
||||
f"prior_elapsed={summary.get('elapsed_seconds', 0):.1f}s "
|
||||
f"source_stamp={self.expected}", flush=True)
|
||||
return True
|
||||
|
||||
def probe_binary(self, binary: Path, expected_stamp: str | None = None) -> tuple[str, str]:
|
||||
self.assert_current_tree(f"before probing {binary.name}")
|
||||
if not binary.is_file():
|
||||
raise RuntimeError(f"missing executable: {binary}")
|
||||
digest = sha256_file(binary)
|
||||
expected_stamp = expected_stamp or self.binary_stamp_overrides.get(str(binary), self.expected)
|
||||
proc = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
output = proc.stdout or "" if proc is not None else ""
|
||||
embedded = first_stamp(output)
|
||||
if proc is None or proc.returncode != 0 or embedded is None:
|
||||
raise RuntimeError(f"{binary} has no usable source stamp; probe output: {output[:300]}")
|
||||
if embedded != expected_stamp:
|
||||
raise RuntimeError(
|
||||
f"stale executable refused: {binary.name} has {embedded}, expected {expected_stamp}")
|
||||
previous = self.binary_hashes.get(str(binary))
|
||||
if previous is not None and previous != digest:
|
||||
raise RuntimeError(f"executable changed during verification: {binary.name}; "
|
||||
f"was {previous}, now {digest}")
|
||||
self.binary_hashes[str(binary)] = digest
|
||||
self.assert_current_tree(f"after probing {binary.name}")
|
||||
return embedded, digest
|
||||
|
||||
def assert_binaries_current(self) -> None:
|
||||
for binary in tuple(self.binary_hashes):
|
||||
self.probe_binary(Path(binary))
|
||||
|
||||
@staticmethod
|
||||
def layout_metadata(layout_path: Path) -> tuple[str | None, tuple[int, ...], dict[str, Any]]:
|
||||
if not layout_path.is_file():
|
||||
return None, (), {}
|
||||
data = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
styles = tuple(sorted({int(passage["style"]) for passage in data.get("passages", [])
|
||||
if "style" in passage}))
|
||||
resolved_strate_styles = {str(index): int(value) for index, value in
|
||||
data.get("resolved_strate_passage_styles", {}).items()}
|
||||
strata = {int(stratum.get("strate_index", stratum.get("index", -1))): stratum
|
||||
for stratum in data.get("strates", [])}
|
||||
details = {
|
||||
"layout_id": data.get("stable_identity"),
|
||||
"layout_schema": data.get("schema_version"),
|
||||
"passage_styles": styles,
|
||||
"strata": {str(index): value.get("generator_kind")
|
||||
for index, value in strata.items()},
|
||||
"resolved_strate_styles": resolved_strate_styles,
|
||||
"generator_passage_styles": {
|
||||
value.get("generator_kind"): resolved_strate_styles[str(index)]
|
||||
for index, value in strata.items()
|
||||
if str(index) in resolved_strate_styles},
|
||||
"passages": [{"id": value.get("stable_id"),
|
||||
"upper": value.get("upper_strate_index"),
|
||||
"lower": value.get("lower_strate_index"),
|
||||
"style": value.get("style")}
|
||||
for value in data.get("passages", [])],
|
||||
}
|
||||
return details["layout_id"], styles, details
|
||||
|
||||
def run_cpp(self, name: str, binary: Path, data: Path, recipe: str,
|
||||
flags: list[str], *, expected_style: int | None = None,
|
||||
workers: int = 12, timeout: int = 1800,
|
||||
layout_path: Path | None = None,
|
||||
max_level: int = -1,
|
||||
expected_strate_styles: dict[str, int] | None = None,
|
||||
script_inputs: list[Path] | None = None,
|
||||
allow_nonzero: bool = False,
|
||||
expected_stamp: str | None = None) -> tuple[dict[str, Any], str]:
|
||||
stamp, binary_digest = self.probe_binary(binary, expected_stamp)
|
||||
if not data.is_file():
|
||||
raise RuntimeError(f"missing input JSON: {data}")
|
||||
data_digest = sha256_file(data)
|
||||
layout_path = layout_path or (OUT / "layouts" / f"{name}.json")
|
||||
layout_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
clean_flags = [flag for flag in flags if not flag.startswith("--resolved-layout-output=")]
|
||||
command = [str(binary), str(data), f"--workers={workers}",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||
f"--max-level={max_level}",
|
||||
f"--recipe={recipe}", *clean_flags, f"--resolved-layout-output={layout_path}"]
|
||||
log = OUT / f"{name}.log"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
proc = bounded_run(command, timeout=timeout, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace") if log.exists() else ""
|
||||
actual_stamp = first_stamp(output)
|
||||
recipe_matches = RECIPE_RE.findall(output)
|
||||
resolved_recipe = recipe_matches[-1][1] if recipe_matches else None
|
||||
layout_id, styles, layout_details = self.layout_metadata(layout_path)
|
||||
code: int | str = proc.returncode if proc is not None else "TIMEOUT"
|
||||
stamp_ok = actual_stamp == stamp
|
||||
recipe_ok = recipe_matches == [(recipe, recipe, "PASS")]
|
||||
style_ok = expected_style is None or styles == (expected_style,)
|
||||
actual_strate_styles = layout_details.get("resolved_strate_styles", {})
|
||||
if expected_strate_styles is not None:
|
||||
style_ok = style_ok and all(actual_strate_styles.get(str(index)) == value
|
||||
for index, value in expected_strate_styles.items())
|
||||
run_ok = (stamp_ok and recipe_ok and style_ok and proc is not None
|
||||
and (allow_nonzero or proc.returncode == 0))
|
||||
row: dict[str, Any] = {
|
||||
"name": name, "tier": self.tier, "status": "PASS" if run_ok else "FAIL",
|
||||
"elapsed_seconds": round(elapsed, 3), "exit_code": code,
|
||||
"source_stamp": stamp, "binary": str(binary),
|
||||
"compiler": "gcc" if "gcc" in binary.name else "clang",
|
||||
"binary_sha256": binary_digest, "input_json": str(data),
|
||||
"input_json_sha256": data_digest, "requested_recipe": recipe,
|
||||
"resolved_recipe": resolved_recipe,
|
||||
"requested_style": expected_style, "resolved_style": list(styles),
|
||||
"style_id": ",".join(f"passage-style-{value}" for value in styles) or None,
|
||||
"requested_strate_styles": expected_strate_styles,
|
||||
"resolved_strate_styles": actual_strate_styles,
|
||||
"resolved_generator_styles": layout_details.get("generator_passage_styles", {}),
|
||||
"layout_id": layout_id, "layout": layout_details,
|
||||
"log": str(log),
|
||||
"audit_scripts": {str(path): script_hash(path) for path in (script_inputs or [])},
|
||||
"command": command,
|
||||
}
|
||||
self.runs.append(row)
|
||||
self.assert_current_tree(f"after run {name}")
|
||||
self.print_step(row)
|
||||
if not run_ok:
|
||||
raise VerificationFailure(f"{name} failed (exit={code}, stamp={stamp_ok}, "
|
||||
f"recipe={recipe_ok}, style={style_ok}); see {log}")
|
||||
return row, output
|
||||
|
||||
def run_program(self, name: str, binary: Path, command: list[str], *,
|
||||
data: Path | None, recipe: str | None, style: int | None,
|
||||
layout_path: Path | None = None, timeout: int = 1800,
|
||||
require_stamp_line: bool = True,
|
||||
script_inputs: list[Path] | None = None) -> tuple[dict[str, Any], str]:
|
||||
stamp, digest = self.probe_binary(binary)
|
||||
data_digest = sha256_file(data) if data is not None and data.is_file() else None
|
||||
log = OUT / f"{name}.log"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
proc = bounded_run(command, timeout=timeout, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
layout_id, styles, details = self.layout_metadata(layout_path) if layout_path else (None, (), {})
|
||||
code: int | str = proc.returncode if proc is not None else "TIMEOUT"
|
||||
stamp_ok = not require_stamp_line or first_stamp(output) == stamp
|
||||
ok = proc is not None and proc.returncode == 0 and stamp_ok
|
||||
row = {
|
||||
"name": name, "tier": self.tier, "status": "PASS" if ok else "FAIL",
|
||||
"elapsed_seconds": round(elapsed, 3), "exit_code": code,
|
||||
"source_stamp": stamp, "binary": str(binary), "binary_sha256": digest,
|
||||
"compiler": "gcc" if "gcc" in binary.name else "clang",
|
||||
"input_json": str(data) if data else None, "input_json_sha256": data_digest,
|
||||
"requested_recipe": recipe, "resolved_recipe": recipe,
|
||||
"requested_style": style, "resolved_style": list(styles),
|
||||
"style_id": ",".join(f"passage-style-{value}" for value in styles) or
|
||||
(f"passage-style-{style}" if style is not None else None),
|
||||
"resolved_strate_styles": details.get("resolved_strate_styles", {}),
|
||||
"resolved_generator_styles": details.get("generator_passage_styles", {}),
|
||||
"layout_id": layout_id, "layout": details, "log": str(log),
|
||||
"audit_scripts": {str(path): script_hash(path) for path in (script_inputs or [])},
|
||||
"command": command,
|
||||
}
|
||||
self.runs.append(row)
|
||||
self.assert_current_tree(f"after run {name}")
|
||||
self.print_step(row)
|
||||
if not ok:
|
||||
raise VerificationFailure(f"{name} failed (exit={code}, stamp={stamp_ok}); see {log}")
|
||||
return row, output
|
||||
|
||||
def run_stage(self, name: str, script: Path, args: list[str], *,
|
||||
binary: Path, data: Path | None, recipe: str | None,
|
||||
style: int | None, layout_path: Path | None = None,
|
||||
env: dict[str, str] | None = None, timeout: int = 1800,
|
||||
script_inputs: list[Path] | None = None) -> tuple[dict[str, Any], str]:
|
||||
merged_env = os.environ.copy()
|
||||
if env:
|
||||
merged_env.update(env)
|
||||
return self.run_program(name, binary,
|
||||
[sys.executable, str(script), *args], data=data, recipe=recipe,
|
||||
style=style, layout_path=layout_path, timeout=timeout,
|
||||
require_stamp_line=False,
|
||||
script_inputs=[script, *(script_inputs or [])]) if env is None else \
|
||||
self._run_stage_with_env(name, script, args, binary, data, recipe, style,
|
||||
layout_path, merged_env, timeout, script_inputs)
|
||||
|
||||
def _run_stage_with_env(self, name: str, script: Path, args: list[str],
|
||||
binary: Path, data: Path | None, recipe: str | None,
|
||||
style: int | None, layout_path: Path | None,
|
||||
env: dict[str, str], timeout: int,
|
||||
script_inputs: list[Path] | None) -> tuple[dict[str, Any], str]:
|
||||
stamp, digest = self.probe_binary(binary)
|
||||
data_digest = sha256_file(data) if data is not None and data.is_file() else None
|
||||
log = OUT / f"{name}.log"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
proc = bounded_run([sys.executable, str(script), *args], timeout=timeout,
|
||||
env=env, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
layout_id, styles, details = self.layout_metadata(layout_path) if layout_path else (None, (), {})
|
||||
code: int | str = proc.returncode if proc is not None else "TIMEOUT"
|
||||
ok = proc is not None and proc.returncode == 0
|
||||
row = {
|
||||
"name": name, "tier": self.tier, "status": "PASS" if ok else "FAIL",
|
||||
"elapsed_seconds": round(elapsed, 3), "exit_code": code,
|
||||
"source_stamp": stamp, "binary": str(binary), "binary_sha256": digest,
|
||||
"compiler": "gcc" if "gcc" in binary.name else "clang",
|
||||
"input_json": str(data) if data else None, "input_json_sha256": data_digest,
|
||||
"requested_recipe": recipe, "resolved_recipe": recipe,
|
||||
"requested_style": style, "resolved_style": list(styles),
|
||||
"style_id": ",".join(f"passage-style-{value}" for value in styles) or
|
||||
(f"passage-style-{style}" if style is not None else None),
|
||||
"resolved_strate_styles": details.get("resolved_strate_styles", {}),
|
||||
"resolved_generator_styles": details.get("generator_passage_styles", {}),
|
||||
"layout_id": layout_id, "layout": details, "log": str(log),
|
||||
"audit_scripts": {str(path): script_hash(path)
|
||||
for path in [script, *(script_inputs or [])]},
|
||||
"command": [sys.executable, str(script), *args],
|
||||
}
|
||||
self.runs.append(row)
|
||||
self.assert_current_tree(f"after stage {name}")
|
||||
self.print_step(row)
|
||||
if not ok:
|
||||
raise VerificationFailure(f"{name} failed (exit={code}); see {log}")
|
||||
return row, output
|
||||
|
||||
def print_step(self, row: dict[str, Any]) -> None:
|
||||
print(f"[{self.tier}] {row['name']} {row['status']} "
|
||||
f"{row.get('elapsed_seconds', 0):.1f}s "
|
||||
f"recipe={row.get('resolved_recipe') or '-'} "
|
||||
f"style={row.get('resolved_style') or '-'} "
|
||||
f"layout={row.get('layout_id') or '-'}", flush=True)
|
||||
|
||||
def require(self, name: str, passed: bool, detail: str = "") -> None:
|
||||
step = {"name": name, "status": "PASS" if passed else "FAIL", "detail": detail}
|
||||
self.steps.append(step)
|
||||
print(f"[{self.tier}] {name} {'PASS' if passed else 'FAIL'} {detail}", flush=True)
|
||||
if not passed:
|
||||
raise VerificationFailure(f"{name} failed: {detail}")
|
||||
|
||||
def save_summary(self, status: str, error: str | None = None) -> Path:
|
||||
elapsed = time.monotonic() - self.started
|
||||
summary = {
|
||||
"tier": self.tier, "status": status, "elapsed_seconds": round(elapsed, 3),
|
||||
"source_stamp": self.expected, "source_manifest_sha256": self.expected,
|
||||
"verification_tools": self.tool_hashes,
|
||||
"binary_sha256": self.binary_hashes, "runs": self.runs,
|
||||
"checks": self.steps, "reused_tiers": self.reused_tiers, "error": error,
|
||||
}
|
||||
path = OUT / f"{self.tier}-summary.json"
|
||||
path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"[{self.tier}] SUMMARY {status} elapsed={elapsed:.1f}s file={path}", flush=True)
|
||||
return path
|
||||
|
||||
|
||||
class VerificationFailure(RuntimeError):
|
||||
pass
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build and verify the final-source manifest used by Linux executables."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
def sha256(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def manifest_entries(root: Path, include_unreal: bool = False) -> list[tuple[str, bytes]]:
|
||||
standalone = root / "Standalone"
|
||||
files: set[Path] = set()
|
||||
|
||||
core = standalone / "Core"
|
||||
for suffix in ("*.h", "*.hpp", "*.cpp", "*.txt"):
|
||||
files.update(core.rglob(suffix))
|
||||
|
||||
source = standalone / "Source"
|
||||
for suffix in ("*.h", "*.hpp", "*.cpp"):
|
||||
files.update(source.glob(suffix))
|
||||
|
||||
for path in standalone.glob("Build*"):
|
||||
if path.is_file():
|
||||
files.add(path)
|
||||
for suffix in ("*.cmake", "*.mk", "Makefile", "CMakeLists.txt"):
|
||||
files.update(standalone.glob(suffix))
|
||||
core_build = standalone / "Core"
|
||||
for suffix in ("*.cmake", "*.mk", "Makefile", "CMakeLists.txt"):
|
||||
files.update(core_build.glob(suffix))
|
||||
helper = standalone / "Tools/verify/source_stamp.py"
|
||||
if helper.is_file():
|
||||
files.add(helper)
|
||||
|
||||
if include_unreal:
|
||||
unreal = source / "VoxelForge"
|
||||
for suffix in ("*.h", "*.hpp", "*.cpp", "*.cs"):
|
||||
files.update(unreal.rglob(suffix))
|
||||
|
||||
entries = [(path.relative_to(root).as_posix(), path.read_bytes())
|
||||
for path in files if path.is_file()]
|
||||
|
||||
# BuildLinux creates one namespace wrapper per listed Core implementation.
|
||||
# Include each exact generated wrapper as a virtual source input.
|
||||
source_list = core / "core_sources.txt"
|
||||
for raw in source_list.read_text(encoding="utf-8").splitlines():
|
||||
name = raw.split("#", 1)[0].strip()
|
||||
if not name or name == "Core/src/VoxelForgeCore.cpp":
|
||||
continue
|
||||
source_path = standalone / name
|
||||
if not source_path.is_file():
|
||||
raise SystemExit(f"missing source listed in Core/core_sources.txt: {name}")
|
||||
wrapper_name = "vfcore_" + "".join(c if c.isalnum() else "_" for c in name)
|
||||
wrapper = ('#include "CoreNamespacePreamble.h"\n'
|
||||
"namespace vfcore::detail\n{\n"
|
||||
f'#include "{source_path}"\n'
|
||||
"}\n").encode("utf-8")
|
||||
entries.append((f"generated/wrappers/{wrapper_name}.cpp", wrapper))
|
||||
|
||||
return sorted(entries, key=lambda item: item[0])
|
||||
|
||||
|
||||
def rendered_manifest(root: Path, include_unreal: bool = False) -> bytes:
|
||||
lines = [f"{sha256(data)} {name}" for name, data in
|
||||
manifest_entries(root, include_unreal)]
|
||||
return ("\n".join(lines) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def source_stamp(root: Path, include_unreal: bool = False) -> str:
|
||||
return sha256(rendered_manifest(root, include_unreal))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=("stamp", "write-manifest"))
|
||||
parser.add_argument("--root", type=Path,
|
||||
default=Path(__file__).resolve().parents[3])
|
||||
parser.add_argument("--output", type=Path)
|
||||
parser.add_argument("--include-unreal-adapter", action="store_true")
|
||||
args = parser.parse_args()
|
||||
root = args.root.resolve()
|
||||
manifest = rendered_manifest(root, args.include_unreal_adapter)
|
||||
stamp = sha256(manifest)
|
||||
if args.command == "write-manifest":
|
||||
if args.output is None:
|
||||
parser.error("write-manifest requires --output")
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_bytes(manifest)
|
||||
print(stamp)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,640 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the reusable VoxelForge verification tiers: fast, medium, or full."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import statistics
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
VERIFY_DIR = ROOT / "Standalone/Tools/verify"
|
||||
LINUX37 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux37"
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
OUT_DEFAULT = ROOT / "Saved/Linux/l39"
|
||||
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("tier", choices=("fast", "medium", "full"))
|
||||
PARSER.add_argument("--out", type=Path, default=OUT_DEFAULT,
|
||||
help="artifact directory (default: Saved/Linux/l39)")
|
||||
ARGS = PARSER.parse_args()
|
||||
OUT = ARGS.out.resolve()
|
||||
os.environ["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
sys.path.insert(0, str(VERIFY_DIR))
|
||||
from owner_step import measure as measure_owner_step
|
||||
from runner import VerificationFailure, VerificationRunner, bounded_run
|
||||
from source_stamp import source_stamp
|
||||
|
||||
|
||||
CLANG = OUT / "bin/VoxelForgeCoreHeadless-l39-clang"
|
||||
GCC = OUT / "bin/VoxelForgeCoreHeadless-l39-gcc"
|
||||
RENDERER = OUT / "bin/VoxelForgeOffscreenCapture-l39-clang"
|
||||
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
RECIPE_RE = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
|
||||
|
||||
def compiler_name(binary: Path) -> str:
|
||||
return "gcc" if "gcc" in binary.name else "clang"
|
||||
|
||||
|
||||
def env_for(*, capture: bool = False) -> dict[str, str]:
|
||||
result = os.environ.copy()
|
||||
result["VOXELFORGE_VERIFY_OUT"] = str(OUT)
|
||||
result["VOXELFORGE_L37_OUT"] = str(OUT)
|
||||
result["VOXELFORGE_L37_CAPTURE_DIR"] = str(OUT / "captures")
|
||||
result["VOXELFORGE_L37_HEADLESS"] = str(CLANG)
|
||||
result["VOXELFORGE_L37_RENDERER"] = str(RENDERER)
|
||||
result["VOXELFORGE_VERIFY_HEADLESS"] = str(CLANG)
|
||||
if capture:
|
||||
result["LP_NUM_THREADS"] = "12"
|
||||
result["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
return result
|
||||
|
||||
|
||||
def all_match(text: str, pattern: str) -> bool:
|
||||
return re.search(pattern, text, re.M) is not None
|
||||
|
||||
|
||||
def parse_ratio(text: str, name: str) -> float | None:
|
||||
match = re.search(rf"(?:^| ){re.escape(name)}=(\d+)/(\d+)", text)
|
||||
return int(match.group(1)) / max(1, int(match.group(2))) if match else None
|
||||
|
||||
|
||||
def validate_passage(runner: VerificationRunner, name: str, output: str) -> None:
|
||||
required = (
|
||||
("strict-layout", r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$"),
|
||||
("layout-controls", r"^\[ResolvedLayoutValidatorControls\].*status=PASS$"),
|
||||
("walkability", r"^\[Walkability\] PASS"),
|
||||
("floor-profile", r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$"),
|
||||
("mouth-profile", r"^\[PassageMouthProfileAudit\].*status=PASS$"),
|
||||
("route-binding", r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$"),
|
||||
("naturalness", r"^\[PassageNaturalnessSummary\].*status=PASS$"),
|
||||
("open-air", r"^\[PassageOpenAirAuditSummary\].*status=PASS$"),
|
||||
("no-long-bridge", r"^\[PassageNoLongBridgeAudit\].*status=PASS$"),
|
||||
("rock-columns", r"^\[PassageRockColumnAuditSummary\].*status=PASS$"),
|
||||
("ripple-controls", r"^\[PassageRippleAuditSummary\].*status=PASS$"),
|
||||
("passage-summary", r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$"),
|
||||
)
|
||||
for gate, pattern in required:
|
||||
runner.require(f"{name}:{gate}", all_match(output, pattern),
|
||||
"required resolved-passage gate")
|
||||
|
||||
rows = re.findall(r"^\[PassageRippleAudit\].*$", output, re.M)
|
||||
ripple_ok = bool(rows)
|
||||
for line in rows:
|
||||
wall = re.search(r"local_peak_amplitude_m=([-0-9.]+)", line)
|
||||
if wall is None:
|
||||
wall = re.search(r"amplitude_m=([-0-9.]+)", line)
|
||||
wall_threshold = re.search(r"threshold_m=([-0-9.]+)", line)
|
||||
floor = re.search(r"floor_amplitude_m=([-0-9.]+)", line)
|
||||
floor_threshold = re.search(r"floor_threshold_m=([-0-9.]+)", line)
|
||||
wall_coverage = parse_ratio(line, "coverage")
|
||||
floor_coverage = parse_ratio(line, "floor_coverage")
|
||||
ripple_ok &= bool(
|
||||
"status=PASS" in line and wall and wall_threshold and floor and floor_threshold
|
||||
and wall_coverage is not None and floor_coverage is not None
|
||||
and float(wall.group(1)) <= float(wall_threshold.group(1))
|
||||
and float(floor.group(1)) <= float(floor_threshold.group(1))
|
||||
and wall_coverage >= 0.75 and floor_coverage >= 0.75)
|
||||
runner.require(f"{name}:wall-floor-ripple-and-coverage", ripple_ok,
|
||||
f"profiles={len(rows)} wall/floor amplitudes under their thresholds; coverage>=0.75")
|
||||
|
||||
|
||||
def run_passage(runner: VerificationRunner, binary: Path, name: str,
|
||||
data: Path, recipe: str, style: int, *, workers: int = 12,
|
||||
flags: list[str] | None = None, max_level: int = -1,
|
||||
layout_path: Path | None = None, passage_style: int | None = None,
|
||||
expected_strate_styles: dict[str, int] | None = None) -> tuple[dict, str]:
|
||||
layout_path = layout_path or OUT / "layouts" / f"{name}.json"
|
||||
row, output = runner.run_cpp(name, binary, data, recipe,
|
||||
["--audit-resolved-layout-passages", *(flags or [])],
|
||||
expected_style=style if passage_style is None else passage_style,
|
||||
expected_strate_styles=expected_strate_styles,
|
||||
workers=workers, max_level=max_level,
|
||||
layout_path=layout_path)
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = style if passage_style is None else passage_style
|
||||
row["requested_strate_styles"] = expected_strate_styles
|
||||
validate_passage(runner, name, output)
|
||||
return row, output
|
||||
|
||||
|
||||
def run_owner_passages(runner: VerificationRunner, binaries: tuple[Path, ...]) -> dict[str, tuple[dict, str]]:
|
||||
result: dict[str, tuple[dict, str]] = {}
|
||||
expected_strate_styles = {"0": 2, "1": 1}
|
||||
for binary in binaries:
|
||||
compiler = compiler_name(binary)
|
||||
for recipe, style in (("floating-islands", 2), ("surface-world", 1)):
|
||||
name = f"owner-{recipe}-style{style}-{compiler}"
|
||||
row, output = run_passage(runner, binary, name, OWNER, recipe, style,
|
||||
passage_style=2, expected_strate_styles=expected_strate_styles)
|
||||
result[f"{compiler}:{recipe}"] = (row, output)
|
||||
actual_styles = row.get("resolved_strate_styles", {})
|
||||
runner.require(f"{name}:owner-style-map",
|
||||
actual_styles == expected_strate_styles,
|
||||
f"resolved_strate_styles={actual_styles} expected={expected_strate_styles}")
|
||||
if compiler == "clang":
|
||||
measured = measure_owner_step(output)
|
||||
worst = measured.get("worst")
|
||||
runner.require(f"{name}:adjacent-owner-step",
|
||||
bool(measured["passed"]),
|
||||
f"max_step_m={measured['maximum_adjacent_step_m']:.6f} "
|
||||
f"limit_m=0.100 pairs={measured['adjacent_pairs']} "
|
||||
f"fitted_periods_m={measured['fitted_periods_m']} "
|
||||
f"period_role=diagnostic worst={worst}")
|
||||
return result
|
||||
|
||||
|
||||
def expected_control_row(runner: VerificationRunner, name: str) -> None:
|
||||
if runner.runs:
|
||||
row = runner.runs[-1]
|
||||
row["observed_status"] = row["status"]
|
||||
row["status"] = "EXPECTED_FAIL"
|
||||
row["negative_control"] = name
|
||||
|
||||
|
||||
def run_stamp_and_input_controls(runner: VerificationRunner) -> None:
|
||||
# Mutate one compiled Core input only while checking that the runner refuses it.
|
||||
core_file = ROOT / "Standalone/Core/src/VoxelForgeCore.cpp"
|
||||
original = core_file.read_bytes()
|
||||
rejected = False
|
||||
try:
|
||||
core_file.write_bytes(original + b"\n// temporary L39 stale-build negative control\n")
|
||||
try:
|
||||
runner.probe_binary(CLANG)
|
||||
except RuntimeError as error:
|
||||
rejected = "source changed" in str(error)
|
||||
finally:
|
||||
core_file.write_bytes(original)
|
||||
runner.assert_current_tree("after restoring stale-build control")
|
||||
runner.require("stale-build-source-edit-control", rejected,
|
||||
"runner refused the binary after a Core source edit; original bytes restored")
|
||||
|
||||
fixture = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
mislabelled = OUT / "controls/mislabelled-owner/owner_world.json"
|
||||
mislabelled.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(fixture, mislabelled)
|
||||
try:
|
||||
runner.run_cpp("control-mislabelled-owner-style0", CLANG, mislabelled,
|
||||
"floating-islands", ["--resolved-layout-only"], expected_style=2,
|
||||
layout_path=OUT / "controls/mislabelled-owner/layout-owner.json")
|
||||
detected = False
|
||||
except VerificationFailure:
|
||||
detected = bool(runner.runs and 0 in runner.runs[-1]["resolved_style"]
|
||||
and runner.runs[-1]["status"] == "FAIL")
|
||||
if detected:
|
||||
expected_control_row(runner, "mislabelled-style0-input")
|
||||
runner.require("mislabelled-owner-style-control", detected,
|
||||
"style-0 fixture named owner_world.json rejected for expected FI Style 2")
|
||||
|
||||
black = OUT / "controls/all-black.png"
|
||||
black.parent.mkdir(parents=True, exist_ok=True)
|
||||
write_black_png(black)
|
||||
script = VERIFY_DIR / "capture_quality.py"
|
||||
try:
|
||||
runner.run_stage("control-all-black-capture", script, [str(black)],
|
||||
binary=CLANG, data=None, recipe=None, style=None,
|
||||
env=env_for(), script_inputs=[black])
|
||||
black_rejected = False
|
||||
except VerificationFailure:
|
||||
black_rejected = "status=FAIL" in (OUT / "control-all-black-capture.log").read_text(
|
||||
encoding="utf-8", errors="replace")
|
||||
if black_rejected:
|
||||
expected_control_row(runner, "all-black-capture")
|
||||
runner.require("all-black-capture-control", black_rejected,
|
||||
"pixel variance and coverage floor reject a valid all-black PNG")
|
||||
|
||||
|
||||
def write_black_png(path: Path) -> None:
|
||||
def chunk(tag: bytes, payload: bytes) -> bytes:
|
||||
return (struct.pack(">I", len(payload)) + tag + payload
|
||||
+ struct.pack(">I", zlib.crc32(tag + payload) & 0xffffffff))
|
||||
pixels = b"".join(b"\0" + b"\0\0\0" * 2 for _ in range(2))
|
||||
body = (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">IIBBBBB", 2, 2, 8, 2, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(pixels)) + chunk(b"IEND", b""))
|
||||
path.write_bytes(body)
|
||||
|
||||
|
||||
def run_regression_controls(runner: VerificationRunner) -> None:
|
||||
run_stamp_and_input_controls(runner)
|
||||
controls = (
|
||||
("l35-wall-ribs", "wall-ribs",
|
||||
ROOT / "Saved/Linux/l35/failed/passage-audit-cascade-cosine-owner-fi-style3-clang.log"),
|
||||
("l36-floor-washboard", "floor-washboard",
|
||||
ROOT / "Saved/Linux/l36/audit-nearest-fi-style0.log"),
|
||||
)
|
||||
script = VERIFY_DIR / "historical_controls.py"
|
||||
for name, kind, log in controls:
|
||||
runner.run_stage(f"control-{name}", script, [kind, str(log)],
|
||||
binary=CLANG, data=None, recipe=None, style=None,
|
||||
env=env_for(), script_inputs=[log])
|
||||
|
||||
l38 = ROOT / "Saved/Linux/l38/owner-fi-real-final.log"
|
||||
step_script = VERIFY_DIR / "owner_step.py"
|
||||
runner.run_stage("control-l38-owner-step", step_script,
|
||||
[str(l38), "--expect-fail"], binary=CLANG, data=None,
|
||||
recipe=None, style=None, env=env_for(), script_inputs=[l38])
|
||||
|
||||
|
||||
def run_hash_pairs(runner: VerificationRunner,
|
||||
owner_results: dict[str, tuple[dict, str]]) -> None:
|
||||
for recipe, style in (("floating-islands", 2), ("surface-world", 1)):
|
||||
hashes: dict[int, tuple[str, ...]] = {}
|
||||
for workers in (1, 12):
|
||||
name = f"owner-hash-{recipe}-style{style}-w{workers}"
|
||||
row, output = runner.run_cpp(name, CLANG, OWNER, recipe,
|
||||
["--stream-hash"], expected_style=2,
|
||||
expected_strate_styles={"0": 2, "1": 1}, workers=workers,
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = 2
|
||||
values = tuple(HASH_RE.findall(output))
|
||||
hashes[workers] = values
|
||||
runner.require(f"{name}:hash-count", len(values) == 12,
|
||||
f"hashes={len(values)}/12")
|
||||
same = hashes[1] == hashes[12]
|
||||
baseline = ROOT / "Saved/Linux/l37" / f"determinism-l25-clang-{recipe}-w12.log"
|
||||
old_hashes = tuple(HASH_RE.findall(baseline.read_text(encoding="utf-8", errors="replace")))
|
||||
same_l37 = hashes[12] == old_hashes and len(old_hashes) == 12
|
||||
runner.require(f"owner-hash-{recipe}-w1-w12", same,
|
||||
"W1 and W12 each produced identical 12-hash signatures")
|
||||
runner.require(f"owner-hash-{recipe}-matches-l37", same_l37,
|
||||
f"current owner hashes match {baseline.name}; hashes={len(old_hashes)}")
|
||||
|
||||
|
||||
def run_ring(runner: VerificationRunner, *, full: bool) -> None:
|
||||
script = LINUX37 / "RunRingAuditsL37.py"
|
||||
audit_inputs = [script,
|
||||
ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py",
|
||||
ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingSetL32.py"]
|
||||
for label, recipe, style in (("fi-floor", "floating-islands", 2),
|
||||
("sw-ceiling", "surface-world", 1)):
|
||||
args = ["--scene", label]
|
||||
layout = OUT / "layouts" / f"ring-layout-{label}-lod0.json"
|
||||
if not full:
|
||||
args.append("--lod0-only")
|
||||
if label == "sw-ceiling":
|
||||
args.append("--skip-controls")
|
||||
runner.run_stage(f"ring-{label}-{'lod0-8' if full else 'lod0'}", script, args,
|
||||
binary=CLANG, data=OWNER, recipe=recipe, style=style,
|
||||
layout_path=layout, env=env_for(), script_inputs=audit_inputs)
|
||||
|
||||
|
||||
def style_override_matrix(runner: VerificationRunner, binary: Path) -> None:
|
||||
sources: list[tuple[str, Path, str]] = []
|
||||
for recipe in ("floating-islands", "surface-world"):
|
||||
for style in range(4):
|
||||
source = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json"
|
||||
sources.append((f"{recipe}-style{style}", source, recipe))
|
||||
for style in range(4):
|
||||
source = ROOT / f"Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style{style}.json"
|
||||
sources.append((f"upper-sw-fixture-style{style}", source, "floating-islands"))
|
||||
|
||||
for fixture_name, source, recipe in sources:
|
||||
label = f"style-override-{fixture_name}"
|
||||
destination = OUT / "fixtures" / f"{label}.json"
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
style = int(re.search(r"style([0-3])$", fixture_name).group(1))
|
||||
name = f"{label}-{compiler_name(binary)}"
|
||||
if fixture_name.startswith("upper-sw-fixture-"):
|
||||
expected_strate_styles = {"1": style}
|
||||
else:
|
||||
expected_strate_styles = {"0": style, "1": 1}
|
||||
row, output = runner.run_cpp(name, binary, destination, recipe,
|
||||
["--audit-resolved-layout-passages"], expected_style=None,
|
||||
expected_strate_styles=expected_strate_styles,
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = style
|
||||
row["requested_strate_styles"] = expected_strate_styles
|
||||
validate_passage(runner, name, output)
|
||||
|
||||
|
||||
def run_transitions_and_invariants(runner: VerificationRunner,
|
||||
binaries: tuple[Path, ...]) -> None:
|
||||
for binary in binaries:
|
||||
compiler = compiler_name(binary)
|
||||
for recipe, style in (("floating-islands", 2), ("surface-world", 1)):
|
||||
for mode in ("hard", "gradient", "interleaved"):
|
||||
name = f"transition-{recipe}-{mode}-{compiler}"
|
||||
row, output = runner.run_cpp(name, binary, OWNER, recipe,
|
||||
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = 2
|
||||
gates = (r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||
r"^\[StrateTransitionNegativeControls\].*status=PASS$")
|
||||
for index, pattern in enumerate(gates):
|
||||
runner.require(f"{name}:gate-{index + 1}", all_match(output, pattern), pattern)
|
||||
|
||||
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||
row, output = runner.run_cpp(name, binary, OWNER, recipe,
|
||||
["--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = 2
|
||||
runner.require(f"{name}:route-parity",
|
||||
all_match(output, r"^\[RouteParityAudit\].*status=PASS$"),
|
||||
"route parity L13")
|
||||
runner.require(f"{name}:uniform-block-proof",
|
||||
all_match(output, r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)"),
|
||||
"uniform-block proof L9")
|
||||
|
||||
|
||||
def run_captures_and_mesh_audits(runner: VerificationRunner) -> None:
|
||||
capture_script = LINUX37 / "CapturePassageStandardSetL37.py"
|
||||
capture_inputs = [capture_script, VERIFY_DIR / "capture_quality.py",
|
||||
ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"]
|
||||
capture_rows: dict[str, tuple[dict, str]] = {}
|
||||
for scene, recipe, style in (("owner-fi", "floating-islands", 2),
|
||||
("owner-sw", "surface-world", 1)):
|
||||
layout_path = OUT / f"layout-{scene}.json"
|
||||
name = f"capture-standard-{scene}-style{style}"
|
||||
row, output = runner.run_stage(name, capture_script,
|
||||
["--scene", scene], binary=CLANG, data=OWNER, recipe=recipe,
|
||||
style=style, layout_path=layout_path, env=env_for(capture=True),
|
||||
script_inputs=capture_inputs)
|
||||
resolved_strate_styles = row.get("resolved_strate_styles", {})
|
||||
target_strate = "0" if scene == "owner-fi" else "1"
|
||||
style_ok = (resolved_strate_styles == {"0": 2, "1": 1}
|
||||
and resolved_strate_styles.get(target_strate) == style)
|
||||
runner.require(f"{name}:resolved-style", style_ok,
|
||||
f"resolved_strate_styles={resolved_strate_styles} "
|
||||
f"target_strate={target_strate} expected_style={style}")
|
||||
capture_rows[scene] = (row, output)
|
||||
|
||||
shape_script = LINUX37 / "AuditMouthShapeL37.py"
|
||||
baseline = ROOT / "Saved/Linux/l37/no-tread-control"
|
||||
controls = [ROOT / "Saved/Linux/l36/owner-fi-ground-mouth.vfmesh"]
|
||||
for scene, recipe, style in (("owner-fi", "floating-islands", 2),
|
||||
("owner-sw", "surface-world", 1)):
|
||||
name = f"mouth-shape-{scene}-style{style}"
|
||||
runner.run_stage(name, shape_script,
|
||||
[f"--out={OUT}", f"--baseline-out={baseline}",
|
||||
"--control-out=Saved/Linux/l36", "--scene", scene],
|
||||
binary=CLANG, data=OWNER, recipe=recipe, style=style,
|
||||
layout_path=OUT / f"layout-{scene}.json", env=env_for(),
|
||||
script_inputs=[shape_script, *controls])
|
||||
|
||||
crease_script = LINUX37 / "RunMouthCreaseAuditL37.py"
|
||||
sw_capture_log = OUT / "capture-standard-owner-sw-style1.log"
|
||||
crease_args = [
|
||||
f"--candidate={OUT / 'owner-fi-ground-mouth.vfmesh'}",
|
||||
f"--candidate-sw={OUT / 'owner-sw-ground-mouth.vfmesh'}",
|
||||
f"--layout-sw={OUT / 'layout-owner-sw.json'}",
|
||||
f"--capture-log-sw={sw_capture_log}"]
|
||||
control_meshes = [ROOT / f"Saved/Linux/{round_name}/owner-fi-ground-mouth.vfmesh"
|
||||
for round_name in ("l35", "l36")]
|
||||
runner.run_stage("mouth-crease-fragments-owner-fi2-sw1", crease_script,
|
||||
crease_args, binary=CLANG, data=OWNER, recipe="floating-islands", style=2,
|
||||
layout_path=OUT / "layout-owner-fi.json", env=env_for(),
|
||||
script_inputs=[crease_script,
|
||||
LINUX37 / "RunMouthFragmentAuditL37.py", *control_meshes,
|
||||
OUT / "owner-fi-ground-mouth.vfmesh", OUT / "owner-sw-ground-mouth.vfmesh",
|
||||
OUT / "layout-owner-sw.json", sw_capture_log])
|
||||
|
||||
overview_script = LINUX37 / "CaptureMouthOverviewL37.py"
|
||||
runner.run_stage("capture-fi-overview-style2", overview_script, [],
|
||||
binary=CLANG, data=OWNER, recipe="floating-islands", style=2,
|
||||
layout_path=OUT / "layout-owner-fi.json", env=env_for(capture=True),
|
||||
script_inputs=[overview_script, VERIFY_DIR / "capture_quality.py",
|
||||
OUT / "owner-fi-ground-mouth.vfmesh"])
|
||||
|
||||
|
||||
def verify_determinism_cell(runner: VerificationRunner, binary: Path,
|
||||
kind: str, recipe: str, style: int, workers: int,
|
||||
reference: dict[tuple[str, str], tuple[str, ...]]) -> None:
|
||||
name = f"determinism-{kind}-{recipe}-{compiler_name(binary)}-w{workers}"
|
||||
flags = ["--stream-hash"]
|
||||
if kind == "crossing24":
|
||||
flags.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||
row, output = runner.run_cpp(name, binary, OWNER, recipe, flags,
|
||||
expected_style=2, expected_strate_styles={"0": 2, "1": 1},
|
||||
workers=workers,
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = 2
|
||||
values = tuple(HASH_RE.findall(output))
|
||||
key = (kind, recipe)
|
||||
expected = reference.get(key)
|
||||
stable = expected is None or values == expected
|
||||
if expected is None and values:
|
||||
reference[key] = values
|
||||
runner.require(f"{name}:stable-hashes", len(values) == 12 and stable,
|
||||
f"hashes={len(values)}/12 stable={stable}")
|
||||
|
||||
|
||||
def run_30_cell_determinism(runner: VerificationRunner) -> None:
|
||||
references: dict[tuple[str, str], tuple[str, ...]] = {}
|
||||
count = 0
|
||||
for kind in ("l25", "crossing24"):
|
||||
for recipe, style in (("surface-world", 1), ("floating-islands", 2)):
|
||||
for binary in (CLANG, GCC):
|
||||
for workers in (1, 8, 12):
|
||||
verify_determinism_cell(runner, binary, kind, recipe, style,
|
||||
workers, references)
|
||||
count += 1
|
||||
|
||||
fixture_source = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||
fixture = OUT / "fixtures/style-override-upper-sw-fixture-style0.json"
|
||||
fixture.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(fixture_source, fixture)
|
||||
signatures = None
|
||||
for binary in (CLANG, GCC):
|
||||
for workers in (1, 8, 12):
|
||||
name = f"determinism-style-override-upper-sw-fixture-style0-{compiler_name(binary)}-w{workers}"
|
||||
row, output = runner.run_cpp(name, binary, fixture, "floating-islands",
|
||||
["--audit-resolved-layout-passages"], expected_style=None,
|
||||
expected_strate_styles={"1": 0}, workers=workers,
|
||||
layout_path=OUT / "layouts" / f"{name}.json")
|
||||
row["requested_style"] = 0
|
||||
row["requested_strate_styles"] = {"1": 0}
|
||||
validate_passage(runner, name, output)
|
||||
current = tuple(re.findall(
|
||||
r"^\[ResolvedLayoutSnapshot\].*stable_identity=([^ ]+)", output, re.M)
|
||||
+ re.findall(r"^\[WorldAssemblyPlanSerialization\].*plan_hash=([0-9a-f]+)", output, re.M)
|
||||
+ re.findall(r"^\[WorldAssemblerRouteAccepted\].*$", output, re.M))
|
||||
stable = signatures is None or current == signatures
|
||||
if signatures is None and current:
|
||||
signatures = current
|
||||
runner.require(f"{name}:stable-plan-route", bool(current) and stable,
|
||||
f"snapshot-plan-route_records={len(current)} stable={stable}")
|
||||
count += 1
|
||||
runner.require("determinism-30-cell-count", count == 30, f"cells={count}/30")
|
||||
|
||||
|
||||
def prepare_l36_cost_baseline(runner: VerificationRunner) -> tuple[Path, str]:
|
||||
temp = Path("/tmp/voxelforge-l36-stamped")
|
||||
if temp.exists():
|
||||
shutil.rmtree(temp)
|
||||
temp.mkdir(parents=True)
|
||||
tarball = temp / "l36.tar"
|
||||
archive = bounded_run(["git", "archive", "-o", str(tarball),
|
||||
"8be1c4d", "Standalone"], timeout=120)
|
||||
if archive is None or archive.returncode != 0 or not tarball.is_file():
|
||||
raise VerificationFailure("could not export historical L36 source commit 8be1c4d")
|
||||
extract = bounded_run(["tar", "-xf", str(tarball), "-C", str(temp)], timeout=120)
|
||||
if extract is None or extract.returncode != 0:
|
||||
raise VerificationFailure("historical L36 source extraction failed")
|
||||
tarball.unlink(missing_ok=True)
|
||||
|
||||
base = temp / "Standalone"
|
||||
(base / "Tools/verify").mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ROOT / "Standalone/BuildLinux.sh", base / "BuildLinux.sh")
|
||||
shutil.copy2(VERIFY_DIR / "source_stamp.py", base / "Tools/verify/source_stamp.py")
|
||||
shutil.copy2(ROOT / "Standalone/Source/SourceStamp.h", base / "Source/SourceStamp.h")
|
||||
legacy_main = base / "Source/StreamBenchmarkHeadless.cpp"
|
||||
text = legacy_main.read_text(encoding="utf-8")
|
||||
if "VoxelForgePrintSourceStamp" not in text:
|
||||
text = text.replace("#include \"OwnerWorldLoader.h\"",
|
||||
"#include \"SourceStamp.h\"\n#include <cstring>\n#include \"OwnerWorldLoader.h\"", 1)
|
||||
anchor = re.search(r"int main\(int argc,\s*char\*\*\s*argv\)\s*\{", text)
|
||||
if anchor is None:
|
||||
raise VerificationFailure("cannot instrument historical L36 headless entry point")
|
||||
injected = ("\n VoxelForgePrintSourceStamp();\n"
|
||||
" if(argc==2&&std::strcmp(argv[1],\"--print-source-stamp\")==0)return 0;")
|
||||
text = text[:anchor.end()] + injected + text[anchor.end():]
|
||||
legacy_main.write_text(text, encoding="utf-8")
|
||||
|
||||
baseline_stamp = source_stamp(temp)
|
||||
binary = OUT / "historical-bin/VoxelForgeCoreHeadless-l36-stamped"
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_BUILD_ROOT"] = str(temp / "build")
|
||||
env["VOXELFORGE_BIN_DIR"] = str(binary.parent)
|
||||
command = ["bash", str(base / "BuildLinux.sh"), "--compiler=clang++-19",
|
||||
"--variant=l36-stamped", "--fp-contract=off", "--jobs=12"]
|
||||
log = OUT / "cost-baseline-build-l36.log"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
proc = bounded_run(command, timeout=1800, env=env, stdout=stream)
|
||||
elapsed = time.monotonic() - started
|
||||
runner.require("cost-baseline-l36-build",
|
||||
proc is not None and proc.returncode == 0 and binary.is_file(),
|
||||
f"source_stamp={baseline_stamp} elapsed_seconds={elapsed:.1f} log={log}")
|
||||
runner.binary_stamp_overrides[str(binary)] = baseline_stamp
|
||||
return binary, baseline_stamp
|
||||
|
||||
|
||||
def run_cost_pairs(runner: VerificationRunner) -> None:
|
||||
baseline, baseline_stamp = prepare_l36_cost_baseline(runner)
|
||||
candidate = CLANG
|
||||
orders = (("l36", "l39"), ("l39", "l36"), ("l36", "l39"),
|
||||
("l39", "l36"), ("l36", "l39"))
|
||||
by_recipe: dict[str, dict[str, list[float]]] = {
|
||||
"floating-islands": {"l36": [], "l39": []},
|
||||
"surface-world": {"l36": [], "l39": []}}
|
||||
by_pair: dict[tuple[str, int], dict[str, float]] = {}
|
||||
for recipe, style in (("floating-islands", 2), ("surface-world", 1)):
|
||||
for pair, order in enumerate(orders, 1):
|
||||
for build in order:
|
||||
binary = baseline if build == "l36" else candidate
|
||||
expected_style = None if build == "l36" else style
|
||||
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||
expected_stamp = baseline_stamp if build == "l36" else None
|
||||
row, output = runner.run_cpp(name, binary, OWNER, recipe, [],
|
||||
expected_style=2 if build == "l39" else None,
|
||||
expected_strate_styles={"0": 2, "1": 1} if build == "l39" else None,
|
||||
layout_path=OUT / "layouts" / f"{name}.json",
|
||||
expected_stamp=expected_stamp)
|
||||
row["requested_style"] = style
|
||||
row["expected_passage_style"] = 2 if build == "l39" else None
|
||||
ready = re.findall(r"\bwhole_view_ready=([0-9.]+)s", output)
|
||||
ok = (len(ready) == 1 and "hash=off" in output
|
||||
and "cache_hits=0" in output
|
||||
and RECIPE_RE.findall(output) == [(recipe, recipe, "PASS")])
|
||||
runner.require(f"{name}:cost-record", ok,
|
||||
f"whole_view_ready={ready[0] if ready else 'MISSING'}s "
|
||||
f"source_stamp={row['source_stamp']}")
|
||||
by_recipe[recipe][build].append(float(ready[0]))
|
||||
by_pair.setdefault((recipe, pair), {})[build] = float(ready[0])
|
||||
for recipe, values in by_recipe.items():
|
||||
old = statistics.median(values["l36"])
|
||||
new = statistics.median(values["l39"])
|
||||
paired = [((by_pair[(recipe, pair)]["l39"] - by_pair[(recipe, pair)]["l36"])
|
||||
/ by_pair[(recipe, pair)]["l36"] * 100.0)
|
||||
for pair in range(1, 6)]
|
||||
runner.require(f"cost-{recipe}-five-pairs",
|
||||
len(values["l36"]) == len(values["l39"]) == 5,
|
||||
f"median_l36_s={old:.3f} median_l39_s={new:.3f} "
|
||||
f"median_change_pct={(new / old - 1.0) * 100.0:+.2f} "
|
||||
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}")
|
||||
|
||||
|
||||
def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False) -> None:
|
||||
binaries = (CLANG, GCC) if both_compilers else (CLANG,)
|
||||
run_owner_passages(runner, binaries)
|
||||
run_regression_controls(runner)
|
||||
style_override_matrix(runner, CLANG)
|
||||
if both_compilers:
|
||||
style_override_matrix(runner, GCC)
|
||||
run_transitions_and_invariants(runner, binaries)
|
||||
run_ring(runner, full=True)
|
||||
run_captures_and_mesh_audits(runner)
|
||||
|
||||
|
||||
def run_gcc_supplement(runner: VerificationRunner) -> None:
|
||||
"""Add GCC evidence to a stamped, already-passed Clang medium tier."""
|
||||
run_owner_passages(runner, (GCC,))
|
||||
style_override_matrix(runner, GCC)
|
||||
run_transitions_and_invariants(runner, (GCC,))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
runner = VerificationRunner(ARGS.tier)
|
||||
started = time.monotonic()
|
||||
try:
|
||||
if not OWNER.is_file():
|
||||
raise VerificationFailure(f"canonical owner input is missing: {OWNER}")
|
||||
for binary in (CLANG, GCC, RENDERER):
|
||||
if binary.is_file():
|
||||
runner.probe_binary(binary)
|
||||
if ARGS.tier == "fast":
|
||||
owners = run_owner_passages(runner, (CLANG,))
|
||||
run_regression_controls(runner)
|
||||
run_hash_pairs(runner, owners)
|
||||
run_ring(runner, full=False)
|
||||
elif ARGS.tier == "medium":
|
||||
run_medium_suite(runner, both_compilers=False)
|
||||
else:
|
||||
medium_summary = OUT / "medium-summary.json"
|
||||
if not runner.reuse_passed_tier(medium_summary, "medium"):
|
||||
run_medium_suite(runner, both_compilers=False)
|
||||
run_gcc_supplement(runner)
|
||||
run_30_cell_determinism(runner)
|
||||
run_cost_pairs(runner)
|
||||
# The validated medium summary carries the final canonical capture set.
|
||||
|
||||
runner.assert_binaries_current()
|
||||
runner.assert_current_tree("at tier completion")
|
||||
elapsed = time.monotonic() - started
|
||||
runner.save_summary("PASS")
|
||||
print(f"[{ARGS.tier}] DONE PASS elapsed={elapsed:.1f}s", flush=True)
|
||||
return 0
|
||||
except Exception as error:
|
||||
elapsed = time.monotonic() - started
|
||||
runner.save_summary("FAIL", str(error))
|
||||
print(f"[{ARGS.tier}] DONE FAIL elapsed={elapsed:.1f}s error={error}", flush=True)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user