diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/AuditPassageMouthMeshProfilesL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/AuditPassageMouthMeshProfilesL34.py new file mode 100644 index 0000000..71edc3c --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/AuditPassageMouthMeshProfilesL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/CapturePassageStandardSetL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/CapturePassageStandardSetL34.py new file mode 100644 index 0000000..44d8c55 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/CapturePassageStandardSetL34.py @@ -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") diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux34/REPORT.md new file mode 100644 index 0000000..3143074 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/REPORT.md @@ -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. diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunCostL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunCostL34.py new file mode 100644 index 0000000..0383999 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunCostL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunDeterminismL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunDeterminismL34.py new file mode 100644 index 0000000..8aa6e82 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunDeterminismL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunInvariantAuditsL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunInvariantAuditsL34.py new file mode 100644 index 0000000..b241660 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunInvariantAuditsL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunLegacyAuditsL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunLegacyAuditsL34.py new file mode 100644 index 0000000..0d98bde --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunLegacyAuditsL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunPassageAuditsL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunPassageAuditsL34.py new file mode 100644 index 0000000..327ad1d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunPassageAuditsL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/RunUpperFixtureDeterminismL34.py b/Standalone/Research/WorldAssemblerM3/Linux34/RunUpperFixtureDeterminismL34.py new file mode 100644 index 0000000..870b7e4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/RunUpperFixtureDeterminismL34.py @@ -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) diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/SHA256SUMS.txt b/Standalone/Research/WorldAssemblerM3/Linux34/captures/SHA256SUMS.txt new file mode 100644 index 0000000..d6f73f0 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux34/captures/SHA256SUMS.txt @@ -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 diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-arrival.jpg new file mode 100644 index 0000000..3f95af7 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-ground-mouth.jpg new file mode 100644 index 0000000..6a6b163 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-mid-tunnel.jpg new file mode 100644 index 0000000..94fa1fb Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-rock-entry.jpg new file mode 100644 index 0000000..8f069da Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-fi-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-arrival.jpg new file mode 100644 index 0000000..07c625d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-ground-mouth.jpg new file mode 100644 index 0000000..05a868e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-mid-tunnel.jpg new file mode 100644 index 0000000..978ca5c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-rock-entry.jpg new file mode 100644 index 0000000..58fdace Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/owner-sw-rock-entry.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-arrival.jpg new file mode 100644 index 0000000..6e06b15 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-ground-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-ground-mouth.jpg new file mode 100644 index 0000000..005d891 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-ground-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-mid-tunnel.jpg new file mode 100644 index 0000000..999175b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-rock-entry.jpg b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-rock-entry.jpg new file mode 100644 index 0000000..b1b88d3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux34/captures/upper-sw-fixture-rock-entry.jpg differ diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index df470f3..b767312 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -14,8 +14,11 @@ #include #include #include +#include +#include #include #include +#include #include #include @@ -39,6 +42,7 @@ namespace float MaximumHeight=-std::numeric_limits::infinity(); std::vector Vertices; std::vector Indices; + std::vector SurfaceIndices; struct FTileRange { std::array Key{}; @@ -198,6 +202,8 @@ namespace for(const auto& Tile:Scene.Tiles)Desired.insert(Tile.Key); std::vector VisibleIndices; VisibleIndices.reserve(Scene.Indices.size()); + Scene.SurfaceIndices.reserve(Scene.Indices.size()- + static_cast(Scene.SkirtTriangles)*3); for(const auto& Tile:Scene.Tiles) { const std::uint8_t VisibleMask=ComputeVisibleSkirtFaceMask(Tile.Key,Desired); @@ -212,6 +218,9 @@ namespace const std::size_t Index=Tile.IndexBegin+static_cast(Triangle)*3; VisibleIndices.insert(VisibleIndices.end(),Scene.Indices.begin()+Index, 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); @@ -219,6 +228,272 @@ namespace 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& InputPlane) + { + FClipCapResult Result; + const double NormalLength=std::hypot(InputPlane[0],InputPlane[1],InputPlane[2]); + if(!(NormalLength>0.0))return Result; + const std::array N={InputPlane[0]/NormalLength, + InputPlane[1]/NormalLength,InputPlane[2]/NormalLength}; + const double D=InputPlane[3]/NormalLength; + const std::array Reference=std::abs(N[2])<0.85 + ?std::array{0.0,0.0,1.0}:std::array{0.0,1.0,0.0}; + const auto Cross=[](const std::array& A,const std::array& B) + { + return std::array{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& A) + { + const double Length=std::hypot(A[0],A[1],A[2]); + return Length>0.0?std::array{A[0]/Length,A[1]/Length,A[2]/Length} + :std::array{1.0,0.0,0.0}; + }; + const auto Dot=[](const std::array& A,const std::array& B) + {return A[0]*B[0]+A[1]*B[1]+A[2]*B[2];}; + const std::array U=Normalize(Cross(N,Reference)); + const std::array V=Normalize(Cross(N,U)); + const std::array Origin={-D*N[0],-D*N[1],-D*N[2]}; + using FPointKey=std::pair; + std::map NodeForPoint; + std::vector> PlanePoints; + std::vector> WorldPoints; + std::set> Segments; + const auto AddPoint=[&](const std::array& Point) + { + const std::array Relative={Point[0]-Origin[0], + Point[1]-Origin[1],Point[2]-Origin[2]}; + const std::array PlanePoint={Dot(Relative,U),Dot(Relative,V)}; + constexpr double Quantization=1000.0; + const FPointKey Key{static_cast(std::llround(PlanePoint[0]*Quantization)), + static_cast(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,3> Points{}; + std::array 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,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;I1.0e-9)||(DA>1.0e-9&&DB< -1.0e-9)) + { + const double T=DA/(DA-DB); + const std::array 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> Adjacency(PlanePoints.size()); + for(const auto& [A,B]:Segments) + {Adjacency[A].push_back(B);Adjacency[B].push_back(A);} + std::vector DegreeOne; + for(std::size_t Node=0;Node2)++Result.BranchNodes; + } + struct FEndpointPair {double Distance=0.0;std::size_t A=0,B=0;}; + std::vector EndpointPairs; + for(std::size_t I=0;I 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> Continuation(Adjacency.size()); + for(std::size_t Node=0;Node Pairs; + for(std::size_t I=0;I 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> UsedSegments; + std::vector> Loops; + for(const auto& Segment:Segments) + { + if(UsedSegments.contains(Segment))continue; + std::vector 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 A{},B{};}; + std::vector Edges; + std::vector Ys; + for(const auto& Loop:Loops) + for(std::size_t I=0;I& A, + const std::array& B,const std::array& C) + { + const std::array,3> PlaneVertices={A,B,C}; + const std::uint32_t Base=static_cast(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(World); + Vertex.WorldPosition[Axis]=static_cast(World); + Vertex.Normal[Axis]=static_cast(-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 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 A={XAt(Left,Y0),Y0}; + const std::array B={XAt(Right,Y0),Y0}; + const std::array C={XAt(Right,Y1),Y1}; + const std::array 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() { using TGetPlatformDisplay=EGLDisplay(*)(EGLenum,void*,const EGLint*); @@ -537,7 +812,7 @@ int main(int argc,char** argv) { 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; } 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 HasCamera=false;std::array Camera{}; bool HasLightDirection=false;std::array 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 ClipPlane{}; + bool FocusRegionEnabled=false;std::array FocusCenter{};double FocusRadius=0.0; for(int I=3;I(ClipCap.Intersections), + static_cast(ClipCap.Loops), + static_cast(ClipCap.OpenContours), + static_cast(ClipCap.ClosedOpenContours), + static_cast(ClipCap.DegreeOneNodes), + static_cast(ClipCap.BranchNodes), + ClipCap.OpenGapMetres,ClipCap.NearestOpenGapMetres, + static_cast(ClipCap.Triangles),Capped?"PASS":"FAIL"); + if(!Capped)return 3; + } + if(!TileSkirts) + { + const std::size_t SurfaceTriangleCount=Scene.SurfaceIndices.size()/3; + std::vector 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(SurfaceTriangleCount), + static_cast(Scene.Indices.size()/3- + SurfaceTriangleCount)); + } + if(FocusRegionEnabled) + { + std::vector FocusedIndices; + FocusedIndices.reserve(Scene.Indices.size()); + const double RadiusSquared=FocusRadius*FocusRadius; + for(std::size_t Triangle=0;Triangle+2 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(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(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", 1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2], Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight, diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 74a1825..8afff40 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -805,8 +805,241 @@ namespace StandaloneStreamBenchmark std::uint64_t wallSamples=0,missingWallSamples=0; 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 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>& profile) + { + std::pair result{}; + if(profile.size()<2)return result; + std::size_t runStart=0; + for(std::size_t i=1;iPassageProfileSpacingMetres*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 path; + path.reserve(end*8+1); + double distance=0.0; + for(std::size_t segment=0;segment1.0e-8))continue; + const std::size_t steps=std::max(1, + static_cast(std::ceil(length/PassageProfileSpacingMetres))); + for(std::size_t step=segment==0?0:1;step<=steps;++step) + { + const double t=static_cast(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(value)*coreMetres + :std::numeric_limits::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::quiet_NaN(); + double highestFloor=std::numeric_limits::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::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::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::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::quiet_NaN(); + }; + std::array>,3>,2> floorProfiles; + std::array>,2>,2> wallProfiles; + std::array endpointFloorCounts{}; + for(std::size_t i=0;i=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](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> 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(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 VoxelForgeCore::ResolvedLayoutPassageV1& passage) { @@ -1498,6 +1731,7 @@ namespace StandaloneStreamBenchmark return zUm>=stratum->content.bottomMicrometres&&zUmcontent.topMicrometres; }; 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 openAirAudits=0,openAirColumnSamples=0,openAirShortSpanSamples=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, &blockedWorld,&segmentSupport); const FPassageNaturalnessAudit naturalAudit=auditPassageNaturalness(Shape,passage); + const FPassageMouthProfileAudit mouthProfileAudit= + auditPassageMouthProfiles(Shape,passage); const FPassageOpenAirAudit openAirAudit=auditPassageOpenAir(Shape,blockedWorld, passage,passageIndex); const FPassageColumnAudit columnAudit=auditPassageColumns(Shape,blockedWorld, @@ -1574,8 +1810,9 @@ namespace StandaloneStreamBenchmark const bool mouthsInContent=mouthInContent(passage.upperStrateIndex,passage.upperMouthSubvoxels) &&mouthInContent(passage.lowerStrateIndex,passage.lowerMouthSubvoxels); ++passageCount;naturalPasses+=naturalAudit.passed; + mouthProfilePasses+=mouthProfileAudit.passed; 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", passage.stableId.c_str(),passage.upperStrateIndex,passage.lowerStrateIndex, passage.controlPoints.size(),static_cast(passageAudit.centers), @@ -1591,6 +1828,15 @@ namespace StandaloneStreamBenchmark static_cast(naturalAudit.wallSamples), static_cast(naturalAudit.missingWallSamples), 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(mouthProfileAudit.floorSamples), + static_cast(mouthProfileAudit.missingFloorSamples), + static_cast(mouthProfileAudit.wallSamples), + static_cast(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", passage.stableId.c_str(),static_cast(openAirAudit.supportSamples), static_cast(openAirAudit.violations), @@ -1891,12 +2137,23 @@ namespace StandaloneStreamBenchmark 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", 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", static_cast(naturalPasses),static_cast(passageCount), l23SmoothWallDetected?"DETECTED":"MISSED",MaximumFlatRunMetres, 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(mouthProfilePasses), + static_cast(passageCount),PassageProfileSpacingMetres, + PassageProfilePlateauMinimumMetres,PassageProfileStepThresholdMetres, + l23TerraceControlDetected?"DETECTED":"MISSED", + mouthProfilePasses==passageCount&&l23TerraceControlDetected?"PASS":"FAIL"); const bool auditPassed=passageCount>0&&passagePasses==passageCount &&naturalPasses==passageCount&&l23SmoothWallDetected + &&mouthProfilePasses==passageCount&&l23TerraceControlDetected &&openAirAudits==passageCount&&openAirViolations==0&&l24OpenAirControlDetected &&columnAuditsPassed&&noLongBridgeAuditPassed &&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings