diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 24bb014..4308313 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -403,7 +403,7 @@ namespace VoxelForgeCore // Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges. // Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB. // Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 212; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 213; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 535877a..8b1c8b6 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -3188,7 +3188,7 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t for(std::size_t i=0;i=xy.size()-1)?mouthRadius:midRadius; + ||(i>lowerMouthIndex&&i+1>=xy.size()-1))?mouthRadius:midRadius; if(surfaceGroundDeparture&&i(cumulative[i]) @@ -3197,6 +3197,24 @@ bool PlanResolvedLayoutPassagesV1(ResolvedLayoutSnapshotV3& layout,std::int32_t radius=surfacePortalRadius+static_cast(std::llround( (midRadius-surfacePortalRadius)*smooth)); } + // Grow the lower bore into its mouth over fixed route distance. Assigning + // the full mouth radius to the penultimate point creates a segment-length + // flare whose cubic-detrended profile can read as a short-period wall beat. + if(i<=lowerMouthIndex) + { + constexpr std::int64_t LowerMouthRadiusBlendLengthMetres=12; + const std::uint64_t blendLength=static_cast( + std::max(LowerMouthRadiusBlendLengthMetres*qPerMetre, + 2*mouthRadius)); + const std::uint64_t distanceToLowerMouth=mouthHorizontal-cumulative[i]; + if(distanceToLowerMouth(distanceToLowerMouth)/blendLength; + const double smooth=t*t*(3.0-2.0*t); + radius=midRadius+static_cast(std::llround( + (mouthRadius-midRadius)*(1.0-smooth))); + } + } std::int64_t playerZ=0; if(i>lowerMouthIndex) playerZ=lowerLandingPoints[i-lowerMouthIndex-1][2]; diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/CaptureOwnerPassagesL68.py b/Standalone/Research/WorldAssemblerM3/Linux68/CaptureOwnerPassagesL68.py new file mode 100644 index 0000000..4184940 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux68/CaptureOwnerPassagesL68.py @@ -0,0 +1,395 @@ +#!/usr/bin/env python3 +"""Render arrival, departure, and mid-tunnel views for the four-strate owner plan.""" +import json +import importlib.util +import argparse +import math +import os +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l68/captures-run" +BIN = ROOT / "Saved/Linux/l68/bin-final-clang" +CAPTURES = Path(__file__).resolve().parent / "captures" +HEADLESS = BIN / "VoxelForgeCoreHeadless-l68-final-clang" +RENDERER = BIN / "VoxelForgeOffscreenCapture-l68-final-clang" +PNG_DIR = OUT / "capture-png" +RECIPE_INPUTS = ( + ("passage-0-1", ROOT / "Standalone/data/owner_world.json", + "surface-world", "SurfaceWorld", "upper"), + ("passage-1-2", ROOT / "Standalone/data/owner_world.json", + "surface-world", "FloatingIslands", "upper"), + ("passage-2-3", ROOT / "Standalone/data/owner_world.json", + "surface-world", "TunnelNetwork", "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=("arrival-mouth", "departure-mouth", "mid-tunnel")) +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"[L68CaptureRun] 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 load_reference_cameras(): + log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log" + pattern = re.compile( + r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) " + r"target_m=\(([^)]*)\) clip=(\S+)", re.M) + values = {} + for name, camera, target, clip in pattern.findall( + log.read_text(encoding="utf-8", errors="replace")): + parse = lambda value: tuple(float(part) for part in value.split(",")) + values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip)) + return values + + +REFERENCE_CAMERAS = {} + + +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 + if (int(passage["upper_strate_index"],) == 1 + and int(passage["lower_strate_index"]) == 2 and not reverse): + # The old 7 m authored-radius beat sat near the middle of this audit window. + middle_distance = min(total - 12.0, 90.8) + 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 + ground["outside_camera"] = True + + 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] - radius + 1.6) + target_pos, target_radius = interpolate_route(path, target_distance) + target = (target_pos[0], target_pos[1], + target_pos[2] - target_radius + 1.6) + 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} + + def inside_mouth_view(prefix, at_start): + endpoint_distance = 0.0 if at_start else total + mouth, _, tangent, mouth_side = route_frame(path, endpoint_distance) + camera_distance = min(total, 7.4) if at_start else max(0.0, total - 5.5) + center, _, _, side = route_frame(path, camera_distance) + camera = (center[0] + side[0] * 2.5, + center[1] + side[1] * 2.5, center[2]) + outward = mul(tangent, -1.0) if at_start else tangent + target = (mouth[0] + outward[0] * 8.0 + mouth_side[0] * 2.5, + mouth[1] + outward[1] * 8.0 + mouth_side[1] * 2.5, + mouth[2]) + yaw, pitch = camera_angles(camera, target) + return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch, + "target": target, "clip": None, "camera_xy": camera[:2], + "focus": mouth, "mouth": mouth, "outside_camera": False, + "camera_location": "passage-air", "focus_radius": 30.0, + "eye_height_z": camera[2], "fov_deg": 80.0} + + # Place each interface view at its true route endpoint. The endpoint camera + # is a side cutaway so the solid terrain in front of an underground mouth + # cannot hide the floor-to-tunnel connection. + arrival_distance = 0.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_location"] = "passage-air" + arrival["focus_radius"] = 20.0 + arrival["yaw"], arrival["pitch"] = camera_angles( + arrival["camera"], arrival["target"]) + + entry_camera_distance = min(8.0, total * 0.1) + entry_center, entry_radius, entry_tangent, entry_side = route_frame( + path, entry_camera_distance) + # Player-height view from just inside the passage, aimed through its actual + # interface mouth so the floor, bore, and opening share one frame. + entry_camera = (entry_center[0], entry_center[1], + entry_center[2] - entry_radius + 1.6) + entry_target_pos, entry_target_radius = interpolate_route( + path, min(total, entry_distance + 14.0)) + entry_target = (entry_target_pos[0], entry_target_pos[1], + entry_camera[2] - 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": False, + "camera_location": "passage-air", "focus_radius": 20.0, + "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_center, _ = interpolate_route(path, middle_distance) + middle_view["camera"] = (middle_camera_center[0] + middle_side[0] * 0.75, + middle_camera_center[1] + middle_side[1] * 0.75, + middle[2] - middle_radius + 1.6) + middle_target_pos, middle_target_radius = interpolate_route( + path, min(total, middle_distance + 14.0)) + middle_view["target"] = (middle_target_pos[0] + middle_side[0] * 2.35, + middle_target_pos[1] + middle_side[1] * 2.35, + middle_target_pos[2] - middle_target_radius + 1.8) + # Keep the camera inside the bore, close to one wall, and look down the + # route so the old beat location is visible at player height. + middle_view["clip"] = None + 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, + inside_mouth_view("inside-mouth-start", True), + inside_mouth_view("inside-mouth-end", False)), total, lower_index + + +def export_capture(scene_name, source, recipe, passage, endpoint, view): + stem = f"{passage['stable_id'].replace('/', '-')}-{endpoint}" + view["name"] = stem + 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"] + export_camera = view.get("focus", camera) + run(f"{stem}-export", [HEADLESS, source, "--workers=12", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", "--clip-radius=3", "--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"] + surface = None + if view.get("outside_camera", False): + surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2]) + if surface is None: + camera = (view["camera_xy"][0], view["camera_xy"][1], view["eye_height_z"]) + else: + camera = (surface[0], surface[1], surface[2] + 1.6) + elif render_clip is not None: + camera = view["camera"] + yaw, pitch = camera_angles(camera, view["target"]) + fov_deg = view.get("fov_deg", 80.0) + print(f"[L68CaptureCamera] view={stem} camera_m=({fmt(camera)}) " + f"target_m=({fmt(view['target'])}) " + f"clip={fmt(render_clip) if render_clip is not None else 'none'} fov_deg={fov_deg:.1f}", + flush=True) + render = [RENDERER, mesh, png, "--size=1600x900", f"--fov-deg={fov_deg:.1f}", + 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"[L68CaptureJPEG] 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") + + +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, _ = select_passage(layout, mouth_generator, preferred_side) + forward_views, route_length, lower_index = make_views(layout, passage, False) + reverse_views, _, _ = make_views(layout, passage, True) + print(f"[L68CapturePassage] interface={scene_name.removeprefix('passage-')} " + f"id={passage['stable_id']} route_m={route_length:.3f} " + f"control_points={lower_index + 1}", flush=True) + if scene_name == "passage-2-3": + endpoint_views = (("arrival-mouth", forward_views[4]), + ("departure-mouth", forward_views[5])) + else: + endpoint_views = ( + ("arrival-mouth", forward_views[0] if scene_name == "passage-0-1" else forward_views[1]), + ("departure-mouth", reverse_views[3]), + ) + for endpoint, view in endpoint_views: + if OPTIONS.view and endpoint != OPTIONS.view: + continue + export_capture(scene_name, source, recipe, passage, endpoint, view) + if scene_name == "passage-1-2" and (OPTIONS.view is None or OPTIONS.view == "mid-tunnel"): + export_capture(scene_name, source, recipe, passage, "mid-tunnel", forward_views[2]) diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux68/REPORT.md new file mode 100644 index 0000000..23eb192 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux68/REPORT.md @@ -0,0 +1,48 @@ +# Linux68 — FI→TN 7 m wall beat + +## Cause and fix + +The 7.06 m beat came from the authored-radius profile. In the failing 32 m window, a radius-only diagnostic reproduces 4.77 cm at 7.0625 m. The route’s interior controls are generally 5.6–6.3 m apart; the last 3.0→4.5 m radius change spans 5.803 m, followed by an 8.8 m constant-radius span. The measured beat does not repeat at either spacing. Removing relief left 7.46 cm, and restoring the earlier carve cutoff left 7.55 cm. The shorter 114.23 m route moved the lower-mouth flare into the failing window; the L67 split rose from 6.78 to 14.52 cm. + +The shared route builder now blends the lower-mouth radius over 12 m with a smooth distance-based taper, replacing the single-segment radius jump. Evaluator version is **v213**. L67 support changes remain in place. + +## Passage audits + +Clang and GCC produced identical results under the authored-radius gauge with cubic detrending per 32 m window. Limits are unchanged: wall ≤3.5 cm, floor ≤2.5 cm. + +| Interface | Wall ripple | Period | Floor ripple | Blocked / unsupported / grade | Result | +|---|---:|---:|---:|---:|---| +| FI 0–1 | 1.45 cm | 4.562 m | 0.06 cm | 0 / 0 / 0 | PASS | +| FI 1–2 | 1.62 cm | 7.250 m | 1.33 cm | 0 / 0 / 0 | PASS | +| TN 2–3 | 0.92 cm | 7.375 m | 0.32 cm | 0 / 0 / 0 | PASS | + +Naturalness, mouth profile, and ripple audits passed 3/3. Synthetic sphere, floor bead, floor step, L23 smooth-wall, and L23 terrace controls were detected. Blocked controls were detected 3/3. The new-plan generator/style maps, 3/3 interfaces, and three transition audits passed; width and thickness negative controls were rejected. + +Clang and GCC builds passed with source stamp `33a8215f36abc00fadcdde5d6f3ff2d7054454fc2158e32470fb059e68bbd12e`. W1/W8/W12 hashes matched for all four strata on both compilers (8 compiler/stratum rows, 12 hash rows per worker count). The final fast tier passed **269/269** checks in 265.1 s. Its new-plan stage passed. The first fast attempt exposed the expected v213 floating-islands hash change against the old L52 pin; the current W1/W12 hashes match, and the v213 pin is [here](hash-baselines/owner-hash-floating-islands-style2-v213.log). + +## Captures + +All files are below 300 KB. The 1–2 camera is at the old ripple window. The 2–3 cameras are inside the passage and aimed through the mouths. The upper 2–3 exterior is sparse in the capture mesh, so that view reads as a dark opening. + +| View | File | Bytes | SHA-256 | +|---|---|---:|---| +| 1–2 mid-tunnel | [JPEG](captures/passage-1-2-0-mid-tunnel.jpg) | 29,282 | `39381b01ae4d1674800391c6d8c65383e0d6866abf3cd70a8330062c677d06fa` | +| 2–3 arrival mouth | [JPEG](captures/passage-2-3-0-arrival-mouth.jpg) | 20,123 | `f73ce987932a5e2ba165a3272b18d8786fcce3f815d0c2a1edcf13c99cc3f7b4` | +| 2–3 departure mouth | [JPEG](captures/passage-2-3-0-departure-mouth.jpg) | 26,024 | `7665166f8f2e78b86774931e53af32243a5f40d2fc95e1b7ad4bf8cd6d310bd5` | + +## Whole-view cost + +Three interleaved L67/L68 pairs per stratum; all 24 runs exited 0 with 12 hash rows. + +| Stratum | L67 median | L68 median | Change | +|---:|---:|---:|---:| +| 0 | 1.011 s | 1.074 s | +6.23% | +| 1 | 1.494 s | 1.503 s | +0.60% | +| 2 | 0.581 s | 0.577 s | −0.69% | +| 3 | 0.210 s | 0.210 s | 0.00% | + +## Failed runs and discarded diagnostics + +The first fast attempt failed only on the stale L52 floating-islands hash expectation; the updated v213 pin and final fast run passed. Relief-off and old-cutoff experiments still failed the wall limit. A generic control-point refinement reduced ripple but introduced one unsupported sample; a post-route refinement retained 7.23 cm ripple. Those experiments were reverted. One early audit used the landmark-demo flag on an incompatible passage fixture and exited during route setup; the corrected audit passed. + +Evidence is under `Saved/Linux/l68`: `build-clang.log`, `build-gcc.log`, `logs/final-{clang,gcc}-owner.log`, `logs/transition-clang.log`, `verify/owner-w1-w8-w12-determinism.json`, `cost/cost-summary.json`, and `verify/fast-tier-final.log`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/RunInterleavedWholeViewCostL68.py b/Standalone/Research/WorldAssemblerM3/Linux68/RunInterleavedWholeViewCostL68.py new file mode 100644 index 0000000..4872bac --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux68/RunInterleavedWholeViewCostL68.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Measure three interleaved L67/L68 whole-view pairs per owner strate.""" +import json +import os +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l68/cost" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "l67": ROOT / "Saved/Linux/l67/bin-final/VoxelForgeCoreHeadless-l67-final-clang", + "l68": ROOT / "Saved/Linux/l68/bin-final-clang/VoxelForgeCoreHeadless-l68-final-clang", +} +CAMERAS = { + 0: "0.000000,0.000000,-374.939453,0.000000,0.000000", + 1: "25.836914,3.199219,-737.000000,0.000000,0.000000", + 2: "19.166016,-14.200195,-753.374023,0.000000,0.000000", + 3: "-20.062500,-5.068359,-900.353516,0.000000,0.000000", +} +ORDER = ("l67", "l68", "l68", "l67", "l67", "l68") +PREFIXES = ( + "[PipelineFullDetailHash]", "[PipelineWholeViewHash]", + "[PipelineLODHash]", "[PipelineExactQueryHash]", +) +ELAPSED_RE = re.compile(r"\belapsed_ms=([0-9.]+)") +WHOLE_VIEW_RE = re.compile(r"^\[StreamPipeline\].*?whole_view_ready=([0-9.]+)s", re.M) +ENV = os.environ.copy() + + +def run(strate, repetition, variant): + name = f"strate{strate}-pair{repetition // 2 + 1}-{variant}" + log_path = OUT / f"{name}.log" + layout_path = OUT / f"{name}.json" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[variant]), str(DATA), "--workers=1", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", "--clip-radius=1", "--max-clip-level=8", "--max-level=0", + "--recipe=surface-world", "--stream-hash", "--max-clip-level=0", + "--render-distance-chunks=0", f"--camera={CAMERAS[strate]}", + f"--resolved-layout-output={layout_path}", + ] + start = time.monotonic() + with log_path.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, env=ENV, stdout=stream, + stderr=subprocess.STDOUT, timeout=1740, check=False) + elapsed = time.monotonic() - start + lines = log_path.read_text(encoding="utf-8", errors="replace").splitlines() + hashes = [line for line in lines if line.startswith(PREFIXES)] + pipeline_elapsed = [float(value) for line in lines + if line.startswith("[PipelineWholeViewHash]") + for value in ELAPSED_RE.findall(line)] + whole_view_match = WHOLE_VIEW_RE.search("\n".join(lines)) + record = { + "strate": strate, "pair_order": repetition, "variant": variant, + "elapsed_seconds": elapsed, "pipeline_elapsed_ms": pipeline_elapsed, + "whole_view_seconds": float(whole_view_match.group(1)) + if whole_view_match else None, + "exit_code": result.returncode, "hash_rows": len(hashes), + "log": str(log_path.relative_to(ROOT)), + } + print(json.dumps(record, sort_keys=True), flush=True) + return record + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + results = [] + for strate in range(4): + for repetition, variant in enumerate(ORDER): + record = run(strate, repetition, variant) + results.append(record) + if record["exit_code"] != 0 or record["hash_rows"] != 12: + break + else: + continue + break + strata = {} + for strate in range(4): + values = {variant: [row["whole_view_seconds"] for row in results + if row["strate"] == strate and row["variant"] == variant + and row["whole_view_seconds"] is not None] + for variant in ("l67", "l68")} + if all(len(values[variant]) == 3 for variant in ("l67", "l68")): + l67 = sorted(values["l67"])[1] + l68 = sorted(values["l68"])[1] + strata[str(strate)] = {"l67_seconds": values["l67"], + "l68_seconds": values["l68"], "l67_median_seconds": l67, + "l68_median_seconds": l68, + "change_percent": (l68 / l67 - 1.0) * 100.0} + summary = {"pairs_per_strate": 3, "order": ORDER, "strata": strata, + "runs": results, "complete": len(results) == 24 + and all(x["exit_code"] == 0 and x["hash_rows"] == 12 for x in results)} + (OUT / "cost-summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print(f"[L68CostSummary] complete={summary['complete']} runs={len(results)}", flush=True) + return 0 if summary["complete"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/RunOwnerW1W8W12DeterminismL68.py b/Standalone/Research/WorldAssemblerM3/Linux68/RunOwnerW1W8W12DeterminismL68.py new file mode 100644 index 0000000..948aed1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux68/RunOwnerW1W8W12DeterminismL68.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""Compare owner four-strata hashes at W1/W8/W12 with both compilers.""" +import json +import re +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l68" +DATA = ROOT / "Standalone/data/owner_world.json" +LAYOUT = OUT / "layouts/final-clang-owner.json" +BINARIES = { + "clang": OUT / "bin-final-clang/VoxelForgeCoreHeadless-l68-final-clang", + "gcc": OUT / "bin-final-gcc/VoxelForgeCoreHeadless-l68-final-gcc", +} +HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +RESULTS = [] + + +def hashes(path): + return HASH_RE.findall(path.read_text(encoding="utf-8", errors="replace")) + + +def cameras_by_strate(): + layout = json.loads(LAYOUT.read_text(encoding="utf-8")) + scale = float(layout["core_voxel_size_um"]) / 256_000_000.0 + cameras = {} + for passage in layout["passages"]: + upper = str(int(passage["upper_strate_index"])) + lower = str(int(passage["lower_strate_index"])) + cameras.setdefault(upper, tuple(float(v) * scale for v in passage["upper_mouth_q"])) + cameras[lower] = tuple(float(v) * scale for v in passage["lower_mouth_q"]) + if set(cameras) != {"0", "1", "2", "3"}: + raise RuntimeError(f"expected a camera for all four strata; found {sorted(cameras)}") + return cameras + + +def main(): + (OUT / "verify").mkdir(parents=True, exist_ok=True) + logs = OUT / "logs/determinism" + layouts = OUT / "layouts/determinism" + logs.mkdir(parents=True, exist_ok=True) + layouts.mkdir(parents=True, exist_ok=True) + cameras = cameras_by_strate() + for compiler, binary in BINARIES.items(): + for strate in range(4): + camera = ",".join(f"{value:.6f}" for value in (*cameras[str(strate)], 0.0, 0.0)) + outputs = {} + codes = {} + for workers in (1, 8, 12): + name = f"owner-new-plan-{compiler}-strate{strate}-w{workers}" + log_path = logs / f"{name}.log" + layout_path = layouts / f"{name}.json" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(binary), str(DATA), f"--workers={workers}", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", "--clip-radius=1", "--max-clip-level=8", + "--max-level=0", "--recipe=surface-world", "--stream-hash", + "--max-clip-level=0", "--render-distance-chunks=0", f"--camera={camera}", + f"--resolved-layout-output={layout_path}", + ] + with log_path.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, timeout=1740, check=False) + outputs[workers] = hashes(log_path) + codes[workers] = result.returncode + print(json.dumps({"compiler": compiler, "strate": strate, + "workers": workers, "exit_code": result.returncode, + "hash_rows": len(outputs[workers]), "log": str(log_path.relative_to(ROOT))}, + sort_keys=True), flush=True) + if result.returncode != 0 or len(outputs[workers]) != 12: + break + passed = (set(codes) == {1, 8, 12} and all(code == 0 for code in codes.values()) + and outputs[1] == outputs[8] == outputs[12]) + RESULTS.append({"compiler": compiler, "strate": strate, + "workers": {str(k): len(v) for k, v in outputs.items()}, + "exit_codes": {str(k): v for k, v in codes.items()}, + "w1_w8_w12_equal": passed}) + if not passed: + break + if RESULTS[-1]["compiler"] != compiler or not RESULTS[-1]["w1_w8_w12_equal"]: + break + passed = len(RESULTS) == 8 and all(row["w1_w8_w12_equal"] for row in RESULTS) + summary = {"status": "PASS" if passed else "FAIL", "strata": RESULTS} + (OUT / "verify/owner-w1-w8-w12-determinism.json").write_text( + json.dumps(summary, indent=2) + "\n", encoding="utf-8") + print(f"[L68OwnerW1W8W12] status={summary['status']} rows={len(RESULTS)}", flush=True) + return 0 if passed else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-1-2-0-mid-tunnel.jpg b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-1-2-0-mid-tunnel.jpg new file mode 100644 index 0000000..9c04f0a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-1-2-0-mid-tunnel.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-arrival-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-arrival-mouth.jpg new file mode 100644 index 0000000..3ac51c0 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-arrival-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-departure-mouth.jpg new file mode 100644 index 0000000..f005e98 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux68/captures/passage-2-3-0-departure-mouth.jpg differ diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 75b6ca8..bfff7b3 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -30,6 +30,7 @@ LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56" LINUX58 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux58" LINUX60 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux60" LINUX67 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux67" +LINUX68 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux68" OWNER = ROOT / "Standalone/data/regression/owner_world_2strate_l61.json" OWNER_NEW = ROOT / "Standalone/data/owner_world.json" OWNER_2STRATE_SHA256 = "7b095f9bcc6b90133d5a463f99f0b571133b2c549ce1ecf2d64fe9175e41e183" @@ -101,6 +102,16 @@ RECIPE_RE = re.compile( r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M) +def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: + if recipe == "surface-world" and style == 1: + return (LINUX67 / "hash-baselines" / "owner-hash-surface-world-style1-v212.log", + "L67-v212") + if recipe == "floating-islands" and style == 2: + return (LINUX68 / "hash-baselines" / "owner-hash-floating-islands-style2-v213.log", + "L68-v213") + raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") + + def compiler_name(binary: Path) -> str: return "gcc" if "gcc" in binary.name else "clang" @@ -624,13 +635,7 @@ def run_hash_pairs(runner: VerificationRunner, l52_baseline = ROOT / "Saved/Linux/l52" / f"owner-hash-{recipe}-style{style}-w12.log" l52_hashes = tuple(HASH_RE.findall(l52_baseline.read_text( encoding="utf-8", errors="replace"))) - if recipe == "surface-world": - expected_baseline = LINUX67 / "hash-baselines" / ( - "owner-hash-surface-world-style1-v212.log") - expected_label = "L67-v212" - else: - expected_baseline = l52_baseline - expected_label = "L52" + expected_baseline, expected_label = owner_hash_baseline(recipe, style) expected_hashes = tuple(HASH_RE.findall(expected_baseline.read_text( encoding="utf-8", errors="replace"))) same_expected = hashes[12] == expected_hashes and len(expected_hashes) == 12 @@ -674,11 +679,11 @@ def run_default_profile_identity(runner: VerificationRunner, binary: Path) -> No expected_style=2, expected_strate_styles={"0": 2, "1": 1}, workers=12, layout_path=OUT / "layouts" / f"{name}.json") values = tuple(HASH_RE.findall(output)) - baseline = ROOT / "Saved/Linux/l52" / f"owner-hash-{recipe}-style{style}-w12.log" + baseline, label = owner_hash_baseline(recipe, style) expected = tuple(HASH_RE.findall(baseline.read_text( encoding="utf-8", errors="replace"))) - runner.require(f"{name}:matches-L52", len(values) == 12 and values == expected, - f"current={len(values)} L52={len(expected)} exact canonical hash rows") + runner.require(f"{name}:matches-{label}", len(values) == 12 and values == expected, + f"current={len(values)} {label}={len(expected)} exact canonical hash rows") class _SplitMix64: