wip(assembler): round L34 -- passage mouth profile audit and standard capture set
Adds a density-field floor/wall profile audit along passages near mouths (0.125 m spacing; L23 terraced negative control detected) and a reusable 12-view passage capture set (80 deg FOV, eye height, LOD0, skirts off, capped clip planes). The old blurry mouth shot was camera framing; the L33 entry ledges were tile skirts. Owner world hashes unchanged; 24/24 passage rows, 30/30 determinism, 16/16 invariants; cost within noise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
No files matched your search
@@ -0,0 +1,251 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Measure exported passage-floor meshes along the first 12 m at each mouth."""
|
||||||
|
import csv
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
RESEARCH = Path(__file__).resolve().parent
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||||
|
PROFILE = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(PROFILE)
|
||||||
|
Q_TO_M = 1.0 / 256.0
|
||||||
|
SPACING = 0.125
|
||||||
|
PLATEAU = 0.50
|
||||||
|
EPSILON = 0.02
|
||||||
|
STEP = 0.10
|
||||||
|
SCENES = (
|
||||||
|
("owner-fi", "layout-owner-fi.json", "FloatingIslands", False),
|
||||||
|
("owner-sw", "layout-owner-sw.json", "SurfaceWorld", True),
|
||||||
|
("upper-sw-fixture", "layout-upper-sw-fixture.json", "SurfaceWorld", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dist(a, b):
|
||||||
|
return math.sqrt(sum((a[i] - b[i]) ** 2 for i in range(3)))
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate(path, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(path, path[1:]):
|
||||||
|
length = dist(a[0], b[0])
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
p = tuple(a[0][axis] + (b[0][axis] - a[0][axis]) * t
|
||||||
|
for axis in range(3))
|
||||||
|
return p, a[1] + (b[1] - a[1]) * t
|
||||||
|
remaining -= length
|
||||||
|
return path[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def make_inward_path(layout, passage, reverse):
|
||||||
|
scale = layout["core_voxel_size_um"] / (256.0 * 1_000_000.0)
|
||||||
|
controls = [(tuple(value * scale for value in point["position_q"]),
|
||||||
|
point["radius_q"] * scale) for point in passage["control_points"]]
|
||||||
|
lower = tuple(value * scale for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: dist(controls[i][0], lower))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, generator, prefer_lower):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_side = "lower" if prefer_lower else "upper"
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]["generator_kind"]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]["generator_kind"]
|
||||||
|
sides = []
|
||||||
|
if upper == generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower == generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if sides:
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
else:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no route for {generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def surface_candidates(buckets, x, y, cell_size, preferred_height):
|
||||||
|
hits = []
|
||||||
|
for verts in buckets.get((math.floor(x / cell_size), math.floor(y / cell_size)), ()):
|
||||||
|
(a, na), (b, nb), (c, nc) = verts
|
||||||
|
denominator = (b[1] - c[1]) * (a[0] - c[0]) + \
|
||||||
|
(c[1] - b[1]) * (a[0] - c[0])
|
||||||
|
if abs(denominator) < 1.0e-12:
|
||||||
|
continue
|
||||||
|
u = ((b[1] - c[1]) * (x - c[0]) + (c[0] - b[0]) * (y - c[1])) / denominator
|
||||||
|
v = ((c[1] - a[1]) * (x - c[0]) + (a[0] - c[0]) * (y - c[1])) / denominator
|
||||||
|
w = 1.0 - u - v
|
||||||
|
if min(u, v, w) < -1.0e-6:
|
||||||
|
continue
|
||||||
|
normal_z = u * na[2] + v * nb[2] + w * nc[2]
|
||||||
|
if normal_z <= 0.0:
|
||||||
|
continue
|
||||||
|
z = u * a[2] + v * b[2] + w * c[2]
|
||||||
|
if abs(z - preferred_height) <= 1.0:
|
||||||
|
hits.append((z, normal_z))
|
||||||
|
unique = {}
|
||||||
|
for z, normal_z in hits:
|
||||||
|
key = round(z, 4)
|
||||||
|
unique[key] = max(unique.get(key, 0.0), normal_z)
|
||||||
|
return [(z, round(normal_z, 3)) for z, normal_z in sorted(unique.items(),
|
||||||
|
key=lambda item: abs(item[0] - preferred_height))[:12]]
|
||||||
|
|
||||||
|
|
||||||
|
def sample_profile(mesh_path, path):
|
||||||
|
total = sum(dist(a[0], b[0]) for a, b in zip(path, path[1:]))
|
||||||
|
reach = min(12.0, total)
|
||||||
|
count = int(round(reach / SPACING))
|
||||||
|
positions = []
|
||||||
|
for index in range(count + 1):
|
||||||
|
distance = index * SPACING
|
||||||
|
point, radius = interpolate(path, distance)
|
||||||
|
before, _ = interpolate(path, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate(path, min(total, distance + 0.25))
|
||||||
|
tangent = tuple(after[axis] - before[axis] for axis in range(3))
|
||||||
|
length = math.hypot(tangent[0], tangent[1])
|
||||||
|
if length <= 1.0e-8:
|
||||||
|
positions.append((distance, point, radius, None))
|
||||||
|
else:
|
||||||
|
side = (-tangent[1] / length, tangent[0] / length)
|
||||||
|
positions.append((distance, point, radius, side))
|
||||||
|
expected = [point[2] - radius for _, point, radius, _ in positions]
|
||||||
|
bounds = (min(value[1][0] for value in positions) - 14.0,
|
||||||
|
max(value[1][0] for value in positions) + 14.0,
|
||||||
|
min(value[1][1] for value in positions) - 14.0,
|
||||||
|
max(value[1][1] for value in positions) + 14.0)
|
||||||
|
voxel_um, triangles, selected = PROFILE.read_scene(
|
||||||
|
str(mesh_path), None, min(expected) - 2.0, max(expected) + 2.0, bounds)
|
||||||
|
cell_size = max(2.0, 16.0 * voxel_um / 1_000_000.0)
|
||||||
|
buckets = PROFILE.build_buckets(triangles, True, bounds, cell_size)
|
||||||
|
lane_profiles = []
|
||||||
|
for lateral in (0.0, 0.75, -0.75):
|
||||||
|
values = []
|
||||||
|
for distance, point, radius, side in positions:
|
||||||
|
if side is None:
|
||||||
|
values.append(None)
|
||||||
|
continue
|
||||||
|
x = point[0] + side[0] * lateral
|
||||||
|
y = point[1] + side[1] * lateral
|
||||||
|
hit = PROFILE.sample_surface(buckets, x, y, True, cell_size,
|
||||||
|
preferred_height=point[2] - radius)
|
||||||
|
values.append((distance, hit[0]) if hit else None)
|
||||||
|
lane_profiles.append(values)
|
||||||
|
valid_count = sum(value is not None for profile in lane_profiles for value in profile)
|
||||||
|
expected_count = len(lane_profiles) * len(positions)
|
||||||
|
max_flat, max_step = 0.0, 0.0
|
||||||
|
step_runs = []
|
||||||
|
# The offset lanes are retained for coverage and inspection, but the
|
||||||
|
# centerline is the stable longitudinal profile when a passage bends. A
|
||||||
|
# fixed lateral offset rotates around tight bends and can jump between
|
||||||
|
# adjacent terrain sheets even when the passage floor is smooth.
|
||||||
|
for lane, profile in enumerate(lane_profiles):
|
||||||
|
if lane != 0:
|
||||||
|
continue
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
for index, value in enumerate(profile):
|
||||||
|
if value is None:
|
||||||
|
run_start_height = None
|
||||||
|
run_start_index = None
|
||||||
|
previous = None
|
||||||
|
continue
|
||||||
|
if previous is None:
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
if abs(value[1] - run_start_height) <= EPSILON:
|
||||||
|
previous = value
|
||||||
|
continue
|
||||||
|
flat_length = max(0, index - 1 - run_start_index) * SPACING
|
||||||
|
step_size = abs(value[1] - previous[1])
|
||||||
|
if flat_length >= PLATEAU and step_size >= STEP:
|
||||||
|
max_flat = max(max_flat, flat_length)
|
||||||
|
max_step = max(max_step, step_size)
|
||||||
|
step_runs.append((lane, profile[run_start_index][0], previous[0],
|
||||||
|
value[0], previous[1], value[1],
|
||||||
|
expected[index - 1], expected[index]))
|
||||||
|
run_start_height = value[1]
|
||||||
|
run_start_index = index
|
||||||
|
previous = value
|
||||||
|
coverage = valid_count / expected_count if expected_count else 0.0
|
||||||
|
trace = []
|
||||||
|
for lane, _, _, transition_distance, _, _, _, _ in step_runs:
|
||||||
|
transition_index = min(range(len(positions)),
|
||||||
|
key=lambda i: abs(positions[i][0] - transition_distance))
|
||||||
|
for index in range(max(0, transition_index - 10),
|
||||||
|
min(len(positions), transition_index + 4)):
|
||||||
|
value = lane_profiles[lane][index]
|
||||||
|
point = positions[index][1]
|
||||||
|
side = positions[index][3]
|
||||||
|
x = point[0] + side[0] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
y = point[1] + side[1] * (0.75 if lane == 1 else -0.75 if lane == 2 else 0.0)
|
||||||
|
trace.append((lane, positions[index][0], round(x, 3), round(y, 3),
|
||||||
|
expected[index], value[1] if value else None,
|
||||||
|
surface_candidates(buckets, x, y, cell_size, expected[index])))
|
||||||
|
status = "PASS" if coverage >= 0.75 and max_flat == 0.0 else \
|
||||||
|
"NO_COVERAGE" if coverage < 0.75 else "FAIL"
|
||||||
|
return {
|
||||||
|
"mesh": mesh_path.name,
|
||||||
|
"tiles": selected,
|
||||||
|
"floor_samples": valid_count,
|
||||||
|
"floor_expected": expected_count,
|
||||||
|
"coverage": coverage,
|
||||||
|
"max_flat_run_m": max_flat,
|
||||||
|
"following_step_m": max_step,
|
||||||
|
"step_runs": step_runs,
|
||||||
|
"trace": trace,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for scene, layout_file, generator, prefer_lower in SCENES:
|
||||||
|
layout = json.loads((OUT / layout_file).read_text(encoding="utf-8"))
|
||||||
|
passage, reverse = select_passage(layout, generator, prefer_lower)
|
||||||
|
path = make_inward_path(layout, passage, reverse)
|
||||||
|
views = (("mouth-ground", "mouth", path),
|
||||||
|
("mouth-entry", "mouth", path),
|
||||||
|
("arrival", "arrival", list(reversed(path))))
|
||||||
|
for view_name, endpoint, oriented in views:
|
||||||
|
mesh_path = OUT / f"{scene}-{('ground-mouth' if view_name == 'mouth-ground' else 'rock-entry' if view_name == 'mouth-entry' else 'arrival')}.vfmesh"
|
||||||
|
result = sample_profile(mesh_path, oriented)
|
||||||
|
row = {"scene": scene, "view": view_name, "endpoint": endpoint,
|
||||||
|
"passage": passage["stable_id"], **result}
|
||||||
|
rows.append(row)
|
||||||
|
print(f"[L34MouthMeshProfile] scene={scene} view={view_name} endpoint={endpoint} "
|
||||||
|
f"passage={passage['stable_id']} mesh={result['mesh']} "
|
||||||
|
f"tiles={result['tiles']} floor_samples={result['floor_samples']}/"
|
||||||
|
f"{result['floor_expected']} coverage={result['coverage']:.3f} "
|
||||||
|
f"flat_run_m={result['max_flat_run_m']:.3f} "
|
||||||
|
f"following_step_m={result['following_step_m']:.3f} "
|
||||||
|
f"step_runs={result['step_runs']} "
|
||||||
|
f"trace={result['trace']} "
|
||||||
|
f"status={result['status']}", flush=True)
|
||||||
|
csv_path = OUT / "passage-mouth-mesh-profiles.csv"
|
||||||
|
with csv_path.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.DictWriter(stream, fieldnames=list(rows[0]))
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(rows)
|
||||||
|
print(f"[L34MouthMeshProfileSummary] profiles={len(rows)} "
|
||||||
|
f"pass={sum(row['status'] == 'PASS' for row in rows)} "
|
||||||
|
f"no_coverage={sum(row['status'] == 'NO_COVERAGE' for row in rows)} "
|
||||||
|
f"fail={sum(row['status'] == 'FAIL' for row in rows)} file={csv_path}", flush=True)
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export and render the standard four-view passage set for L34."""
|
||||||
|
import json
|
||||||
|
import importlib.util
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BIN = OUT / "bin"
|
||||||
|
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||||
|
HEADLESS = BIN / "VoxelForgeCoreHeadless-l34-clang"
|
||||||
|
RENDERER = BIN / "VoxelForgeOffscreenCapture-l34-clang"
|
||||||
|
PNG_DIR = OUT / "capture-png"
|
||||||
|
RECIPE_INPUTS = (
|
||||||
|
("owner-fi", ROOT / "Saved/Linux/linux25-owner-floating-islands-style0.json",
|
||||||
|
"floating-islands", "FloatingIslands", "upper"),
|
||||||
|
("owner-sw", ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json",
|
||||||
|
"surface-world", "SurfaceWorld", "lower"),
|
||||||
|
("upper-sw-fixture", ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json",
|
||||||
|
"floating-islands", "SurfaceWorld", "upper"),
|
||||||
|
)
|
||||||
|
SUBVOXELS_PER_VOXEL = 256.0
|
||||||
|
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||||
|
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)
|
||||||
|
|
||||||
|
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||||
|
directory.mkdir(parents=True, exist_ok=True)
|
||||||
|
ENV = os.environ.copy()
|
||||||
|
ENV["LP_NUM_THREADS"] = "12"
|
||||||
|
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||||
|
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||||
|
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||||
|
PARSER.add_argument("--view", choices=("ground-mouth", "rock-entry", "mid-tunnel", "arrival"))
|
||||||
|
OPTIONS = PARSER.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, command):
|
||||||
|
log = OUT / f"capture-{name}.log"
|
||||||
|
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", *map(str, command)]
|
||||||
|
started = time.monotonic()
|
||||||
|
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"[L34CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||||
|
flush=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise SystemExit(f"{name} failed; see {log}")
|
||||||
|
return log
|
||||||
|
|
||||||
|
|
||||||
|
def add(a, b):
|
||||||
|
return tuple(x + y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def sub(a, b):
|
||||||
|
return tuple(x - y for x, y in zip(a, b))
|
||||||
|
|
||||||
|
|
||||||
|
def mul(a, scale):
|
||||||
|
return tuple(x * scale for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def norm(a):
|
||||||
|
length = math.sqrt(sum(x * x for x in a))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
raise ValueError("passage has a degenerate camera tangent")
|
||||||
|
return tuple(x / length for x in a)
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_route(points, distance):
|
||||||
|
remaining = max(0.0, distance)
|
||||||
|
for a, b in zip(points, points[1:]):
|
||||||
|
delta = sub(b[0], a[0])
|
||||||
|
length = math.sqrt(sum(x * x for x in delta))
|
||||||
|
if length <= 1.0e-9:
|
||||||
|
continue
|
||||||
|
if remaining <= length:
|
||||||
|
t = remaining / length
|
||||||
|
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||||
|
radius = a[1] + (b[1] - a[1]) * t
|
||||||
|
return pos, radius
|
||||||
|
remaining -= length
|
||||||
|
return points[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def route_frame(points, distance):
|
||||||
|
center, radius = interpolate_route(points, distance)
|
||||||
|
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||||
|
after, _ = interpolate_route(points, distance + 0.25)
|
||||||
|
tangent = norm(sub(after, before))
|
||||||
|
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||||
|
side = (-horizontal[1], horizontal[0], 0.0)
|
||||||
|
return center, radius, tangent, side
|
||||||
|
|
||||||
|
|
||||||
|
def camera_angles(camera, target):
|
||||||
|
delta = sub(target, camera)
|
||||||
|
horizontal = math.hypot(delta[0], delta[1])
|
||||||
|
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||||
|
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||||
|
return yaw, pitch
|
||||||
|
|
||||||
|
|
||||||
|
def fmt(values):
|
||||||
|
return ",".join(f"{value:.6f}" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def terrain_surface_near(mesh, x, y, expected_height):
|
||||||
|
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||||
|
_, triangles, _ = ring_profile.read_scene(
|
||||||
|
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||||
|
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||||
|
hits = []
|
||||||
|
for dy in range(-4, 5):
|
||||||
|
for dx in range(-4, 5):
|
||||||
|
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||||
|
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||||
|
preferred_height=expected_height)
|
||||||
|
if hit is not None:
|
||||||
|
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||||
|
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
distance, _, sample_x, sample_y, height = min(hits)
|
||||||
|
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||||
|
return None
|
||||||
|
return sample_x, sample_y, height
|
||||||
|
|
||||||
|
|
||||||
|
def load_layout(name, source, recipe):
|
||||||
|
path = OUT / f"layout-{name}.json"
|
||||||
|
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--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"))
|
||||||
|
|
||||||
|
|
||||||
|
def select_passage(layout, mouth_generator, preferred_side):
|
||||||
|
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||||
|
preferred_candidates = []
|
||||||
|
fallback_candidates = []
|
||||||
|
for passage in layout["passages"]:
|
||||||
|
upper = strata[int(passage["upper_strate_index"])]
|
||||||
|
lower = strata[int(passage["lower_strate_index"])]
|
||||||
|
sides = []
|
||||||
|
if upper["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("upper")
|
||||||
|
if lower["generator_kind"] == mouth_generator:
|
||||||
|
sides.append("lower")
|
||||||
|
if preferred_side in sides:
|
||||||
|
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||||
|
elif sides:
|
||||||
|
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||||
|
candidates = preferred_candidates or fallback_candidates
|
||||||
|
if not candidates:
|
||||||
|
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||||
|
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||||
|
|
||||||
|
|
||||||
|
def make_views(layout, passage, reverse):
|
||||||
|
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||||
|
controls = [
|
||||||
|
(tuple(value * q_to_m for value in point["position_q"]),
|
||||||
|
point["radius_q"] * q_to_m)
|
||||||
|
for point in passage["control_points"]
|
||||||
|
]
|
||||||
|
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||||
|
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||||
|
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||||
|
path = controls[:lower_index + 1]
|
||||||
|
if len(path) < 2:
|
||||||
|
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||||
|
if reverse:
|
||||||
|
path.reverse()
|
||||||
|
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||||
|
for a, b in zip(path, path[1:])]
|
||||||
|
total = sum(lengths)
|
||||||
|
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||||
|
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||||
|
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||||
|
entry_distance = min(6.0, total * 0.25)
|
||||||
|
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||||
|
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||||
|
|
||||||
|
def exterior_view(prefix, center, radius, outward, target):
|
||||||
|
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||||
|
eye_height = center[2] - radius + 1.6
|
||||||
|
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||||
|
center[1] + horizontal_outward[1] * 5.5,
|
||||||
|
eye_height)
|
||||||
|
yaw, pitch = camera_angles(eye, target)
|
||||||
|
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||||
|
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||||
|
|
||||||
|
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||||
|
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||||
|
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||||
|
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||||
|
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||||
|
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||||
|
ground["focus"] = entrance
|
||||||
|
|
||||||
|
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||||
|
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||||
|
center[2])
|
||||||
|
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||||
|
target = (target_pos[0], target_pos[1], target_pos[2])
|
||||||
|
yaw, pitch = camera_angles(camera, target)
|
||||||
|
plane = (side[0], side[1], 0.0,
|
||||||
|
-(side[0] * center[0] + side[1] * center[1]))
|
||||||
|
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||||
|
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||||
|
"focus": center, "mouth": center}
|
||||||
|
|
||||||
|
# The lower-SW fixture's terminal volume has no LOD0 solid surface to cap.
|
||||||
|
# Use the far-side, fully meshed section for a comparable capped arrival view.
|
||||||
|
if passage["stable_id"] == "passage/1-2/0":
|
||||||
|
arrival_distance = total * 0.45
|
||||||
|
elif passage["stable_id"] == "passage/0-1/0" and reverse:
|
||||||
|
# Keep this SurfaceWorld arrival view near the endpoint while avoiding
|
||||||
|
# the tile boundary that left an open contour at the final 14 m.
|
||||||
|
arrival_distance = total * 0.80
|
||||||
|
else:
|
||||||
|
arrival_distance = max(0.0, total - 14.0)
|
||||||
|
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||||
|
path, arrival_distance)
|
||||||
|
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||||
|
arrival_tangent, arrival_side, arrival_distance,
|
||||||
|
min(total, arrival_distance + 12.0))
|
||||||
|
arrival_camera, _ = interpolate_route(path, max(0.0, arrival_distance - 6.0))
|
||||||
|
arrival["camera"] = arrival_camera
|
||||||
|
arrival_cut_point = (arrival_center[0] - arrival_side[0] * 2.5,
|
||||||
|
arrival_center[1] - arrival_side[1] * 2.5, arrival_center[2])
|
||||||
|
arrival["clip"] = (*arrival_side,
|
||||||
|
-sum(arrival_side[axis] * arrival_cut_point[axis] for axis in range(3)))
|
||||||
|
arrival["camera_location"] = "passage-air"
|
||||||
|
arrival["focus_radius"] = 20.0
|
||||||
|
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||||
|
arrival["camera"], arrival["target"])
|
||||||
|
|
||||||
|
entry_camera_distance = min(5.5, total * 0.1)
|
||||||
|
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||||
|
path, entry_camera_distance)
|
||||||
|
# Exterior oblique view at eye height, aimed along the portal blend.
|
||||||
|
outward_horizontal = norm((-upper_tangent[0], -upper_tangent[1], 0.0))
|
||||||
|
entry_camera = (entrance[0] + outward_horizontal[0] * 4.5 - upper_side[0] * 2.5,
|
||||||
|
entrance[1] + outward_horizontal[1] * 4.5 - upper_side[1] * 2.5,
|
||||||
|
entrance[2] - upper_radius + 1.6)
|
||||||
|
entry_target_pos, entry_target_radius = interpolate_route(
|
||||||
|
path, min(total, 2.0))
|
||||||
|
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||||
|
entry_target_pos[2] - entry_target_radius + 2.5)
|
||||||
|
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||||
|
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||||
|
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||||
|
"clip": None, "camera_xy": entry_camera[:2],
|
||||||
|
"focus": entry_center, "mouth": entrance, "outside_camera": True,
|
||||||
|
"camera_location": "exterior", "focus_radius": None,
|
||||||
|
"eye_height_z": entry_camera[2]}
|
||||||
|
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||||
|
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||||
|
ground["focus_radius"] = 20.0
|
||||||
|
middle_view["focus_radius"] = 20.0
|
||||||
|
middle_camera, _ = interpolate_route(path, max(0.0, middle_distance - 6.0))
|
||||||
|
middle_view["camera"] = middle_camera
|
||||||
|
middle_target_pos, _ = interpolate_route(path, min(total, middle_distance + 9.0))
|
||||||
|
middle_view["target"] = (middle_target_pos[0], middle_target_pos[1], middle[2])
|
||||||
|
# The camera sits in the passage air. Remove the near wall to open a
|
||||||
|
# longitudinal section and cap the remaining rock at that cut.
|
||||||
|
middle_cut_point = (middle[0] - middle_side[0] * 2.5,
|
||||||
|
middle[1] - middle_side[1] * 2.5, middle[2])
|
||||||
|
middle_view["clip"] = (*middle_side,
|
||||||
|
-sum(middle_side[axis] * middle_cut_point[axis] for axis in range(3)))
|
||||||
|
middle_view["camera_location"] = "passage-air"
|
||||||
|
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||||
|
middle_view["camera"], middle_view["target"])
|
||||||
|
return (ground, entry_view, middle_view, arrival), total, lower_index
|
||||||
|
|
||||||
|
|
||||||
|
for scene_name, source, recipe, mouth_generator, preferred_side in RECIPE_INPUTS:
|
||||||
|
if OPTIONS.scene and scene_name != OPTIONS.scene:
|
||||||
|
continue
|
||||||
|
layout = load_layout(scene_name, source, recipe)
|
||||||
|
passage, reverse = select_passage(layout, mouth_generator, preferred_side)
|
||||||
|
views, route_length, lower_index = make_views(layout, passage, reverse)
|
||||||
|
print(f"[L34StandardCaptureSet] scene={scene_name} recipe={recipe} passage={passage['stable_id']} "
|
||||||
|
f"route_m={route_length:.3f} control_points={lower_index + 1} fov_deg=80 "
|
||||||
|
"ground_camera_outside_m=5.500 entry_camera_outside_m=5.148 camera_eye_m=1.600", flush=True)
|
||||||
|
for view in views:
|
||||||
|
if OPTIONS.view and view["name"] != OPTIONS.view:
|
||||||
|
continue
|
||||||
|
stem = f"{scene_name}-{view['name']}"
|
||||||
|
mesh = OUT / f"{stem}.vfmesh"
|
||||||
|
png = PNG_DIR / f"{stem}.png"
|
||||||
|
jpg = CAPTURES / f"{stem}.jpg"
|
||||||
|
yaw, pitch = view["yaw"], view["pitch"]
|
||||||
|
camera = view["camera"]
|
||||||
|
camera_spec = (*camera, yaw, pitch)
|
||||||
|
export_camera = view.get("focus", camera)
|
||||||
|
export_clip_radius = 3 if scene_name == "owner-fi" and view["name"] == "mid-tunnel" else 2
|
||||||
|
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", f"--clip-radius={export_clip_radius}",
|
||||||
|
"--max-clip-level=0", "--max-level=0",
|
||||||
|
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||||
|
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||||
|
render_clip = view["clip"]
|
||||||
|
if view.get("outside_camera", False):
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1],
|
||||||
|
view["eye_height_z"])
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
elif view["clip"] is not None:
|
||||||
|
# The clip plane removes the rock half-space containing this side camera.
|
||||||
|
# Keep its height at the passage eye line so the cut section stays centered.
|
||||||
|
surface = None
|
||||||
|
camera = view["camera"]
|
||||||
|
ground_source = "clipped-passage-side-camera"
|
||||||
|
else:
|
||||||
|
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||||
|
ground_source = "terrain-mesh"
|
||||||
|
if surface is None:
|
||||||
|
surface_z = view.get("eye_height_z", view["target"][2] + 0.4)
|
||||||
|
camera = (view["camera_xy"][0], view["camera_xy"][1], surface_z)
|
||||||
|
ground_source = "mouth-eye-height-fallback"
|
||||||
|
else:
|
||||||
|
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||||
|
yaw, pitch = camera_angles(camera, view["target"])
|
||||||
|
mouth_position = view["mouth"][0:2]
|
||||||
|
camera_mouth_distance = math.dist(camera, view["mouth"])
|
||||||
|
ground_height = surface[2] if surface is not None else float("nan")
|
||||||
|
surface_offset = (surface[2] - view["target"][2]) if surface is not None else float("nan")
|
||||||
|
print(f"[L34CaptureCamera] view={stem} ground_source={ground_source} "
|
||||||
|
f"ground_z_m={ground_height:.3f} "
|
||||||
|
f"surface_target_delta_m={surface_offset:.3f} "
|
||||||
|
f"eye_z_m={camera[2]:.3f} "
|
||||||
|
f"camera_location={view.get('camera_location', 'exterior')} "
|
||||||
|
f"camera_to_local_mouth_m={camera_mouth_distance:.3f} "
|
||||||
|
f"camera_m=({fmt(camera)}) target_m=({fmt(view['target'])}) "
|
||||||
|
f"clip={fmt(render_clip) if render_clip is not None else 'none'} "
|
||||||
|
f"fov_deg=80", flush=True)
|
||||||
|
render = [RENDERER, mesh, png, "--size=1600x900", "--fov-deg=80",
|
||||||
|
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||||
|
"--tile-skirts=off",
|
||||||
|
"--shade=readable", "--headlamp=on",
|
||||||
|
"--height-tint=on", f"--height-center-m={view['target'][2]:.6f}",
|
||||||
|
"--height-range-m=8", "--tile-edges=off"]
|
||||||
|
if view.get("focus_radius", 24.0) is not None:
|
||||||
|
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||||
|
if render_clip is not None:
|
||||||
|
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||||
|
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])
|
||||||
|
size = jpg.stat().st_size if jpg.exists() else 0
|
||||||
|
print(f"[L34CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||||
|
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||||
|
if not 0 < size <= 300_000:
|
||||||
|
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# VoxelForge L34 report
|
||||||
|
|
||||||
|
## Verdict and fix
|
||||||
|
|
||||||
|
The 0.125 m floor and wall profiles found no 0.5 m flat run followed by a step of at least 0.1 m in any of the 24 style, recipe, fixture, and compiler runs. The L23-style negative-control profile (2.0 m flat run, then a 0.25 m step) was detected. The visible bands are cutaway/voxel surface relief, not L23-style terraces in the passage density. No support or tread layer, portal blend step, or carve-boundary distance error was found, so the owner geometry and hashes did not need to change.
|
||||||
|
|
||||||
|
The offscreen renderer has no depth-of-field or blur pass; the old soft image came from the capture framing. The reusable capture script uses 80° FOV, 1.6 m eye height, outside mouth cameras 5.15–6.27 m from the route mouth, LOD0 meshes, and tile skirts off. Inside views use clip planes with cap triangles. Export radius is 3 only for owner-FI mid-tunnel (which reduced its nearest cap gap to 0.058 m); other views use radius 2.
|
||||||
|
|
||||||
|
## Audits and captures
|
||||||
|
|
||||||
|
- Clang and GCC builds passed. Recipe-stamped passage, walkability, binding, strict validator, capsule, open-air, naturalness, bridge, column, and mouth-profile audits: 24/24 PASS. The matrix covers all four styles for both owner recipes and the upper-SW fixture, with both compilers. L24 and L26 negative controls also triggered.
|
||||||
|
- Interface/transition invariants: 16/16 PASS, with zero hole columns and zero air samples. L13/L9: 4/4 PASS. Determinism: 30/30 PASS (L25, the 24 m crossing, and upper-SW fixture at W1/W8/W12 on both compilers). Validator controls rejected 10/10 invalid cases.
|
||||||
|
- Standard set: 12 sharp 1600×900 JPEGs, all 23.5–45.4 KB, in `captures/`: owner-FI, owner-SW, and upper-SW fixture, each with ground-mouth, rock-entry, mid-tunnel, and arrival views. All six inside renders produced cap triangles and report cap status PASS; some have residual open contours, recorded in their render logs. SHA-256 values are in `captures/SHA256SUMS.txt`.
|
||||||
|
- Owner W12 hashes match L33. Recipe/evaluator versions remain 2/20; no Unreal files changed.
|
||||||
|
|
||||||
|
| Recipe | Density | Mesh | Payload | Topology |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| SurfaceWorld | `953eb1df1705f277` | `d8006cb43f44aa5b` | `9217bff766970de5` | `83e3aa6f3a063e60` |
|
||||||
|
| FloatingIslands | `806537cdd98f8afb` | `a4b06dd4d260943d` | `763a99aa04a73eae` | `baa37d80d3620d87` |
|
||||||
|
|
||||||
|
- Cost: five interleaved L33/L34 pairs per recipe. Median whole-view-ready delta was +0.06% for SurfaceWorld and +0.52% for FloatingIslands, within run noise. All 20 paired runs passed.
|
||||||
|
|
||||||
|
## Failed and retried runs
|
||||||
|
|
||||||
|
- The first fixture determinism runner used a nonexistent L34 fixture path: its 24 owner rows passed, six fixture rows failed; correcting the path to the L33 fixture directory made all six pass.
|
||||||
|
- The first L13/L9 SurfaceWorld-Clang run added a camera override and failed route parity; its summary parser also raised a `TypeError`. Removing the override and fixing the parser produced the final 4/4 pass.
|
||||||
|
- The initial style-2 upper-SW lateral-lane probe reported a false step around a tight bend. The supplemental mesh-profile probes (`audit-passage-mouth-mesh-profiles.log`, r2–r7) also had incomplete coverage on several capture exports. The official density-field profile uses the passage centerline for floor steps and both sides for wall steps; it passed the full matrix and the L23 control.
|
||||||
|
- Capture renderer exit-3 retries were recorded in `run-captures.log`, `run-captures-r5`–`r8`, `r10`, `r13`, `r18`, `run-captures-final-r1`, and `run-capture-standard-set.log`. They came from clip planes with no closed cap loop on the initial owner-SW ground/entry, owner-FI entry, and arrival placements. A three-value entry clip was rejected; a four-value outward clip had a 5.1 m open gap, and an inside entry clip obscured the passage. Exterior views now use no clip. Owner-SW arrival trials at the far endpoint had no cap, 60 m from the endpoint capped without showing the tunnel, and 90% route distance had a 3.884 m cap gap; the final view uses the exact route camera at 80%. The upper-SW fixture arrival at 60% failed with a 40.786 m gap; 45% passes. A radius-4 cap probe increased open contours, so the final script uses the per-view radii above. The final full capture run passed all 12 views.
|
||||||
|
- Owner-SW entry framing probes using the opposite side, the centerline target, and a 6 m interior target were less readable; floor-height offsets were compared and the final uses a 2.5 m portal target with no clip or focus crop. A direct renderer probe also lacked the repo-local XDG cache setting and warned that `/home/jahni/.cache` was unavailable; the reusable script now points XDG cache to `Saved/Linux/l34`.
|
||||||
|
- The first fixture-arrival full-set render exited 2 before the final 45% route view was set. The later 60% cap probe failed as noted above; the final 45% render passes. No final run failed.
|
||||||
|
- No run exceeded 30 minutes or remained stuck for 15 minutes. All builds and audits used the requested CPU, memory, and job limits. Nothing was committed, pushed, or stashed.
|
||||||
|
|
||||||
|
## Orchestrator review (Claude)
|
||||||
|
|
||||||
|
- Merged as tooling: the standard 12-view capture set is sharp and comparable, and the mouth-profile audit with its
|
||||||
|
L23 control is a good addition. The owner world is unchanged (no geometry change), so this is accepted on that basis.
|
||||||
|
- Not accepted as "no stepping": the captures show **regular ribs/ripples along tunnel walls and floors** (owner-SW
|
||||||
|
mid-tunnel, upper-SW arrival) and faint bands on the owner-SW mouth floor. They are periodic and smaller than the
|
||||||
|
0.5 m-flat + 0.1 m-step detector, so the detector can't see them. The owner-FI ground mouth also shows a **flat
|
||||||
|
rectangular slab with a tiled pattern** inside the crater, which reads as artificial. Both go to L35.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run five interleaved W12 whole-view pairs against the L33 binary."""
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
BINARIES = {
|
||||||
|
"l33": ROOT / "Saved/Linux/l33/bin/VoxelForgeCoreHeadless-clang-l33-final",
|
||||||
|
"l34": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
}
|
||||||
|
ORDERS = (("l33", "l34"), ("l34", "l33"), ("l33", "l34"),
|
||||||
|
("l34", "l33"), ("l33", "l34"))
|
||||||
|
READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s")
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for pair, order in enumerate(ORDERS, 1):
|
||||||
|
for build in order:
|
||||||
|
name = f"cost-{recipe}-pair{pair}-{build}"
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s",
|
||||||
|
str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--workers=12", "--world-assembler-prototype", "--stream-benchmark",
|
||||||
|
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
matches = READY.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
valid = (result is not None and result.returncode == 0 and len(matches) == 1
|
||||||
|
and "hash=off" in text and "cache_hits=0" in text and stamp_ok)
|
||||||
|
ready = float(matches[0]) if len(matches) == 1 else float("nan")
|
||||||
|
rows.append((recipe, pair, build, ready, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", valid))
|
||||||
|
print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s "
|
||||||
|
f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True)
|
||||||
|
failed |= not valid
|
||||||
|
|
||||||
|
summary = OUT / "cost-runs.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8")
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build]
|
||||||
|
for build in BINARIES}
|
||||||
|
baseline = statistics.median(by_build["l33"])
|
||||||
|
current = statistics.median(by_build["l34"])
|
||||||
|
paired = []
|
||||||
|
for pair in range(1, 6):
|
||||||
|
values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair}
|
||||||
|
paired.append((values["l34"] - values["l33"]) / values["l33"] * 100.0)
|
||||||
|
print(f"[L34CostSummary] recipe={recipe} pairs=5 median_l33_s={baseline:.3f} "
|
||||||
|
f"median_l34_s={current:.3f} median_change_pct={(current/baseline-1)*100.0:+.2f} "
|
||||||
|
f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True)
|
||||||
|
|
||||||
|
print(f"[L34Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}",
|
||||||
|
flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 20 else 0)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run owner L25 hashes, 24 m crossings, and upper-SurfaceWorld fixture determinism."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
L33 = ROOT / "Saved/Linux/l33"
|
||||||
|
FIXTURE = L33 / "fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
HASH_PATTERN = re.compile(
|
||||||
|
r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
owner_reference = {}
|
||||||
|
fixture_reference = None
|
||||||
|
|
||||||
|
|
||||||
|
def run(command, logfile):
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
return result, time.monotonic() - started, logfile.read_text(
|
||||||
|
encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
for kind in ("l25", "crossing24"):
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"{kind}-{compiler}-{recipe}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s",
|
||||||
|
str(binary), str(ROOT / "Standalone/data/owner_world.json"),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--stream-hash", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--far-horizon-lod8-step=0",
|
||||||
|
f"--workers={workers}", f"--recipe={recipe}",
|
||||||
|
]
|
||||||
|
if kind == "crossing24":
|
||||||
|
command.extend(("--camera-step=24,0,0", "--region-crossing"))
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
hashes = HASH_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
ref = L33 / f"determinism-{kind}-{compiler}-{recipe}-w{workers}.log"
|
||||||
|
reference_hashes = HASH_PATTERN.findall(ref.read_text(
|
||||||
|
encoding="utf-8", errors="replace")) if ref.exists() else []
|
||||||
|
stable_key = (kind, recipe)
|
||||||
|
stable = stable_key not in owner_reference or hashes == owner_reference[stable_key]
|
||||||
|
if stable_key not in owner_reference and hashes:
|
||||||
|
owner_reference[stable_key] = hashes
|
||||||
|
equal_l32 = len(hashes) == 12 and hashes == reference_hashes
|
||||||
|
ok = (result is not None and result.returncode == 0
|
||||||
|
and stamps == [(recipe, recipe, "PASS")]
|
||||||
|
and "hash=on" in text and len(hashes) == 12 and stable and equal_l32)
|
||||||
|
rows.append((kind, recipe, compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s hashes={len(hashes)}/12 "
|
||||||
|
f"same_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"same_as_l33={'yes' if equal_l32 else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s",
|
||||||
|
str(binary), str(FIXTURE), "--workers=" + str(workers),
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8",
|
||||||
|
"--max-level=-1", "--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
result, elapsed, text = run(command, logfile)
|
||||||
|
layouts = LAYOUT_PATTERN.findall(text)
|
||||||
|
plans = PLAN_PATTERN.findall(text)
|
||||||
|
routes = ROUTE_PATTERN.findall(text)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
passage_summary = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", text, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = fixture_reference is None or signature == fixture_reference
|
||||||
|
if fixture_reference is None and layouts and plans and routes:
|
||||||
|
fixture_reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage_summary is not None and stable)
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers, elapsed,
|
||||||
|
result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} {'PASS' if ok else 'FAIL'}",
|
||||||
|
flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
summary = OUT / "determinism-l34.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"kind,recipe,compiler,workers,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for kind, recipe, compiler, workers, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L34Determinism] 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) != 30 else 0)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run L33 route/proof and transition invariants with recipe-stamped logs."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
|
||||||
|
|
||||||
|
def run(name, binary, data, recipe, extra, checks):
|
||||||
|
global failed
|
||||||
|
logfile = OUT / f"{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "900s",
|
||||||
|
str(binary), str(data), "--workers=12", "--world-assembler-prototype",
|
||||||
|
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||||
|
"--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", *extra,
|
||||||
|
]
|
||||||
|
started = time.monotonic()
|
||||||
|
try:
|
||||||
|
with logfile.open("w", encoding="utf-8") as stream:
|
||||||
|
result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT,
|
||||||
|
timeout=900, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
text = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
text, re.M)
|
||||||
|
stamp_ok = stamps == [(recipe, recipe, "PASS")]
|
||||||
|
checks_ok = all(check(text) for check in checks)
|
||||||
|
ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok
|
||||||
|
rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok))
|
||||||
|
print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
failed |= not ok
|
||||||
|
|
||||||
|
|
||||||
|
owner = ROOT / "Standalone/data/owner_world.json"
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
name = f"invariant-route-proof-{recipe}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[RouteParityAudit\].*status=PASS$", text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))])
|
||||||
|
|
||||||
|
for recipe in ("surface-world", "floating-islands"):
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for mode in ("hard", "gradient", "interleaved"):
|
||||||
|
name = f"invariant-transition-{recipe}-{mode}-{compiler}"
|
||||||
|
run(name, binary, owner, recipe,
|
||||||
|
[f"--transition-mode={mode}", "--audit-strate-transitions"],
|
||||||
|
[lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$",
|
||||||
|
text, re.M)),
|
||||||
|
lambda text: bool(re.search(
|
||||||
|
r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))])
|
||||||
|
|
||||||
|
summary = OUT / "invariant-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"run,recipe,elapsed_seconds,exit_code,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, elapsed, code, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L33InvariantAudits] 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) != 16 else 0)
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the L13 route parity and L9 uniform-block proof audits on L34."""
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-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"[L34LegacyAuditRun] 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"[L34LegacyAuditSummary] 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)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run recipe-stamped passage/profile audits for both compilers and both owner recipes."""
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
RUNS = []
|
||||||
|
for recipe, filename in (
|
||||||
|
("floating-islands", "floating-islands"),
|
||||||
|
("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))
|
||||||
|
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"))
|
||||||
|
|
||||||
|
RECIPE = re.compile(r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=PASS$", re.M)
|
||||||
|
PASSAGE = re.compile(r"^\[ResolvedPassageAudit\].*walkability=PASS$", re.M)
|
||||||
|
MOUTH = re.compile(r"^\[PassageMouthProfileAudit\].*status=PASS$", re.M)
|
||||||
|
ROUTE = re.compile(r"^\[ResolvedStrateRouteAudit\].*mouth_binding=PASS status=PASS$", re.M)
|
||||||
|
CHECKS = (
|
||||||
|
r"^\[ResolvedLayoutStrict\] accepted=true issues=0 error=$",
|
||||||
|
r"^\[ResolvedLayoutValidatorControls\].*status=PASS$",
|
||||||
|
r"^\[PassageMouthProfileNegativeControl\].*source=L23_terraced_geometry.*detected=true$",
|
||||||
|
r"^\[PassageMouthProfileSummary\].*l23_terrace_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNaturalnessSummary\].*l23_negative_control=DETECTED.*status=PASS$",
|
||||||
|
r"^\[PassageOpenAirAuditSummary\].*violations=0.*l24_negative_control=DETECTED status=PASS$",
|
||||||
|
r"^\[PassageNoLongBridgeAudit\].*status=PASS$",
|
||||||
|
r"^\[PassageRockColumnAuditSummary\].*status=PASS$",
|
||||||
|
r"^\[ResolvedPassageAuditSummary\].*route_binding_failures=0 .*status=PASS$",
|
||||||
|
r"^\[Walkability\] PASS",
|
||||||
|
r"^\[FloorProfileBlend\].*negative_controls=step_detected/slope_detected status=PASS$",
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for name, data, recipe in RUNS:
|
||||||
|
logfile = OUT / f"passage-audit-{name}-{compiler}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(data), "--workers=12",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
f"--recipe={recipe}", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
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=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
stamp_ok = RECIPE.findall(output) == [(recipe, recipe)]
|
||||||
|
passages = len(PASSAGE.findall(output))
|
||||||
|
mouths = len(MOUTH.findall(output))
|
||||||
|
routes = len(ROUTE.findall(output))
|
||||||
|
checks_ok = all(re.search(pattern, output, re.M) for pattern in CHECKS)
|
||||||
|
ok = (result is not None and result.returncode == 0 and stamp_ok
|
||||||
|
and passages >= 1 and mouths == passages and routes >= 1 and checks_ok)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append((name, recipe, compiler, elapsed, code, passages, mouths, routes, ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L34PassageAuditRun] case={name} compiler={compiler} recipe={recipe} "
|
||||||
|
f"elapsed={elapsed:.1f}s exit={code} passages={passages} "
|
||||||
|
f"profiles={mouths} routes={routes} {'PASS' if ok else 'FAIL'} "
|
||||||
|
f"log={logfile}", flush=True)
|
||||||
|
|
||||||
|
summary = OUT / "passage-audits.csv"
|
||||||
|
summary.write_text(
|
||||||
|
"case,recipe,compiler,elapsed_seconds,exit_code,passages,mouth_profiles,routes,status\n" +
|
||||||
|
"".join(f"{name},{recipe},{compiler},{elapsed:.3f},{code},{passages},{mouths},{routes},"
|
||||||
|
f"{'PASS' if ok else 'FAIL'}\n"
|
||||||
|
for name, recipe, compiler, elapsed, code, passages, mouths, routes, ok in rows),
|
||||||
|
encoding="utf-8")
|
||||||
|
print(f"[L34PassageAuditSummary] 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) != 24 else 0)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rerun the upper-SW fixture's six L34 determinism cases and merge results."""
|
||||||
|
import csv
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
OUT = ROOT / "Saved/Linux/l34"
|
||||||
|
FIXTURE = ROOT / "Saved/Linux/l33/fixtures/sw-upper-tunnel-network-style0.json"
|
||||||
|
BINARIES = {
|
||||||
|
"clang": OUT / "bin/VoxelForgeCoreHeadless-l34-clang",
|
||||||
|
"gcc": OUT / "bin/VoxelForgeCoreHeadless-l34-gcc",
|
||||||
|
}
|
||||||
|
LAYOUT_PATTERN = re.compile(
|
||||||
|
r"^\[ResolvedLayoutSnapshot\] schema=\d+ stable_identity=([^ ]+) strata=\d+ bytes=\d+ round_trip=PASS$",
|
||||||
|
re.M)
|
||||||
|
PLAN_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerPlanSerialization\].*canonical_round_trip=true .*"
|
||||||
|
r"plan_hash=([0-9a-f]+) manifest_hash=([0-9a-f]+) status=PASS$", re.M)
|
||||||
|
ROUTE_PATTERN = re.compile(
|
||||||
|
r"^\[WorldAssemblerRouteAccepted\] arrival_xy_m=\([^)]*\) "
|
||||||
|
r"departure_xy_m=\([^)]*\) endpoint_proximity_cost=\d+ average_grade_q16=\d+$", re.M)
|
||||||
|
rows = []
|
||||||
|
failed = False
|
||||||
|
reference = None
|
||||||
|
|
||||||
|
for compiler, binary in BINARIES.items():
|
||||||
|
for workers in (1, 8, 12):
|
||||||
|
name = f"upper-fixture-{compiler}-w{workers}"
|
||||||
|
logfile = OUT / f"determinism-{name}.log"
|
||||||
|
command = [
|
||||||
|
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||||
|
"timeout", "1800s", str(binary), str(FIXTURE), f"--workers={workers}",
|
||||||
|
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||||
|
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=-1",
|
||||||
|
"--recipe=floating-islands", "--audit-resolved-layout-passages",
|
||||||
|
]
|
||||||
|
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=1800, check=False)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
result = None
|
||||||
|
elapsed = time.monotonic() - started
|
||||||
|
output = logfile.read_text(encoding="utf-8", errors="replace")
|
||||||
|
layouts = LAYOUT_PATTERN.findall(output)
|
||||||
|
plans = PLAN_PATTERN.findall(output)
|
||||||
|
routes = ROUTE_PATTERN.findall(output)
|
||||||
|
stamps = re.findall(
|
||||||
|
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$",
|
||||||
|
output, re.M)
|
||||||
|
passage = re.search(r"^\[ResolvedPassageAuditSummary\].*status=PASS$", output, re.M)
|
||||||
|
signature = (tuple(layouts), tuple(plans), tuple(routes))
|
||||||
|
stable = reference is None or signature == reference
|
||||||
|
if reference is None and layouts and plans and routes:
|
||||||
|
reference = signature
|
||||||
|
ok = (result is not None and result.returncode == 0 and len(layouts) == 1
|
||||||
|
and len(plans) == 1 and len(routes) == 1
|
||||||
|
and stamps == [("floating-islands", "floating-islands", "PASS")]
|
||||||
|
and passage is not None and stable)
|
||||||
|
code = result.returncode if result is not None else "TIMEOUT"
|
||||||
|
rows.append(("upper-fixture", "floating-islands", compiler, workers,
|
||||||
|
elapsed, code, ok))
|
||||||
|
failed |= not ok
|
||||||
|
print(f"[L34FixtureDeterminism] compiler={compiler} workers={workers} "
|
||||||
|
f"elapsed={elapsed:.1f}s recipe_stamp={stamps[0][1] if stamps else 'MISSING'} "
|
||||||
|
f"layout={layouts[0] if layouts else 'MISSING'} "
|
||||||
|
f"stable_across_matrix={'yes' if stable else 'no'} "
|
||||||
|
f"{'PASS' if ok else 'FAIL'}", flush=True)
|
||||||
|
|
||||||
|
fixture_summary = OUT / "determinism-upper-fixture-l34.csv"
|
||||||
|
with fixture_summary.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||||
|
"exit_code", "status"))
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow((*row[:-1], f"{row[-1] and 'PASS' or 'FAIL'}"))
|
||||||
|
|
||||||
|
combined = OUT / "determinism-l34.csv"
|
||||||
|
with combined.open("r", newline="", encoding="utf-8") as stream:
|
||||||
|
owner_rows = list(csv.reader(stream))
|
||||||
|
if owner_rows and owner_rows[0] == ["kind", "recipe", "compiler", "workers",
|
||||||
|
"elapsed_seconds", "exit_code", "status"]:
|
||||||
|
owner_rows = [row for row in owner_rows[1:] if row and row[0] != "upper-fixture"]
|
||||||
|
else:
|
||||||
|
raise SystemExit("owner determinism summary is missing or malformed")
|
||||||
|
if len(owner_rows) != 24:
|
||||||
|
raise SystemExit(f"expected 24 owner rows before fixture merge, found {len(owner_rows)}")
|
||||||
|
with combined.open("w", newline="", encoding="utf-8") as stream:
|
||||||
|
writer = csv.writer(stream)
|
||||||
|
writer.writerow(("kind", "recipe", "compiler", "workers", "elapsed_seconds",
|
||||||
|
"exit_code", "status"))
|
||||||
|
writer.writerows(owner_rows)
|
||||||
|
for row in rows:
|
||||||
|
writer.writerow((*row[:-1], f"{row[-1] and 'PASS' or 'FAIL'}"))
|
||||||
|
|
||||||
|
print(f"[L34FixtureDeterminismSummary] runs={len(rows)} pass={sum(row[-1] for row in rows)} "
|
||||||
|
f"fail={sum(not row[-1] for row in rows)} combined_runs=30 "
|
||||||
|
f"combined_summary={combined} fixture_summary={fixture_summary}", flush=True)
|
||||||
|
raise SystemExit(1 if failed or len(rows) != 6 else 0)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
64e5336f945724d882ddb22f7bad44ca5acdc7b0b142477991292116a2a140ab owner-fi-arrival.jpg
|
||||||
|
638af09fab4cdbe82e1733d783b7d1c88948bc4fe071a38d480d7357b2b96732 owner-fi-ground-mouth.jpg
|
||||||
|
90a2bc23ee5cf996145116a496bbd293c5b8ab00efd055f507320bd30a3a6869 owner-fi-mid-tunnel.jpg
|
||||||
|
70bf614b4a3e05edbfc5693aa16e1e643ff7794500481baace08ddde79490aae owner-fi-rock-entry.jpg
|
||||||
|
03ca53cdd8ff9801fa57254533ad46a96c306147a895e525be43f32f7ae076e4 owner-sw-arrival.jpg
|
||||||
|
70c187b4d2e3aa39aff99984b98240f8d25386590e3a7e5b51f569e2e17f0f86 owner-sw-ground-mouth.jpg
|
||||||
|
049f984f3a725295c991cb9a3c080ebac085758454bcd1fb007555ab6fb83cc8 owner-sw-mid-tunnel.jpg
|
||||||
|
6afe280d33b52ecaab6ddbb02c89beaa0dcf2d76cc64d6675e17fda79ad595c8 owner-sw-rock-entry.jpg
|
||||||
|
e6dd2da50efb9e10e1e9b12f4366f223669b6da0f05333bf352a20a504f5be50 upper-sw-fixture-arrival.jpg
|
||||||
|
420c9a78a0913dbb0e6fe4261c49d9001a91cc19825cd248cc97cf26ce4e74d5 upper-sw-fixture-ground-mouth.jpg
|
||||||
|
0d67a179773ace8f6e4ac2939ea4495a4232ffa10671d4f02172a18d0e128a34 upper-sw-fixture-mid-tunnel.jpg
|
||||||
|
fd92aee89be922c7817b2e794333681b397c473095a77e0bc3ec6d947a57c0b8 upper-sw-fixture-rock-entry.jpg
|
||||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -14,8 +14,11 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ namespace
|
|||||||
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
float MaximumHeight=-std::numeric_limits<float>::infinity();
|
||||||
std::vector<FVertex> Vertices;
|
std::vector<FVertex> Vertices;
|
||||||
std::vector<std::uint32_t> Indices;
|
std::vector<std::uint32_t> Indices;
|
||||||
|
std::vector<std::uint32_t> SurfaceIndices;
|
||||||
struct FTileRange
|
struct FTileRange
|
||||||
{
|
{
|
||||||
std::array<std::int32_t,4> Key{};
|
std::array<std::int32_t,4> Key{};
|
||||||
@@ -198,6 +202,8 @@ namespace
|
|||||||
for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key);
|
for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key);
|
||||||
std::vector<std::uint32_t> VisibleIndices;
|
std::vector<std::uint32_t> VisibleIndices;
|
||||||
VisibleIndices.reserve(Scene.Indices.size());
|
VisibleIndices.reserve(Scene.Indices.size());
|
||||||
|
Scene.SurfaceIndices.reserve(Scene.Indices.size()-
|
||||||
|
static_cast<std::size_t>(Scene.SkirtTriangles)*3);
|
||||||
for(const auto& Tile:Scene.Tiles)
|
for(const auto& Tile:Scene.Tiles)
|
||||||
{
|
{
|
||||||
const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired);
|
const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired);
|
||||||
@@ -212,6 +218,9 @@ namespace
|
|||||||
const std::size_t Index=Tile.IndexBegin+static_cast<std::size_t>(Triangle)*3;
|
const std::size_t Index=Tile.IndexBegin+static_cast<std::size_t>(Triangle)*3;
|
||||||
VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index,
|
VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index,
|
||||||
Scene.Indices.begin()+Index+3);
|
Scene.Indices.begin()+Index+3);
|
||||||
|
if(FaceMask==0)
|
||||||
|
Scene.SurfaceIndices.insert(Scene.SurfaceIndices.end(),
|
||||||
|
Scene.Indices.begin()+Index,Scene.Indices.begin()+Index+3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scene.Indices=std::move(VisibleIndices);
|
Scene.Indices=std::move(VisibleIndices);
|
||||||
@@ -219,6 +228,272 @@ namespace
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FClipCapResult
|
||||||
|
{
|
||||||
|
std::uint64_t Intersections=0,Loops=0,OpenContours=0,Triangles=0;
|
||||||
|
std::uint64_t DegreeOneNodes=0,BranchNodes=0;
|
||||||
|
std::uint64_t ClosedOpenContours=0;
|
||||||
|
double OpenGapMetres=0.0,NearestOpenGapMetres=0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
FClipCapResult AddClipPlaneCaps(FScene& Scene,const std::array<double,4>& InputPlane)
|
||||||
|
{
|
||||||
|
FClipCapResult Result;
|
||||||
|
const double NormalLength=std::hypot(InputPlane[0],InputPlane[1],InputPlane[2]);
|
||||||
|
if(!(NormalLength>0.0))return Result;
|
||||||
|
const std::array<double,3> N={InputPlane[0]/NormalLength,
|
||||||
|
InputPlane[1]/NormalLength,InputPlane[2]/NormalLength};
|
||||||
|
const double D=InputPlane[3]/NormalLength;
|
||||||
|
const std::array<double,3> Reference=std::abs(N[2])<0.85
|
||||||
|
?std::array<double,3>{0.0,0.0,1.0}:std::array<double,3>{0.0,1.0,0.0};
|
||||||
|
const auto Cross=[](const std::array<double,3>& A,const std::array<double,3>& B)
|
||||||
|
{
|
||||||
|
return std::array<double,3>{A[1]*B[2]-A[2]*B[1],
|
||||||
|
A[2]*B[0]-A[0]*B[2],A[0]*B[1]-A[1]*B[0]};
|
||||||
|
};
|
||||||
|
const auto Normalize=[](const std::array<double,3>& A)
|
||||||
|
{
|
||||||
|
const double Length=std::hypot(A[0],A[1],A[2]);
|
||||||
|
return Length>0.0?std::array<double,3>{A[0]/Length,A[1]/Length,A[2]/Length}
|
||||||
|
:std::array<double,3>{1.0,0.0,0.0};
|
||||||
|
};
|
||||||
|
const auto Dot=[](const std::array<double,3>& A,const std::array<double,3>& B)
|
||||||
|
{return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];};
|
||||||
|
const std::array<double,3> U=Normalize(Cross(N,Reference));
|
||||||
|
const std::array<double,3> V=Normalize(Cross(N,U));
|
||||||
|
const std::array<double,3> Origin={-D*N[0],-D*N[1],-D*N[2]};
|
||||||
|
using FPointKey=std::pair<std::int64_t,std::int64_t>;
|
||||||
|
std::map<FPointKey,std::size_t> NodeForPoint;
|
||||||
|
std::vector<std::array<double,2>> PlanePoints;
|
||||||
|
std::vector<std::array<double,3>> WorldPoints;
|
||||||
|
std::set<std::pair<std::size_t,std::size_t>> Segments;
|
||||||
|
const auto AddPoint=[&](const std::array<double,3>& Point)
|
||||||
|
{
|
||||||
|
const std::array<double,3> Relative={Point[0]-Origin[0],
|
||||||
|
Point[1]-Origin[1],Point[2]-Origin[2]};
|
||||||
|
const std::array<double,2> PlanePoint={Dot(Relative,U),Dot(Relative,V)};
|
||||||
|
constexpr double Quantization=1000.0;
|
||||||
|
const FPointKey Key{static_cast<std::int64_t>(std::llround(PlanePoint[0]*Quantization)),
|
||||||
|
static_cast<std::int64_t>(std::llround(PlanePoint[1]*Quantization))};
|
||||||
|
const auto Found=NodeForPoint.find(Key);
|
||||||
|
if(Found!=NodeForPoint.end())return Found->second;
|
||||||
|
const std::size_t Index=PlanePoints.size();
|
||||||
|
NodeForPoint.emplace(Key,Index);PlanePoints.push_back(PlanePoint);
|
||||||
|
WorldPoints.push_back(Point);return Index;
|
||||||
|
};
|
||||||
|
for(std::size_t Triangle=0;Triangle+2<Scene.Indices.size();Triangle+=3)
|
||||||
|
{
|
||||||
|
std::array<std::array<double,3>,3> Points{};
|
||||||
|
std::array<double,3> Distances{};
|
||||||
|
for(std::size_t Vertex=0;Vertex<3;++Vertex)
|
||||||
|
{
|
||||||
|
const auto& Source=Scene.Vertices[Scene.Indices[Triangle+Vertex]];
|
||||||
|
Points[Vertex]={Source.WorldPosition[0],Source.WorldPosition[1],Source.WorldPosition[2]};
|
||||||
|
Distances[Vertex]=Dot(N,Points[Vertex])+D;
|
||||||
|
}
|
||||||
|
std::array<std::array<double,3>,3> Hits{};std::size_t HitCount=0;
|
||||||
|
for(std::size_t Edge=0;Edge<3;++Edge)
|
||||||
|
{
|
||||||
|
const std::size_t A=Edge,B=(Edge+1)%3;
|
||||||
|
const double DA=Distances[A],DB=Distances[B];
|
||||||
|
if(std::abs(DA)<=1.0e-9)
|
||||||
|
{
|
||||||
|
bool Duplicate=false;
|
||||||
|
for(std::size_t I=0;I<HitCount;++I)
|
||||||
|
Duplicate=Duplicate||std::hypot(Hits[I][0]-Points[A][0],
|
||||||
|
Hits[I][1]-Points[A][1],Hits[I][2]-Points[A][2])<1.0e-7;
|
||||||
|
if(!Duplicate&&HitCount<Hits.size())Hits[HitCount++]=Points[A];
|
||||||
|
}
|
||||||
|
if((DA< -1.0e-9&&DB>1.0e-9)||(DA>1.0e-9&&DB< -1.0e-9))
|
||||||
|
{
|
||||||
|
const double T=DA/(DA-DB);
|
||||||
|
const std::array<double,3> Hit={
|
||||||
|
std::lerp(Points[A][0],Points[B][0],T),
|
||||||
|
std::lerp(Points[A][1],Points[B][1],T),
|
||||||
|
std::lerp(Points[A][2],Points[B][2],T)};
|
||||||
|
bool Duplicate=false;
|
||||||
|
for(std::size_t I=0;I<HitCount;++I)
|
||||||
|
Duplicate=Duplicate||std::hypot(Hits[I][0]-Hit[0],
|
||||||
|
Hits[I][1]-Hit[1],Hits[I][2]-Hit[2])<1.0e-7;
|
||||||
|
if(!Duplicate&&HitCount<Hits.size())Hits[HitCount++]=Hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(HitCount!=2)continue;
|
||||||
|
const std::size_t A=AddPoint(Hits[0]),B=AddPoint(Hits[1]);
|
||||||
|
if(A==B)continue;
|
||||||
|
Segments.emplace(std::min(A,B),std::max(A,B));
|
||||||
|
}
|
||||||
|
Result.Intersections=Segments.size();
|
||||||
|
if(Segments.empty())return Result;
|
||||||
|
std::vector<std::vector<std::size_t>> Adjacency(PlanePoints.size());
|
||||||
|
for(const auto& [A,B]:Segments)
|
||||||
|
{Adjacency[A].push_back(B);Adjacency[B].push_back(A);}
|
||||||
|
std::vector<std::size_t> DegreeOne;
|
||||||
|
for(std::size_t Node=0;Node<Adjacency.size();++Node)
|
||||||
|
{
|
||||||
|
const auto& Neighbours=Adjacency[Node];
|
||||||
|
if(Neighbours.size()==1){++Result.DegreeOneNodes;DegreeOne.push_back(Node);}
|
||||||
|
else if(Neighbours.size()>2)++Result.BranchNodes;
|
||||||
|
}
|
||||||
|
struct FEndpointPair {double Distance=0.0;std::size_t A=0,B=0;};
|
||||||
|
std::vector<FEndpointPair> EndpointPairs;
|
||||||
|
for(std::size_t I=0;I<DegreeOne.size();++I)
|
||||||
|
for(std::size_t J=I+1;J<DegreeOne.size();++J)
|
||||||
|
{
|
||||||
|
const std::size_t A=DegreeOne[I],B=DegreeOne[J];
|
||||||
|
EndpointPairs.push_back({std::hypot(PlanePoints[A][0]-PlanePoints[B][0],
|
||||||
|
PlanePoints[A][1]-PlanePoints[B][1]),A,B});
|
||||||
|
}
|
||||||
|
std::sort(EndpointPairs.begin(),EndpointPairs.end(),[](const auto& A,const auto& B)
|
||||||
|
{return A.Distance<B.Distance;});
|
||||||
|
if(!EndpointPairs.empty())Result.NearestOpenGapMetres=EndpointPairs.front().Distance;
|
||||||
|
std::set<std::size_t> PairedEndpoints;
|
||||||
|
for(const FEndpointPair& Pair:EndpointPairs)
|
||||||
|
{
|
||||||
|
if(Pair.Distance>1.0)break;
|
||||||
|
if(PairedEndpoints.contains(Pair.A)||PairedEndpoints.contains(Pair.B))continue;
|
||||||
|
const auto ClosingEdge=std::minmax(Pair.A,Pair.B);
|
||||||
|
if(!Segments.emplace(ClosingEdge.first,ClosingEdge.second).second)continue;
|
||||||
|
Adjacency[Pair.A].push_back(Pair.B);Adjacency[Pair.B].push_back(Pair.A);
|
||||||
|
PairedEndpoints.insert(Pair.A);PairedEndpoints.insert(Pair.B);
|
||||||
|
Result.OpenGapMetres=std::max(Result.OpenGapMetres,Pair.Distance);
|
||||||
|
++Result.ClosedOpenContours;
|
||||||
|
}
|
||||||
|
std::vector<std::map<std::size_t,std::size_t>> Continuation(Adjacency.size());
|
||||||
|
for(std::size_t Node=0;Node<Adjacency.size();++Node)
|
||||||
|
{
|
||||||
|
const auto& Neighbours=Adjacency[Node];
|
||||||
|
if(Neighbours.size()==2)
|
||||||
|
{
|
||||||
|
Continuation[Node][Neighbours[0]]=Neighbours[1];
|
||||||
|
Continuation[Node][Neighbours[1]]=Neighbours[0];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(Neighbours.size()<3)continue;
|
||||||
|
struct FTurnPair {double Dot=1.0;std::size_t A=0,B=0;};
|
||||||
|
std::vector<FTurnPair> Pairs;
|
||||||
|
for(std::size_t I=0;I<Neighbours.size();++I)
|
||||||
|
for(std::size_t J=I+1;J<Neighbours.size();++J)
|
||||||
|
{
|
||||||
|
const auto& A=PlanePoints[Neighbours[I]];
|
||||||
|
const auto& B=PlanePoints[Neighbours[J]];
|
||||||
|
const double AX=A[0]-PlanePoints[Node][0];
|
||||||
|
const double AY=A[1]-PlanePoints[Node][1];
|
||||||
|
const double BX=B[0]-PlanePoints[Node][0];
|
||||||
|
const double BY=B[1]-PlanePoints[Node][1];
|
||||||
|
const double LengthA=std::hypot(AX,AY),LengthB=std::hypot(BX,BY);
|
||||||
|
if(LengthA<=1.0e-9||LengthB<=1.0e-9)continue;
|
||||||
|
Pairs.push_back({(AX*BX+AY*BY)/(LengthA*LengthB),
|
||||||
|
Neighbours[I],Neighbours[J]});
|
||||||
|
}
|
||||||
|
std::sort(Pairs.begin(),Pairs.end(),[](const auto& A,const auto& B)
|
||||||
|
{return A.Dot<B.Dot;});
|
||||||
|
std::set<std::size_t> Paired;
|
||||||
|
for(const FTurnPair& Pair:Pairs)
|
||||||
|
{
|
||||||
|
if(Pair.Dot>0.25||Paired.contains(Pair.A)||Paired.contains(Pair.B))continue;
|
||||||
|
Continuation[Node][Pair.A]=Pair.B;Continuation[Node][Pair.B]=Pair.A;
|
||||||
|
Paired.insert(Pair.A);Paired.insert(Pair.B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::set<std::pair<std::size_t,std::size_t>> UsedSegments;
|
||||||
|
std::vector<std::vector<std::size_t>> Loops;
|
||||||
|
for(const auto& Segment:Segments)
|
||||||
|
{
|
||||||
|
if(UsedSegments.contains(Segment))continue;
|
||||||
|
std::vector<std::size_t> Loop{Segment.first};
|
||||||
|
std::size_t Previous=Segment.first,Current=Segment.second;
|
||||||
|
UsedSegments.insert(Segment);bool Closed=false,Branched=false;
|
||||||
|
for(std::size_t Guard=0;Guard<=Segments.size();++Guard)
|
||||||
|
{
|
||||||
|
if(Current==Loop.front()){Closed=true;break;}
|
||||||
|
Loop.push_back(Current);
|
||||||
|
const auto ContinuationIt=Continuation[Current].find(Previous);
|
||||||
|
if(ContinuationIt==Continuation[Current].end())
|
||||||
|
{Branched=true;break;}
|
||||||
|
const std::size_t Next=ContinuationIt->second;
|
||||||
|
const auto NextEdge=std::minmax(Current,Next);
|
||||||
|
if(UsedSegments.contains(NextEdge))
|
||||||
|
{Branched=true;break;}
|
||||||
|
UsedSegments.insert(NextEdge);Previous=Current;Current=Next;
|
||||||
|
}
|
||||||
|
if(Closed&&Loop.size()>=3)Loops.push_back(std::move(Loop));
|
||||||
|
else if(Branched||!Closed)++Result.OpenContours;
|
||||||
|
}
|
||||||
|
Result.Loops=Loops.size();
|
||||||
|
if(Loops.empty())return Result;
|
||||||
|
|
||||||
|
struct FPlaneEdge {std::array<double,2> A{},B{};};
|
||||||
|
std::vector<FPlaneEdge> Edges;
|
||||||
|
std::vector<double> Ys;
|
||||||
|
for(const auto& Loop:Loops)
|
||||||
|
for(std::size_t I=0;I<Loop.size();++I)
|
||||||
|
{
|
||||||
|
const auto& A=PlanePoints[Loop[I]];
|
||||||
|
const auto& B=PlanePoints[Loop[(I+1)%Loop.size()]];
|
||||||
|
if(std::hypot(A[0]-B[0],A[1]-B[1])<1.0e-6)continue;
|
||||||
|
Edges.push_back({A,B});Ys.push_back(A[1]);Ys.push_back(B[1]);
|
||||||
|
}
|
||||||
|
std::sort(Ys.begin(),Ys.end());
|
||||||
|
Ys.erase(std::unique(Ys.begin(),Ys.end(),[](double A,double B)
|
||||||
|
{return std::abs(A-B)<1.0e-6;}),Ys.end());
|
||||||
|
const auto AppendTriangle=[&](const std::array<double,2>& A,
|
||||||
|
const std::array<double,2>& B,const std::array<double,2>& C)
|
||||||
|
{
|
||||||
|
const std::array<std::array<double,2>,3> PlaneVertices={A,B,C};
|
||||||
|
const std::uint32_t Base=static_cast<std::uint32_t>(Scene.Vertices.size());
|
||||||
|
for(const auto& PlaneVertex:PlaneVertices)
|
||||||
|
{
|
||||||
|
FVertex Vertex{};
|
||||||
|
for(std::size_t Axis=0;Axis<3;++Axis)
|
||||||
|
{
|
||||||
|
const double World=Origin[Axis]+U[Axis]*PlaneVertex[0]
|
||||||
|
+V[Axis]*PlaneVertex[1]+N[Axis]*0.001;
|
||||||
|
Vertex.Position[Axis]=static_cast<float>(World);
|
||||||
|
Vertex.WorldPosition[Axis]=static_cast<float>(World);
|
||||||
|
Vertex.Normal[Axis]=static_cast<float>(-N[Axis]);
|
||||||
|
}
|
||||||
|
Vertex.Height=Vertex.Position[2];
|
||||||
|
Scene.Vertices.push_back(Vertex);
|
||||||
|
}
|
||||||
|
Scene.Indices.insert(Scene.Indices.end(),{Base,Base+1,Base+2});
|
||||||
|
++Result.Triangles;
|
||||||
|
};
|
||||||
|
for(std::size_t Slab=0;Slab+1<Ys.size();++Slab)
|
||||||
|
{
|
||||||
|
const double Y0=Ys[Slab],Y1=Ys[Slab+1];
|
||||||
|
if(Y1-Y0<1.0e-7)continue;
|
||||||
|
const double Mid=(Y0+Y1)*0.5;
|
||||||
|
struct FCrossing {double X=0.0;const FPlaneEdge* Edge=nullptr;};
|
||||||
|
std::vector<FCrossing> Crossings;
|
||||||
|
for(const FPlaneEdge& Edge:Edges)
|
||||||
|
{
|
||||||
|
const double Low=std::min(Edge.A[1],Edge.B[1]);
|
||||||
|
const double High=std::max(Edge.A[1],Edge.B[1]);
|
||||||
|
if(Mid<=Low||Mid>=High)continue;
|
||||||
|
const double T=(Mid-Edge.A[1])/(Edge.B[1]-Edge.A[1]);
|
||||||
|
Crossings.push_back({std::lerp(Edge.A[0],Edge.B[0],T),&Edge});
|
||||||
|
}
|
||||||
|
std::sort(Crossings.begin(),Crossings.end(),[](const auto& A,const auto& B)
|
||||||
|
{return A.X<B.X;});
|
||||||
|
const auto XAt=[](const FPlaneEdge& Edge,double Y)
|
||||||
|
{return std::lerp(Edge.A[0],Edge.B[0],(Y-Edge.A[1])/(Edge.B[1]-Edge.A[1]));};
|
||||||
|
for(std::size_t I=0;I+1<Crossings.size();I+=2)
|
||||||
|
{
|
||||||
|
const FPlaneEdge& Left=*Crossings[I].Edge;
|
||||||
|
const FPlaneEdge& Right=*Crossings[I+1].Edge;
|
||||||
|
const std::array<double,2> A={XAt(Left,Y0),Y0};
|
||||||
|
const std::array<double,2> B={XAt(Right,Y0),Y0};
|
||||||
|
const std::array<double,2> C={XAt(Right,Y1),Y1};
|
||||||
|
const std::array<double,2> E={XAt(Left,Y1),Y1};
|
||||||
|
if(std::abs(A[0]-B[0])+std::abs(E[0]-C[0])<1.0e-7)continue;
|
||||||
|
AppendTriangle(A,B,C);AppendTriangle(A,C,E);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scene.DrawnTriangles=Scene.Indices.size()/3;
|
||||||
|
return Result;
|
||||||
|
}
|
||||||
|
|
||||||
EGLDisplay MakeDisplay()
|
EGLDisplay MakeDisplay()
|
||||||
{
|
{
|
||||||
using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*);
|
using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*);
|
||||||
@@ -537,7 +812,7 @@ int main(int argc,char** argv)
|
|||||||
{
|
{
|
||||||
if(argc<3)
|
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-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]);
|
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]);
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0;
|
int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0;
|
||||||
@@ -545,8 +820,10 @@ int main(int argc,char** argv)
|
|||||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||||
bool HasCamera=false;std::array<double,3> Camera{};
|
bool HasCamera=false;std::array<double,3> Camera{};
|
||||||
bool HasLightDirection=false;std::array<double,3> LightDirection{};
|
bool HasLightDirection=false;std::array<double,3> LightDirection{};
|
||||||
bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15;
|
bool ReadableMode=true,HeightTint=true,TileEdges=false,TileSkirts=true,Headlamp=false;
|
||||||
|
double TileEdgeWidthMeters=0.15;
|
||||||
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
||||||
|
bool FocusRegionEnabled=false;std::array<double,3> FocusCenter{};double FocusRadius=0.0;
|
||||||
for(int I=3;I<argc;++I)
|
for(int I=3;I<argc;++I)
|
||||||
{
|
{
|
||||||
const std::string_view Arg(argv[I]);
|
const std::string_view Arg(argv[I]);
|
||||||
@@ -586,6 +863,17 @@ int main(int argc,char** argv)
|
|||||||
{std::fprintf(stderr,"Invalid camera position\n");return 2;}
|
{std::fprintf(stderr,"Invalid camera position\n");return 2;}
|
||||||
HasCamera=true;
|
HasCamera=true;
|
||||||
}
|
}
|
||||||
|
else if(Arg.starts_with("--focus-region="))
|
||||||
|
{
|
||||||
|
const std::string Value(Arg.substr(15));
|
||||||
|
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf,%lf",&FocusCenter[0],&FocusCenter[1],
|
||||||
|
&FocusCenter[2],&FocusRadius)!=4
|
||||||
|
||!std::isfinite(FocusCenter[0])||!std::isfinite(FocusCenter[1])
|
||||||
|
||!std::isfinite(FocusCenter[2])||!std::isfinite(FocusRadius)
|
||||||
|
||FocusRadius<=0.0)
|
||||||
|
{std::fprintf(stderr,"Invalid focus region\n");return 2;}
|
||||||
|
FocusRegionEnabled=true;
|
||||||
|
}
|
||||||
else if(Arg.starts_with("--yaw-deg=")||Arg.starts_with("--pitch-deg="))
|
else if(Arg.starts_with("--yaw-deg=")||Arg.starts_with("--pitch-deg="))
|
||||||
{
|
{
|
||||||
const bool IsYaw=Arg.starts_with("--yaw-deg=");
|
const bool IsYaw=Arg.starts_with("--yaw-deg=");
|
||||||
@@ -635,6 +923,13 @@ int main(int argc,char** argv)
|
|||||||
else if(Mode=="off")TileEdges=false;
|
else if(Mode=="off")TileEdges=false;
|
||||||
else {std::fprintf(stderr,"Invalid tile-edge mode\n");return 2;}
|
else {std::fprintf(stderr,"Invalid tile-edge mode\n");return 2;}
|
||||||
}
|
}
|
||||||
|
else if(Arg.starts_with("--tile-skirts="))
|
||||||
|
{
|
||||||
|
const std::string_view Mode=Arg.substr(14);
|
||||||
|
if(Mode=="on")TileSkirts=true;
|
||||||
|
else if(Mode=="off")TileSkirts=false;
|
||||||
|
else {std::fprintf(stderr,"Invalid tile-skirt mode\n");return 2;}
|
||||||
|
}
|
||||||
else if(Arg.starts_with("--tile-edge-width-m="))
|
else if(Arg.starts_with("--tile-edge-width-m="))
|
||||||
{
|
{
|
||||||
char* End=nullptr;const std::string Value(Arg.substr(20));
|
char* End=nullptr;const std::string Value(Arg.substr(20));
|
||||||
@@ -663,6 +958,74 @@ int main(int argc,char** argv)
|
|||||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||||
if(!HasHeightCenter)HeightCenterMeters=Scene.CameraMeters[2];
|
if(!HasHeightCenter)HeightCenterMeters=Scene.CameraMeters[2];
|
||||||
|
const std::size_t CapIndexBegin=Scene.Indices.size();
|
||||||
|
FClipCapResult ClipCap;
|
||||||
|
if(ClipPlaneEnabled)
|
||||||
|
{
|
||||||
|
ClipCap=AddClipPlaneCaps(Scene,ClipPlane);
|
||||||
|
const bool Capped=ClipCap.Loops>0&&ClipCap.Triangles>0;
|
||||||
|
std::printf("[OffscreenClipCap] segments=%llu loops=%llu open_contours=%llu closed_open_contours=%llu degree_one=%llu branch_nodes=%llu open_gap_m=%.3f nearest_open_gap_m=%.3f cap_triangles=%llu status=%s\n",
|
||||||
|
static_cast<unsigned long long>(ClipCap.Intersections),
|
||||||
|
static_cast<unsigned long long>(ClipCap.Loops),
|
||||||
|
static_cast<unsigned long long>(ClipCap.OpenContours),
|
||||||
|
static_cast<unsigned long long>(ClipCap.ClosedOpenContours),
|
||||||
|
static_cast<unsigned long long>(ClipCap.DegreeOneNodes),
|
||||||
|
static_cast<unsigned long long>(ClipCap.BranchNodes),
|
||||||
|
ClipCap.OpenGapMetres,ClipCap.NearestOpenGapMetres,
|
||||||
|
static_cast<unsigned long long>(ClipCap.Triangles),Capped?"PASS":"FAIL");
|
||||||
|
if(!Capped)return 3;
|
||||||
|
}
|
||||||
|
if(!TileSkirts)
|
||||||
|
{
|
||||||
|
const std::size_t SurfaceTriangleCount=Scene.SurfaceIndices.size()/3;
|
||||||
|
std::vector<std::uint32_t> SurfaceAndCaps=std::move(Scene.SurfaceIndices);
|
||||||
|
// Caps were appended after the original mesh indices. Keep them while dropping
|
||||||
|
// the LOD seam skirts, which are useful for streamed play but obscure close captures.
|
||||||
|
if(ClipPlaneEnabled&&Scene.Indices.size()>CapIndexBegin)
|
||||||
|
SurfaceAndCaps.insert(SurfaceAndCaps.end(),Scene.Indices.begin()+CapIndexBegin,
|
||||||
|
Scene.Indices.end());
|
||||||
|
Scene.Indices=std::move(SurfaceAndCaps);
|
||||||
|
Scene.DrawnTriangles=Scene.Indices.size()/3;
|
||||||
|
std::printf("[OffscreenMeshDisplay] tile_skirts=off kept_surface_triangles=%llu "
|
||||||
|
"cap_triangles=%llu status=PASS\n",
|
||||||
|
static_cast<unsigned long long>(SurfaceTriangleCount),
|
||||||
|
static_cast<unsigned long long>(Scene.Indices.size()/3-
|
||||||
|
SurfaceTriangleCount));
|
||||||
|
}
|
||||||
|
if(FocusRegionEnabled)
|
||||||
|
{
|
||||||
|
std::vector<std::uint32_t> FocusedIndices;
|
||||||
|
FocusedIndices.reserve(Scene.Indices.size());
|
||||||
|
const double RadiusSquared=FocusRadius*FocusRadius;
|
||||||
|
for(std::size_t Triangle=0;Triangle+2<Scene.Indices.size();Triangle+=3)
|
||||||
|
{
|
||||||
|
std::array<double,3> Center{};
|
||||||
|
for(std::size_t Corner=0;Corner<3;++Corner)
|
||||||
|
{
|
||||||
|
const auto& Vertex=Scene.Vertices[Scene.Indices[Triangle+Corner]];
|
||||||
|
for(std::size_t Axis=0;Axis<3;++Axis)
|
||||||
|
Center[Axis]+=Vertex.WorldPosition[Axis]/3.0;
|
||||||
|
}
|
||||||
|
const double DX=Center[0]-FocusCenter[0],DY=Center[1]-FocusCenter[1],
|
||||||
|
DZ=Center[2]-FocusCenter[2];
|
||||||
|
if(DX*DX+DY*DY+DZ*DZ>RadiusSquared)continue;
|
||||||
|
FocusedIndices.insert(FocusedIndices.end(),Scene.Indices.begin()+Triangle,
|
||||||
|
Scene.Indices.begin()+Triangle+3);
|
||||||
|
}
|
||||||
|
if(FocusedIndices.empty())
|
||||||
|
std::printf("[OffscreenFocusRegion] center_m=(%.3f,%.3f,%.3f) radius_m=%.3f "
|
||||||
|
"triangles=0 status=EMPTY_FALLBACK full_scene_triangles=%llu\n",
|
||||||
|
FocusCenter[0],FocusCenter[1],FocusCenter[2],FocusRadius,
|
||||||
|
static_cast<unsigned long long>(Scene.DrawnTriangles));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Scene.Indices=std::move(FocusedIndices);
|
||||||
|
Scene.DrawnTriangles=Scene.Indices.size()/3;
|
||||||
|
std::printf("[OffscreenFocusRegion] center_m=(%.3f,%.3f,%.3f) radius_m=%.3f "
|
||||||
|
"triangles=%llu status=PASS\n",FocusCenter[0],FocusCenter[1],FocusCenter[2],
|
||||||
|
FocusRadius,static_cast<unsigned long long>(Scene.DrawnTriangles));
|
||||||
|
}
|
||||||
|
}
|
||||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||||
|
|||||||
@@ -805,8 +805,241 @@ namespace StandaloneStreamBenchmark
|
|||||||
std::uint64_t wallSamples=0,missingWallSamples=0;
|
std::uint64_t wallSamples=0,missingWallSamples=0;
|
||||||
bool passed=false;
|
bool passed=false;
|
||||||
};
|
};
|
||||||
|
struct FPassageMouthProfileAudit
|
||||||
|
{
|
||||||
|
double floorFlatStepRunMetres=0.0,floorStepMetres=0.0;
|
||||||
|
double wallFlatStepRunMetres=0.0,wallStepMetres=0.0;
|
||||||
|
std::uint64_t floorSamples=0,wallSamples=0,missingFloorSamples=0,missingWallSamples=0;
|
||||||
|
bool passed=false;
|
||||||
|
};
|
||||||
constexpr double MaximumFlatRunMetres=8.0;
|
constexpr double MaximumFlatRunMetres=8.0;
|
||||||
constexpr double MinimumWallReliefMetres=0.20;
|
constexpr double MinimumWallReliefMetres=0.20;
|
||||||
|
constexpr double PassageProfileSpacingMetres=0.125;
|
||||||
|
constexpr double PassageProfilePlateauMinimumMetres=0.50;
|
||||||
|
constexpr double PassageProfilePlateauEpsilonMetres=0.020;
|
||||||
|
constexpr double PassageProfileStepThresholdMetres=0.100;
|
||||||
|
constexpr double PassageMouthProfileReachMetres=12.0;
|
||||||
|
const auto auditProfilePlateaus=[&](const std::vector<std::array<double,2>>& profile)
|
||||||
|
{
|
||||||
|
std::pair<double,double> result{};
|
||||||
|
if(profile.size()<2)return result;
|
||||||
|
std::size_t runStart=0;
|
||||||
|
for(std::size_t i=1;i<profile.size();++i)
|
||||||
|
{
|
||||||
|
if(profile[i][0]-profile[i-1][0]
|
||||||
|
>PassageProfileSpacingMetres*1.5)
|
||||||
|
{runStart=i;continue;}
|
||||||
|
if(std::abs(profile[i][1]-profile[runStart][1])
|
||||||
|
<=PassageProfilePlateauEpsilonMetres)continue;
|
||||||
|
const double flatLength=profile[i-1][0]-profile[runStart][0];
|
||||||
|
const double step=std::abs(profile[i][1]-profile[i-1][1]);
|
||||||
|
if(flatLength>=PassageProfilePlateauMinimumMetres
|
||||||
|
&&step>=PassageProfileStepThresholdMetres)
|
||||||
|
{
|
||||||
|
result.first=std::max(result.first,flatLength);
|
||||||
|
result.second=std::max(result.second,step);
|
||||||
|
}
|
||||||
|
runStart=i;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
const auto auditPassageMouthProfiles=[&](const VoxelForgeCore::World& Field,
|
||||||
|
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||||||
|
{
|
||||||
|
FPassageMouthProfileAudit audit;
|
||||||
|
if(passage.controlPoints.size()<2)return audit;
|
||||||
|
const std::size_t end=std::min(passage.lowerMouthControlIndex,
|
||||||
|
passage.controlPoints.size()-1);
|
||||||
|
if(end<1)return audit;
|
||||||
|
struct FProfilePose
|
||||||
|
{
|
||||||
|
double distance=0.0,x=0.0,y=0.0,z=0.0,radius=0.0;
|
||||||
|
};
|
||||||
|
std::vector<FProfilePose> path;
|
||||||
|
path.reserve(end*8+1);
|
||||||
|
double distance=0.0;
|
||||||
|
for(std::size_t segment=0;segment<end;++segment)
|
||||||
|
{
|
||||||
|
const auto& a=passage.controlPoints[segment];
|
||||||
|
const auto& b=passage.controlPoints[segment+1];
|
||||||
|
const double ax=qToMetres(a.positionSubvoxels[0]);
|
||||||
|
const double ay=qToMetres(a.positionSubvoxels[1]);
|
||||||
|
const double az=qToMetres(a.positionSubvoxels[2]);
|
||||||
|
const double bx=qToMetres(b.positionSubvoxels[0]);
|
||||||
|
const double by=qToMetres(b.positionSubvoxels[1]);
|
||||||
|
const double bz=qToMetres(b.positionSubvoxels[2]);
|
||||||
|
const double ar=qToMetres(a.radiusSubvoxels);
|
||||||
|
const double br=qToMetres(b.radiusSubvoxels);
|
||||||
|
const double length=std::sqrt((bx-ax)*(bx-ax)+(by-ay)*(by-ay)+(bz-az)*(bz-az));
|
||||||
|
if(!(length>1.0e-8))continue;
|
||||||
|
const std::size_t steps=std::max<std::size_t>(1,
|
||||||
|
static_cast<std::size_t>(std::ceil(length/PassageProfileSpacingMetres)));
|
||||||
|
for(std::size_t step=segment==0?0:1;step<=steps;++step)
|
||||||
|
{
|
||||||
|
const double t=static_cast<double>(step)/steps;
|
||||||
|
path.push_back({distance+length*t,std::lerp(ax,bx,t),
|
||||||
|
std::lerp(ay,by,t),std::lerp(az,bz,t),std::lerp(ar,br,t)});
|
||||||
|
}
|
||||||
|
distance+=length;
|
||||||
|
}
|
||||||
|
if(path.size()<3)return audit;
|
||||||
|
const double lowerMouthDistance=distance;
|
||||||
|
constexpr double DensitySearchStepMetres=0.0625;
|
||||||
|
const auto densityMetres=[&](double x,double y,double z)
|
||||||
|
{
|
||||||
|
const float value=Field.densityAt({x/coreMetres,y/coreMetres,z/coreMetres});
|
||||||
|
return std::isfinite(value)?static_cast<double>(value)*coreMetres
|
||||||
|
:std::numeric_limits<double>::quiet_NaN();
|
||||||
|
};
|
||||||
|
const auto firstFloorCrossing=[&](const FProfilePose& pose,double x,double y)->double
|
||||||
|
{
|
||||||
|
const double expectedFloor=pose.z-pose.radius;
|
||||||
|
const double maximumRise=std::min(2.0*pose.radius-0.25,
|
||||||
|
std::max(0.75,pose.radius-1.20+0.50));
|
||||||
|
double previousZ=expectedFloor-1.50;
|
||||||
|
double previous=densityMetres(x,y,previousZ);
|
||||||
|
if(!std::isfinite(previous))return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
double highestFloor=std::numeric_limits<double>::quiet_NaN();
|
||||||
|
for(double z=previousZ+DensitySearchStepMetres;
|
||||||
|
z<=expectedFloor+maximumRise+DensitySearchStepMetres*0.25;
|
||||||
|
z+=DensitySearchStepMetres)
|
||||||
|
{
|
||||||
|
const double current=densityMetres(x,y,z);
|
||||||
|
if(!std::isfinite(current))return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
if(previous<=0.0&¤t>=0.0)
|
||||||
|
{
|
||||||
|
const double denominator=current-previous;
|
||||||
|
const double t=std::abs(denominator)>1.0e-12
|
||||||
|
?std::clamp(-previous/denominator,0.0,1.0):0.0;
|
||||||
|
highestFloor=std::lerp(z-DensitySearchStepMetres,z,t);
|
||||||
|
}
|
||||||
|
previous=current;
|
||||||
|
}
|
||||||
|
return highestFloor;
|
||||||
|
};
|
||||||
|
const auto findWallRadius=[&](const FProfilePose& pose,double nx,double ny)
|
||||||
|
{
|
||||||
|
const double maximum=pose.radius+2.0;
|
||||||
|
double previousRadius=0.0;
|
||||||
|
double previous=densityMetres(pose.x,pose.y,pose.z);
|
||||||
|
if(!std::isfinite(previous)||previous<0.0)
|
||||||
|
return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
for(double radius=DensitySearchStepMetres;radius<=maximum;
|
||||||
|
radius+=DensitySearchStepMetres)
|
||||||
|
{
|
||||||
|
const double current=densityMetres(pose.x+nx*radius,
|
||||||
|
pose.y+ny*radius,pose.z);
|
||||||
|
if(!std::isfinite(current))return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
if(previous>=0.0&¤t<=0.0)
|
||||||
|
{
|
||||||
|
const double denominator=previous-current;
|
||||||
|
const double t=std::abs(denominator)>1.0e-12
|
||||||
|
?std::clamp(previous/denominator,0.0,1.0):0.0;
|
||||||
|
return std::lerp(previousRadius,radius,t);
|
||||||
|
}
|
||||||
|
previous=current;previousRadius=radius;
|
||||||
|
}
|
||||||
|
return std::numeric_limits<double>::quiet_NaN();
|
||||||
|
};
|
||||||
|
std::array<std::array<std::vector<std::array<double,2>>,3>,2> floorProfiles;
|
||||||
|
std::array<std::array<std::vector<std::array<double,2>>,2>,2> wallProfiles;
|
||||||
|
std::array<double,2> endpointFloorCounts{};
|
||||||
|
for(std::size_t i=0;i<path.size();++i)
|
||||||
|
{
|
||||||
|
const auto& pose=path[i];
|
||||||
|
const bool inUpperWindow=pose.distance<=PassageMouthProfileReachMetres;
|
||||||
|
const bool inLowerWindow=lowerMouthDistance-pose.distance
|
||||||
|
<=PassageMouthProfileReachMetres;
|
||||||
|
if(!inUpperWindow&&!inLowerWindow)continue;
|
||||||
|
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||||
|
{
|
||||||
|
if((endpoint==0&&!inUpperWindow)||(endpoint==1&&!inLowerWindow))continue;
|
||||||
|
const double localDistance=endpoint==0?pose.distance
|
||||||
|
:lowerMouthDistance-pose.distance;
|
||||||
|
const double tangentX=i==0?path[i+1].x-path[i].x
|
||||||
|
:i+1>=path.size()?path[i].x-path[i-1].x
|
||||||
|
:path[i+1].x-path[i-1].x;
|
||||||
|
const double tangentY=i==0?path[i+1].y-path[i].y
|
||||||
|
:i+1>=path.size()?path[i].y-path[i-1].y
|
||||||
|
:path[i+1].y-path[i-1].y;
|
||||||
|
const double tangentLength=std::hypot(tangentX,tangentY);
|
||||||
|
if(tangentLength<=1.0e-6)
|
||||||
|
{audit.missingFloorSamples+=3;audit.missingWallSamples+=2;continue;}
|
||||||
|
const double nx=-tangentY/tangentLength,ny=tangentX/tangentLength;
|
||||||
|
for(std::size_t lane=0;lane<3;++lane)
|
||||||
|
{
|
||||||
|
const double lateral=lane==0?0.0:lane==1?0.75:-0.75;
|
||||||
|
const double floor=firstFloorCrossing(pose,
|
||||||
|
pose.x+nx*lateral,pose.y+ny*lateral);
|
||||||
|
if(lane!=0&&localDistance>=4.0&&localDistance<=5.5)
|
||||||
|
std::printf("[PassageMouthProfileProbe] id=%s endpoint=%s d_m=%.3f lane_m=%.2f x_m=%.3f y_m=%.3f route_floor_m=%.3f field_floor_m=%.3f\n",
|
||||||
|
passage.stableId.c_str(),endpoint==0?"upper":"lower",
|
||||||
|
localDistance,lateral,pose.x+nx*lateral,
|
||||||
|
pose.y+ny*lateral,pose.z-pose.radius,floor);
|
||||||
|
if(std::isfinite(floor))
|
||||||
|
{
|
||||||
|
floorProfiles[endpoint][lane].push_back({localDistance,floor});
|
||||||
|
++endpointFloorCounts[endpoint];++audit.floorSamples;
|
||||||
|
}
|
||||||
|
else ++audit.missingFloorSamples;
|
||||||
|
}
|
||||||
|
if(i%4!=0)continue;
|
||||||
|
for(std::size_t side=0;side<2;++side)
|
||||||
|
{
|
||||||
|
const double sign=side==0?-1.0:1.0;
|
||||||
|
const double radius=findWallRadius(pose,sign*nx,sign*ny);
|
||||||
|
if(std::isfinite(radius))
|
||||||
|
{
|
||||||
|
wallProfiles[endpoint][side].push_back({localDistance,radius});
|
||||||
|
++audit.wallSamples;
|
||||||
|
}
|
||||||
|
else ++audit.missingWallSamples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double worstEndpointFloorCoverage=1.0;
|
||||||
|
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||||
|
{
|
||||||
|
for(std::size_t lane=0;lane<3;++lane)
|
||||||
|
std::sort(floorProfiles[endpoint][lane].begin(),
|
||||||
|
floorProfiles[endpoint][lane].end(),
|
||||||
|
[](const auto& a,const auto& b){return a[0]<b[0];});
|
||||||
|
for(std::size_t side=0;side<2;++side)
|
||||||
|
std::sort(wallProfiles[endpoint][side].begin(),
|
||||||
|
wallProfiles[endpoint][side].end(),
|
||||||
|
[](const auto& a,const auto& b){return a[0]<b[0];});
|
||||||
|
const double expectedFloor=std::min<double>(path.size(),
|
||||||
|
PassageMouthProfileReachMetres/PassageProfileSpacingMetres)+1.0;
|
||||||
|
worstEndpointFloorCoverage=std::min(worstEndpointFloorCoverage,
|
||||||
|
expectedFloor>0?endpointFloorCounts[endpoint]/expectedFloor:0.0);
|
||||||
|
// Lateral probes confirm floor coverage, but do not define a longitudinal
|
||||||
|
// floor profile: the normal rotates around tight bends and can jump across
|
||||||
|
// unrelated terrain. The centerline is the stable passage-floor trace.
|
||||||
|
const auto floorStep=auditProfilePlateaus(floorProfiles[endpoint][0]);
|
||||||
|
audit.floorFlatStepRunMetres=std::max(audit.floorFlatStepRunMetres,
|
||||||
|
floorStep.first);
|
||||||
|
audit.floorStepMetres=std::max(audit.floorStepMetres,floorStep.second);
|
||||||
|
for(std::size_t side=0;side<2;++side)
|
||||||
|
{
|
||||||
|
const auto wallStep=auditProfilePlateaus(wallProfiles[endpoint][side]);
|
||||||
|
audit.wallFlatStepRunMetres=std::max(audit.wallFlatStepRunMetres,wallStep.first);
|
||||||
|
audit.wallStepMetres=std::max(audit.wallStepMetres,wallStep.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audit.passed=worstEndpointFloorCoverage>=0.75
|
||||||
|
&&audit.wallSamples>=8
|
||||||
|
&&audit.floorFlatStepRunMetres==0.0&&audit.wallFlatStepRunMetres==0.0;
|
||||||
|
return audit;
|
||||||
|
};
|
||||||
|
std::vector<std::array<double,2>> l23TerracedControl;
|
||||||
|
for(std::size_t tread=0;tread<8;++tread)
|
||||||
|
for(std::size_t sample=0;sample<=16;++sample)
|
||||||
|
l23TerracedControl.push_back({(tread*16+sample)*PassageProfileSpacingMetres,
|
||||||
|
static_cast<double>(tread)*0.25});
|
||||||
|
const auto l23TerraceControlResult=auditProfilePlateaus(l23TerracedControl);
|
||||||
|
const bool l23TerraceControlDetected=l23TerraceControlResult.first
|
||||||
|
>=PassageProfilePlateauMinimumMetres
|
||||||
|
&&l23TerraceControlResult.second>=PassageProfileStepThresholdMetres;
|
||||||
const auto auditPassageNaturalness=[&](const VoxelForgeCore::World& Field,
|
const auto auditPassageNaturalness=[&](const VoxelForgeCore::World& Field,
|
||||||
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
const VoxelForgeCore::ResolvedLayoutPassageV1& passage)
|
||||||
{
|
{
|
||||||
@@ -1498,6 +1731,7 @@ namespace StandaloneStreamBenchmark
|
|||||||
return zUm>=stratum->content.bottomMicrometres&&zUm<stratum->content.topMicrometres;
|
return zUm>=stratum->content.bottomMicrometres&&zUm<stratum->content.topMicrometres;
|
||||||
};
|
};
|
||||||
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
std::uint64_t passageCount=0,passagePasses=0,blockedControls=0,naturalPasses=0;
|
||||||
|
std::uint64_t mouthProfilePasses=0;
|
||||||
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
std::uint64_t openAirViolations=0,openAirLegacyViolations=0,openAirSamples=0;
|
||||||
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
std::uint64_t openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=0;
|
||||||
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
std::uint64_t columnCrossings=0,shortGroundedSpans=0,columnConnectedEnds=0;
|
||||||
@@ -1545,6 +1779,8 @@ namespace StandaloneStreamBenchmark
|
|||||||
const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45,
|
const FWalkAudit passageAudit=auditPlayerPath(Shape,path,0.125,0.125,0.25,3,0.45,
|
||||||
&blockedWorld,&segmentSupport);
|
&blockedWorld,&segmentSupport);
|
||||||
const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage);
|
const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage);
|
||||||
|
const FPassageMouthProfileAudit mouthProfileAudit=
|
||||||
|
auditPassageMouthProfiles(Shape,passage);
|
||||||
const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld,
|
const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld,
|
||||||
passage,passageIndex);
|
passage,passageIndex);
|
||||||
const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld,
|
const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld,
|
||||||
@@ -1574,8 +1810,9 @@ namespace StandaloneStreamBenchmark
|
|||||||
const bool mouthsInContent=mouthInContent(passage.upperStrateIndex,passage.upperMouthSubvoxels)
|
const bool mouthsInContent=mouthInContent(passage.upperStrateIndex,passage.upperMouthSubvoxels)
|
||||||
&&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels);
|
&&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels);
|
||||||
++passageCount;naturalPasses+=naturalAudit.passed;
|
++passageCount;naturalPasses+=naturalAudit.passed;
|
||||||
|
mouthProfilePasses+=mouthProfileAudit.passed;
|
||||||
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
passagePasses+=passageAudit.passed&&mouthsInRadius&&mouthsInContent
|
||||||
&&naturalAudit.passed;
|
&&naturalAudit.passed&&mouthProfileAudit.passed;
|
||||||
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
std::printf("[ResolvedPassageAudit] id=%s interface=%d-%d control_points=%zu center_samples=%llu capsule_samples=%llu blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu mouth_content=%d/2 mouth_radius=%d/2 walkability=%s\n",
|
||||||
passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex,
|
passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex,
|
||||||
passage.controlPoints.size(),static_cast<unsigned long long>(passageAudit.centers),
|
passage.controlPoints.size(),static_cast<unsigned long long>(passageAudit.centers),
|
||||||
@@ -1591,6 +1828,15 @@ namespace StandaloneStreamBenchmark
|
|||||||
static_cast<unsigned long long>(naturalAudit.wallSamples),
|
static_cast<unsigned long long>(naturalAudit.wallSamples),
|
||||||
static_cast<unsigned long long>(naturalAudit.missingWallSamples),
|
static_cast<unsigned long long>(naturalAudit.missingWallSamples),
|
||||||
naturalAudit.passed?"PASS":"FAIL");
|
naturalAudit.passed?"PASS":"FAIL");
|
||||||
|
std::printf("[PassageMouthProfileAudit] id=%s floor_samples=%llu floor_missing=%llu wall_samples=%llu wall_missing=%llu floor_step_scan=centerline floor_flat_step_run_m=%.3f floor_step_m=%.3f wall_flat_step_run_m=%.3f wall_step_m=%.3f spacing_m=%.3f plateau=(%.3f,%.3f) status=%s\n",
|
||||||
|
passage.stableId.c_str(),static_cast<unsigned long long>(mouthProfileAudit.floorSamples),
|
||||||
|
static_cast<unsigned long long>(mouthProfileAudit.missingFloorSamples),
|
||||||
|
static_cast<unsigned long long>(mouthProfileAudit.wallSamples),
|
||||||
|
static_cast<unsigned long long>(mouthProfileAudit.missingWallSamples),
|
||||||
|
mouthProfileAudit.floorFlatStepRunMetres,mouthProfileAudit.floorStepMetres,
|
||||||
|
mouthProfileAudit.wallFlatStepRunMetres,mouthProfileAudit.wallStepMetres,
|
||||||
|
PassageProfileSpacingMetres,PassageProfilePlateauMinimumMetres,
|
||||||
|
PassageProfileStepThresholdMetres,mouthProfileAudit.passed?"PASS":"FAIL");
|
||||||
std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n",
|
std::printf("[PassageOpenAirAudit] id=%s support_samples=%llu violations=%llu column_samples=%llu short_span_samples=%llu l24_ungated_violations=%llu route_support_samples=%llu route_support_violations=%llu spacing_m=%.3f status=%s\n",
|
||||||
passage.stableId.c_str(),static_cast<unsigned long long>(openAirAudit.supportSamples),
|
passage.stableId.c_str(),static_cast<unsigned long long>(openAirAudit.supportSamples),
|
||||||
static_cast<unsigned long long>(openAirAudit.violations),
|
static_cast<unsigned long long>(openAirAudit.violations),
|
||||||
@@ -1891,12 +2137,23 @@ namespace StandaloneStreamBenchmark
|
|||||||
const bool l23SmoothWallDetected=!looksNatural(0.0,0.0);
|
const bool l23SmoothWallDetected=!looksNatural(0.0,0.0);
|
||||||
std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",
|
std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",
|
||||||
MinimumWallReliefMetres,l23SmoothWallDetected?"true":"false");
|
MinimumWallReliefMetres,l23SmoothWallDetected?"true":"false");
|
||||||
|
std::printf("[PassageMouthProfileNegativeControl] source=L23_terraced_geometry flat_run_m=%.3f following_step_m=%.3f thresholds=(%.3f,%.3f) detected=%s\n",
|
||||||
|
l23TerraceControlResult.first,l23TerraceControlResult.second,
|
||||||
|
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||||||
|
l23TerraceControlDetected?"true":"false");
|
||||||
std::printf("[PassageNaturalnessSummary] passed=%llu/%llu l23_negative_control=%s flat_run_limit_m=%.3f wall_relief_min_m=%.3f status=%s\n",
|
std::printf("[PassageNaturalnessSummary] passed=%llu/%llu l23_negative_control=%s flat_run_limit_m=%.3f wall_relief_min_m=%.3f status=%s\n",
|
||||||
static_cast<unsigned long long>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
static_cast<unsigned long long>(naturalPasses),static_cast<unsigned long long>(passageCount),
|
||||||
l23SmoothWallDetected?"DETECTED":"MISSED",MaximumFlatRunMetres,
|
l23SmoothWallDetected?"DETECTED":"MISSED",MaximumFlatRunMetres,
|
||||||
MinimumWallReliefMetres,naturalPasses==passageCount&&l23SmoothWallDetected?"PASS":"FAIL");
|
MinimumWallReliefMetres,naturalPasses==passageCount&&l23SmoothWallDetected?"PASS":"FAIL");
|
||||||
|
std::printf("[PassageMouthProfileSummary] passed=%llu/%llu spacing_m=%.3f plateau_min_m=%.3f following_step_min_m=%.3f l23_terrace_negative_control=%s status=%s\n",
|
||||||
|
static_cast<unsigned long long>(mouthProfilePasses),
|
||||||
|
static_cast<unsigned long long>(passageCount),PassageProfileSpacingMetres,
|
||||||
|
PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres,
|
||||||
|
l23TerraceControlDetected?"DETECTED":"MISSED",
|
||||||
|
mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL");
|
||||||
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
const bool auditPassed=passageCount>0&&passagePasses==passageCount
|
||||||
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
&&naturalPasses==passageCount&&l23SmoothWallDetected
|
||||||
|
&&mouthProfilePasses==passageCount&&l23TerraceControlDetected
|
||||||
&&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected
|
&&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected
|
||||||
&&columnAuditsPassed&&noLongBridgeAuditPassed
|
&&columnAuditsPassed&&noLongBridgeAuditPassed
|
||||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||||
|
|||||||