passages: two-pass swept-tube tunnels (owner design, L99, v215) - pending owner visual review
Route plan unchanged. Per tile, route segments whose bounded influence (tube + 4 m) touches the tile write min-combined tube distance (base sweep); relief/floor/mouth shaping only in the shell (detail sweep); 0.5 m edge blend into the strate field. Near-ready @W12: SW 1.01->0.80, FI 2.23->1.43, TN 2.18->1.37, VS 1.09->0.98 s; whole view FI 3.38->2.37 s. All passage walk audits pass (21,425/21,425 canonical, 3/3 interfaces); 24-run Clang/GCC W1/W8/W12 determinism; 0 sign flips. Change: mean 0.02 cm / 0.05 deg; max 25.9 cm (passage walls/roofs ~3.5 m from route, LOD2) and 49.8 deg (rock walls 13-25 m from routes, LOD2). Captures with diffs in Linux99/captures. Fast tier PASS (v215 pins). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,406 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reframe and verify the ten L99 passage-change hotspot captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HERE = Path(__file__).resolve().parent
|
||||
CAPTURES = HERE / "captures"
|
||||
RUNS = ROOT / "Saved/Linux/l99/captures-fix"
|
||||
LAYOUT = ROOT / "Saved/Linux/l99/resolved-sw.json"
|
||||
REACH = ROOT / "Saved/Linux/l99/carve-reach/summary.json"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
RENDERER = ROOT / "Saved/Linux/l99/bin/VoxelForgeOffscreenCapture-l99-clang"
|
||||
BINARIES = {
|
||||
"before-l98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"after-l99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
}
|
||||
sys.path.insert(0, str(HERE))
|
||||
from RunL99Captures import angles, fmt, make_passages, nearest_route, norm, add, mul, sub # noqa: E402
|
||||
|
||||
|
||||
def screen_projection(camera, target, yaw_degrees, pitch_degrees, width=1600, height=900,
|
||||
horizontal_fov=56.0):
|
||||
yaw, pitch = math.radians(yaw_degrees), math.radians(pitch_degrees)
|
||||
forward = (math.sin(yaw) * math.cos(pitch), math.cos(yaw) * math.cos(pitch),
|
||||
math.sin(pitch))
|
||||
right = (math.cos(yaw), -math.sin(yaw), 0.0)
|
||||
up = (right[1] * forward[2] - right[2] * forward[1],
|
||||
right[2] * forward[0] - right[0] * forward[2],
|
||||
right[0] * forward[1] - right[1] * forward[0])
|
||||
delta = sub(target, camera)
|
||||
depth = sum(delta[i] * forward[i] for i in range(3))
|
||||
x = sum(delta[i] * right[i] for i in range(3))
|
||||
y = sum(delta[i] * up[i] for i in range(3))
|
||||
tan_h = math.tan(math.radians(horizontal_fov) * 0.5)
|
||||
tan_v = tan_h / (width / height)
|
||||
return (0.5 + x / (2.0 * depth * tan_h),
|
||||
0.5 - y / (2.0 * depth * tan_v))
|
||||
|
||||
|
||||
def run(name: str, command: list[str]) -> dict:
|
||||
log = RUNS / f"{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", *map(str, command)]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1745)
|
||||
row = {"name": name, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L99CaptureFixRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"capture step failed: {name}; see {log}")
|
||||
return row
|
||||
|
||||
|
||||
def closest_on_triangle(p, a, b, c):
|
||||
"""Ericson closest point, with barycentric weights for normal interpolation."""
|
||||
ab, ac, ap = sub(b, a), sub(c, a), sub(p, a)
|
||||
d1, d2 = sum(ab[i] * ap[i] for i in range(3)), sum(ac[i] * ap[i] for i in range(3))
|
||||
if d1 <= 0.0 and d2 <= 0.0:
|
||||
return a, (1.0, 0.0, 0.0)
|
||||
bp = sub(p, b)
|
||||
d3, d4 = sum(ab[i] * bp[i] for i in range(3)), sum(ac[i] * bp[i] for i in range(3))
|
||||
if d3 >= 0.0 and d4 <= d3:
|
||||
return b, (0.0, 1.0, 0.0)
|
||||
vc = d1 * d4 - d3 * d2
|
||||
if vc <= 0.0 and d1 >= 0.0 and d3 <= 0.0:
|
||||
v = d1 / (d1 - d3)
|
||||
return add(a, mul(ab, v)), (1.0 - v, v, 0.0)
|
||||
cp = sub(p, c)
|
||||
d5, d6 = sum(ab[i] * cp[i] for i in range(3)), sum(ac[i] * cp[i] for i in range(3))
|
||||
if d6 >= 0.0 and d5 <= d6:
|
||||
return c, (0.0, 0.0, 1.0)
|
||||
vb = d5 * d2 - d1 * d6
|
||||
if vb <= 0.0 and d2 >= 0.0 and d6 <= 0.0:
|
||||
w = d2 / (d2 - d6)
|
||||
return add(a, mul(ac, w)), (1.0 - w, 0.0, w)
|
||||
va = d3 * d6 - d5 * d4
|
||||
if va <= 0.0 and (d4 - d3) >= 0.0 and (d5 - d6) >= 0.0:
|
||||
w = (d4 - d3) / ((d4 - d3) + (d5 - d6))
|
||||
return add(b, mul(sub(c, b), w)), (0.0, 1.0 - w, w)
|
||||
denominator = 1.0 / (va + vb + vc)
|
||||
v, w = vb * denominator, vc * denominator
|
||||
return add(a, add(mul(ab, v), mul(ac, w))), (1.0 - v - w, v, w)
|
||||
|
||||
|
||||
def nearest_surface(mesh: Path, point):
|
||||
"""Find the closest non-skirt LOD2 triangle in the point's tile neighbourhood."""
|
||||
best = None
|
||||
target_key = tuple(math.floor(value / 32.0) for value in point)
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh header {magic!r}: {mesh}")
|
||||
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if materials else 1):
|
||||
raise RuntimeError(f"mesh header version mismatch: {mesh}")
|
||||
stream.seek(40, 1)
|
||||
lod_counts = {}
|
||||
relevant_tiles = []
|
||||
for _ in range(tile_count):
|
||||
key = struct.unpack("<4i", stream.read(16))
|
||||
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
lod_counts[key[3]] = lod_counts.get(key[3], 0) + 1
|
||||
positions = array("f")
|
||||
positions.frombytes(stream.read(vertex_count * 12))
|
||||
normals = array("f")
|
||||
normals.frombytes(stream.read(vertex_count * 12))
|
||||
if materials:
|
||||
stream.seek(vertex_count * 10, 1)
|
||||
indices = array("I")
|
||||
indices.frombytes(stream.read(index_count * 4))
|
||||
masks = stream.read(mask_count)
|
||||
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
indices.byteswap()
|
||||
if key[3] != 2 or any(abs(key[axis] - target_key[axis]) > 1
|
||||
for axis in range(3)):
|
||||
continue
|
||||
relevant_tiles.append(key)
|
||||
xyz = [tuple(float(positions[i * 3 + axis]) for axis in range(3))
|
||||
for i in range(vertex_count)]
|
||||
nrm = [tuple(float(normals[i * 3 + axis]) for axis in range(3))
|
||||
for i in range(vertex_count)]
|
||||
triangle_count = index_count // 3
|
||||
for tri in range(triangle_count):
|
||||
if masks and masks[tri] != 0:
|
||||
continue
|
||||
ids = tuple(int(indices[tri * 3 + corner]) for corner in range(3))
|
||||
if any(index >= surface_count for index in ids):
|
||||
continue
|
||||
hit, weights = closest_on_triangle(point, *(xyz[index] for index in ids))
|
||||
distance_sq = sum((hit[axis] - point[axis]) ** 2 for axis in range(3))
|
||||
if best is not None and distance_sq >= best[0]:
|
||||
continue
|
||||
raw_normal = tuple(sum(weights[k] * nrm[ids[k]][axis] for k in range(3))
|
||||
for axis in range(3))
|
||||
length = math.sqrt(sum(value * value for value in raw_normal))
|
||||
if length < 1.0e-8:
|
||||
continue
|
||||
best = (distance_sq, hit, tuple(value / length for value in raw_normal), key,
|
||||
ids, weights)
|
||||
if best is None:
|
||||
raise RuntimeError(f"no LOD2 surface triangles near {point}; lods={lod_counts}")
|
||||
return {"distance_m": math.sqrt(best[0]), "position_m": list(best[1]),
|
||||
"normal": list(best[2]), "tile": list(best[3]),
|
||||
"triangle_vertices": list(best[4]), "barycentric": list(best[5]),
|
||||
"lod_counts": lod_counts, "nearby_lod2_tiles": len(relevant_tiles)}
|
||||
|
||||
|
||||
def decode_rgba(path: Path) -> tuple[int, int, bytes]:
|
||||
command = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", "ffmpeg", "-v", "error", "-i", str(path),
|
||||
"-frames:v", "1", "-f", "rawvideo", "-pix_fmt", "rgba", "pipe:1"]
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, check=False, timeout=1745)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"PNG decode failed for {path}: {result.stderr.decode(errors='replace')}")
|
||||
# ffmpeg rawvideo has no dimensions. Read the PNG IHDR separately for an exact size.
|
||||
with path.open("rb") as stream:
|
||||
if stream.read(8) != b"\x89PNG\r\n\x1a\n":
|
||||
raise RuntimeError(f"invalid PNG signature: {path}")
|
||||
stream.seek(16)
|
||||
width, height = struct.unpack(">II", stream.read(8))
|
||||
if len(result.stdout) != width * height * 4:
|
||||
raise RuntimeError(f"decoded RGBA size mismatch for {path}: {len(result.stdout)}")
|
||||
return width, height, result.stdout
|
||||
|
||||
|
||||
def write_png(path: Path, width: int, height: int, rgba: bytes):
|
||||
def chunk(kind: bytes, data: bytes) -> bytes:
|
||||
payload = kind + data
|
||||
return struct.pack(">I", len(data)) + payload + struct.pack(">I", zlib.crc32(payload) & 0xffffffff)
|
||||
rows = bytearray()
|
||||
stride = width * 4
|
||||
for y in range(height):
|
||||
rows.append(0)
|
||||
rows.extend(rgba[y * stride:(y + 1) * stride])
|
||||
png = (b"\x89PNG\r\n\x1a\n"
|
||||
+ chunk(b"IHDR", struct.pack(">2I5B", width, height, 8, 6, 0, 0, 0))
|
||||
+ chunk(b"IDAT", zlib.compress(bytes(rows), 9))
|
||||
+ chunk(b"IEND", b""))
|
||||
path.write_bytes(png)
|
||||
|
||||
|
||||
def pixel_diff(before: Path, after: Path, output: Path, amplify: int = 64):
|
||||
width, height, a = decode_rgba(before)
|
||||
width_b, height_b, b = decode_rgba(after)
|
||||
if (width, height) != (width_b, height_b):
|
||||
raise RuntimeError(f"image dimensions differ: {before} and {after}")
|
||||
diff = bytearray(len(a))
|
||||
changed = 0
|
||||
channel_sum = 0
|
||||
max_delta = 0
|
||||
for offset in range(0, len(a), 4):
|
||||
dr = abs(a[offset] - b[offset])
|
||||
dg = abs(a[offset + 1] - b[offset + 1])
|
||||
db = abs(a[offset + 2] - b[offset + 2])
|
||||
if dr or dg or db:
|
||||
changed += 1
|
||||
channel_sum += dr + dg + db
|
||||
max_delta = max(max_delta, dr, dg, db)
|
||||
diff[offset] = min(255, dr * amplify)
|
||||
diff[offset + 1] = min(255, dg * amplify)
|
||||
diff[offset + 2] = min(255, db * amplify)
|
||||
diff[offset + 3] = 255
|
||||
write_png(output, width, height, bytes(diff))
|
||||
return {"width": width, "height": height, "changed_pixels": changed,
|
||||
"total_pixels": width * height, "changed_percent": round(100.0 * changed / (width * height), 5),
|
||||
"sum_abs_rgb": channel_sum, "max_channel_delta": max_delta,
|
||||
"diff_amplification": amplify}
|
||||
|
||||
|
||||
def surface_description(region, outward_normal, route_height_delta, route_radius,
|
||||
route_distance):
|
||||
nz = outward_normal[2]
|
||||
if region == 2:
|
||||
return "passage mouth surface"
|
||||
if region == 1:
|
||||
return "passage floor"
|
||||
if region == 0:
|
||||
if route_distance > route_radius + 2.0:
|
||||
if nz < -0.55:
|
||||
return "rock overhang / underside near passage"
|
||||
if abs(nz) < 0.35:
|
||||
return "other strata rock wall near passage"
|
||||
return "sloping strata surface near passage"
|
||||
if route_height_delta > 0.55 * route_radius:
|
||||
return "passage upper wall / roof"
|
||||
if route_height_delta < -0.55 * route_radius:
|
||||
return "passage floor"
|
||||
return "passage wall"
|
||||
if nz < -0.55:
|
||||
return "rock overhang / island underside outside the passage envelope"
|
||||
if nz > 0.7:
|
||||
return "upper terrain surface outside the passage envelope"
|
||||
if abs(nz) < 0.35:
|
||||
return "other strata rock wall outside the passage envelope"
|
||||
return "sloping strata surface outside the passage envelope"
|
||||
|
||||
|
||||
def main():
|
||||
RUNS.mkdir(parents=True, exist_ok=True)
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
for path in (*BINARIES.values(), RENDERER, DATA, LAYOUT, REACH):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing input: {path}")
|
||||
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
reach = json.loads(REACH.read_text(encoding="utf-8"))
|
||||
_, routes = make_passages(layout)
|
||||
sites = []
|
||||
for kind, rows, value_key, point_key, region_key in (
|
||||
("displacement", reach["top_displacement_tiles"], "max_displacement_cm",
|
||||
"displacement_location_metres", "displacement_region"),
|
||||
("normal", reach["top_normal_tiles"], "max_normal_degrees",
|
||||
"normal_location_metres", "normal_region"),
|
||||
):
|
||||
for index, row in enumerate(rows, 1):
|
||||
point = tuple(float(v) for v in row[point_key])
|
||||
route, passage_id = min(((nearest_route(route_points, point), stable_id)
|
||||
for stable_id, route_points in routes),
|
||||
key=lambda candidate: candidate[0][0])
|
||||
sites.append({"name": f"top-{kind}-{index:02d}", "kind": kind,
|
||||
"point": point, "strate": row["strate"],
|
||||
"value": float(row[value_key]), "region": int(row[region_key]),
|
||||
"nearest_passage": passage_id,
|
||||
"passage_distance_m": float(route[0]),
|
||||
"route_tangent": list(route[2]),
|
||||
"route_height_delta_m": point[2] - route[1][2],
|
||||
"route_radius_m": float(route[4]),
|
||||
"row": row})
|
||||
|
||||
records = []
|
||||
for site in sites:
|
||||
point = site["point"]
|
||||
# Place the export camera far enough away that the audited location is in the LOD2 shell.
|
||||
# The offscreen renderer then moves back to the exact changed surface for a close view.
|
||||
direction = -1.0 if point[0] >= 0.0 else 1.0
|
||||
export_camera = (point[0] + direction * 80.0, point[1], point[2])
|
||||
yaw_export, pitch_export = 0.0, 0.0
|
||||
meshes = {}
|
||||
exports = {}
|
||||
for label, binary in BINARIES.items():
|
||||
mesh = RUNS / f"{site['name']}-{label}.vfmesh"
|
||||
exports[label] = run(f"{site['name']}-{label}-export", [binary, DATA,
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--workers=8", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=2", "--max-level=2", "--render-distance-chunks=0",
|
||||
f"--camera={fmt((*export_camera, yaw_export, pitch_export))}",
|
||||
f"--mesh-export={mesh}"])
|
||||
if not mesh.is_file() or mesh.stat().st_size <= 0:
|
||||
raise RuntimeError(f"empty exported scene for {site['name']} {label}")
|
||||
meshes[label] = mesh
|
||||
anchor = nearest_surface(meshes["before-l98"], point)
|
||||
after_anchor = nearest_surface(meshes["after-l99"], point)
|
||||
expected_tile = list(site["row"]["tile"])
|
||||
if anchor["tile"] != expected_tile or after_anchor["tile"] != expected_tile:
|
||||
raise RuntimeError(f"change tile not represented by the LOD2 export for {site['name']}: "
|
||||
f"expected={expected_tile} before={anchor['tile']} "
|
||||
f"after={after_anchor['tile']}")
|
||||
if anchor["distance_m"] > 0.1 or after_anchor["distance_m"] > 0.1:
|
||||
raise RuntimeError(f"change point too far from exported surface for {site['name']}: "
|
||||
f"before={anchor['distance_m']:.3f}m "
|
||||
f"after={after_anchor['distance_m']:.3f}m")
|
||||
camera_distance = 3.25
|
||||
# Marching-cubes normals point into the solid in these exports, so the air-side view
|
||||
# starts along their negative direction.
|
||||
air_normal = mul(tuple(anchor["normal"]), -1.0)
|
||||
camera_direction = air_normal
|
||||
if site["kind"] == "displacement":
|
||||
route_tangent = tuple(site["route_tangent"])
|
||||
tangent = sub(route_tangent, mul(air_normal,
|
||||
sum(route_tangent[i] * air_normal[i] for i in range(3))))
|
||||
tangent = norm(tangent)
|
||||
camera_direction = norm(add(mul(air_normal, 0.70710678),
|
||||
mul(tangent, 0.70710678)))
|
||||
camera = add(tuple(anchor["position_m"]), mul(camera_direction, camera_distance))
|
||||
yaw, pitch = angles(camera, point)
|
||||
actual_camera_distance = math.sqrt(sum((camera[axis] - point[axis]) ** 2
|
||||
for axis in range(3)))
|
||||
if actual_camera_distance > 5.0 or actual_camera_distance < 2.0:
|
||||
raise RuntimeError(f"changed point outside close framing range for {site['name']}: "
|
||||
f"camera-to-point={actual_camera_distance:.3f}m; "
|
||||
f"surface-offset={anchor['distance_m']:.3f}m")
|
||||
target_uv = screen_projection(camera, point, yaw, pitch)
|
||||
projection_error_px = math.hypot((target_uv[0] - 0.5) * 1600,
|
||||
(target_uv[1] - 0.5) * 900)
|
||||
if projection_error_px > 0.01:
|
||||
raise RuntimeError(f"changed point is off-axis for {site['name']}: {target_uv}")
|
||||
view = {**site, "focus": list(point), "camera": list(camera),
|
||||
"target": list(point), "yaw": yaw, "pitch": pitch,
|
||||
"target_screen_uv": list(target_uv),
|
||||
"target_projection_error_px": projection_error_px,
|
||||
"change_tile_lod2": expected_tile,
|
||||
"surface_match_distance_m": anchor["distance_m"],
|
||||
"camera_to_surface_m": camera_distance,
|
||||
"camera_to_changed_point_m": actual_camera_distance,
|
||||
"camera_side": "air side, 45-degree passage-tangent view"
|
||||
if site["kind"] == "displacement" else "air side (negative exported normal)",
|
||||
"surface_anchor": anchor, "after_surface_anchor": after_anchor,
|
||||
"surface_type": surface_description(site["region"],
|
||||
tuple(-value for value in anchor["normal"]),
|
||||
site["route_height_delta_m"], site["route_radius_m"],
|
||||
site["passage_distance_m"]),
|
||||
"export_camera": list(export_camera), "export_lod_counts": anchor["lod_counts"]}
|
||||
render_records = {}
|
||||
render_shade = "normal" if site["kind"] == "normal" else "readable"
|
||||
view["render_shade"] = render_shade
|
||||
for label in BINARIES:
|
||||
suffix = "before-l98" if label == "before-l98" else "after-l99"
|
||||
image = CAPTURES / f"{site['name']}-{suffix}.png"
|
||||
render_args = [RENDERER, meshes[label], image, "--size=1600x900",
|
||||
"--fov-deg=56", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.8f}",
|
||||
f"--pitch-deg={pitch:.8f}", f"--shade={render_shade}", "--headlamp=on",
|
||||
"--height-tint=on", "--tile-edges=off", "--tile-skirts=off",
|
||||
f"--height-center-m={point[2]:.6f}", "--height-range-m=8",
|
||||
f"--focus-region={fmt((*point, 10.0))}"]
|
||||
render_records[label] = run(f"{site['name']}-{label}-render", render_args)
|
||||
render_records[label]["image"] = str(image.relative_to(ROOT))
|
||||
render_records[label]["image_bytes"] = image.stat().st_size
|
||||
for mesh in meshes.values():
|
||||
mesh.unlink()
|
||||
diff_path = CAPTURES / f"{site['name']}-diff-amplified.png"
|
||||
pixel_stats = pixel_diff(CAPTURES / f"{site['name']}-before-l98.png",
|
||||
CAPTURES / f"{site['name']}-after-l99.png", diff_path)
|
||||
record = {"view": view, "before": render_records["before-l98"],
|
||||
"after": render_records["after-l99"],
|
||||
"diff": str(diff_path.relative_to(ROOT)), "pixel_diff": pixel_stats,
|
||||
"exports": exports}
|
||||
records.append(record)
|
||||
print("[L99CaptureFixSite] " + json.dumps({"name": site["name"],
|
||||
"lod_counts": anchor["lod_counts"], "surface_distance_m": anchor["distance_m"],
|
||||
"camera_distance_m": camera_distance,
|
||||
"changed_pixels": pixel_stats["changed_pixels"],
|
||||
"surface_type": view["surface_type"], "passage": site["nearest_passage"],
|
||||
"passage_distance_m": site["passage_distance_m"]}, sort_keys=True), flush=True)
|
||||
summary = {"source_layout_identity": layout["stable_identity"],
|
||||
"capture_lod": 2, "image_size": [1600, 900],
|
||||
"camera_surface_distance_m": 3.25,
|
||||
"sites": records}
|
||||
(RUNS / "fixed-summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L99CaptureFixSummary] " + json.dumps({
|
||||
"sites": len(records),
|
||||
"changed_pixels": {r["view"]["name"]: r["pixel_diff"]["changed_pixels"]
|
||||
for r in records},
|
||||
"fixed_summary": str((RUNS / "fixed-summary.json").relative_to(ROOT))},
|
||||
sort_keys=True), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,114 @@
|
||||
# Linux 99 — bounded swept-tube passages
|
||||
|
||||
Branch: `assembler/world` · evaluator version: **215** · no commit made.
|
||||
|
||||
## Design
|
||||
|
||||
The route plan and its three passage splines are unchanged. L98 and L99 resolve the same layout identity (`resolved-layout-v3-fnv64:0589300eb07dfe06`) and exact passage control points. L99 prepares each tile's sorted route-segment candidates from tube bounds expanded by 4 m, min-combines their tube distances, then applies localized wall/floor relief and mouth shaping in the detail sweep. A 0.5 m edge blend bounds the integration with the strate field. Existing tread, clearance, mouth fade, and void-crossing support remain in generation.
|
||||
|
||||
## Speed
|
||||
|
||||
L80 protocol: three interleaved measured L98/L99 pairs at W8 and W12, with warmups, for each stratum. Entries are median seconds (`L98 → L99`). Stratum labels are explicit: SW = Surface World, FI = Floating Islands, TN = Tunnel Network, VS = Vertical Shafts. Whole/near tile counts are shown to keep the world labels fixed.
|
||||
|
||||
| Workers | Stratum | Whole / LOD0–2 tiles | Near ready (s) | Whole-view ready (s) |
|
||||
|---:|---|---:|---:|---:|
|
||||
| 8 | SW | 2,850 / 1,408 | 1.179 → 0.970 | 2.605 → 2.241 |
|
||||
| 8 | FI | 2,249 / 1,216 | 2.342 → 1.499 | 3.583 → 2.524 |
|
||||
| 8 | TN | 2,233 / 1,344 | 2.357 → 1.506 | 3.420 → 2.449 |
|
||||
| 8 | VS | 2,329 / 1,344 | 2.204 → 1.721 | 3.429 → 3.028 |
|
||||
| 12 | SW | 2,850 / 1,408 | 1.006 → 0.799 | 2.114 → 1.845 |
|
||||
| 12 | FI | 2,249 / 1,216 | 2.225 → 1.432 | 3.382 → 2.373 |
|
||||
| 12 | TN | 2,233 / 1,344 | 2.183 → 1.369 | 3.188 → 2.237 |
|
||||
| 12 | VS | 2,329 / 1,344 | 1.093 → 0.983 | 2.019 → 1.884 |
|
||||
|
||||
All eight cells beat L98. FI/TN approach the ~1 s target, but remain above the passage-off ceilings (0.81/0.85 s). Native Clang and GCC builds took about 11m30s and 1m38s combined; these are accounted separately from readiness times. The Unreal `VFHostEditor Linux Development` build succeeded after timing in 71.50 s with `-NoUBA -MaxParallelActions=4`.
|
||||
|
||||
The fast tier is **PASS**: 80 runs, 301.4 s, 269 passing checks and 5 retained historical-hash diagnostics. Both v215 owner hash pins pass.
|
||||
|
||||
## Passage audits and measured work
|
||||
|
||||
The full-view canonical audit (`max-level=-1`, W12) passes **21,425/21,425** centerline samples. All three resolved interfaces passed:
|
||||
|
||||
| Interface | Center / capsule samples | Blocked | Support samples / unsupported | Grade failures | Mouth content / radius | Result |
|
||||
|---|---:|---:|---:|---:|---:|---|
|
||||
| 0–1 | 4,151 / 261,513 | 0 | 27,206 / 0 | 0 | 2/2 · 2/2 | PASS |
|
||||
| 1–2 | 239 / 15,057 | 0 | 1,309 / 0 | 0 | 2/2 · 2/2 | PASS |
|
||||
| 2–3 | 1,027 / 64,701 | 0 | 5,651 / 0 | 0 | 2/2 · 2/2 | PASS |
|
||||
|
||||
The audit summary reports naturalness 3/3, all three interfaces passed, and zero route-binding failures. The fast-tier owner style-1 and style-2 audits also pass, as does the arrival/departure chain (`interfaces=3/3 all_walkable=true`). Capsule clearance, support, headroom, grade, floor and mouth profiles, route binding, open-air, no-long-bridge, rock-column and ripple checks pass.
|
||||
|
||||
L77 near-LOD passage CPU share (three repeats, measured mean):
|
||||
|
||||
| Stratum | L98 | L99 |
|
||||
|---|---:|---:|
|
||||
| SW | 42.83% | 19.75% |
|
||||
| FI | 56.22% | 34.78% |
|
||||
| TN | 55.58% | 28.81% |
|
||||
| VS | 30.21% | 8.56% |
|
||||
|
||||
Measured LOD0–2 sweep counters (W8):
|
||||
|
||||
| Stratum | Bounded segment candidates | Segment/sample pairs (outside-bound rejects) | Grid samples (no influence) | Base / detail samples |
|
||||
|---|---:|---:|---:|---:|
|
||||
| SW | 892 | 8,568,233 (4,699,672) | 14,011,370 (11,949,427) | 2,061,943 / 210,393 |
|
||||
| FI | 1,439 | 15,261,585 (8,730,625) | 21,565,975 (18,604,965) | 3,377,667 / 347,335 |
|
||||
| TN | 1,560 | 14,723,215 (8,313,402) | 17,969,538 (15,233,648) | 3,152,585 / 332,547 |
|
||||
| VS | 1,558 | 14,092,586 (7,348,748) | 20,749,529 (18,066,064) | 2,683,465 / 273,636 |
|
||||
| **Total** | **5,449** | **52,645,619 (29,092,447)** | **74,296,412 (63,854,104)** | **11,275,660 / 1,163,911** |
|
||||
|
||||
The profile covered 478/629/614/699 near tiles (SW/FI/TN/VS). Counts are measured. The bounded-reject and no-influence counts mean those segment tests and tube samples do not proceed to distance composition; the remaining 43,354,950 prepared-sweep lookups had 11,996,257 hits and 31,358,693 misses.
|
||||
|
||||
## World change and captures
|
||||
|
||||
Carve-reach comparisons covered every desired near-set tile: SW 1,408/1,408, FI 1,216/1,216, TN 1,344/1,344, VS 1,344/1,344. Every stratum had zero sign flips and zero edge-crossing mismatches.
|
||||
|
||||
| Stratum | Max / mean displacement | Max / mean normal change | Moved crossings | Pairing failures |
|
||||
|---|---:|---:|---:|---:|
|
||||
| SW | 20.11 / 0.0180 cm | 21.35 / 0.0874° | 9,214 | 21 |
|
||||
| FI | 19.80 / 0.0185 cm | 29.91 / 0.0477° | 10,238 | 114 |
|
||||
| TN | 21.74 / 0.0387 cm | 41.79 / 0.0291° | 11,370 | 116 |
|
||||
| VS | 25.88 / 0.0178 cm | 49.82 / 0.00731° | 5,087 | 32 |
|
||||
|
||||
The audit's `FAIL` status is only its existing 1 cm displacement / 2° normal-change diagnostic threshold; those thresholds were not an L99 gate. Maxima and their coordinates are in the capture index. Paired L98/L99 captures include the top five displacement sites, top five normal-change sites, both mouths of all three passages, and a midpoint view for each passage: **19 matched views / 38 before-and-after PNGs** in [`captures/INDEX.md`](captures/INDEX.md), plus ten amplified hotspot diffs.
|
||||
|
||||
## Determinism and version
|
||||
|
||||
All 24 whole-view runs passed: Clang/GCC at W1/W8/W12 across SW/FI/TN/VS. The 20 peer comparisons had zero tile-count, per-tile hash, or whole-hash mismatches. Representative Clang W1 whole hashes (tile count followed by four components):
|
||||
|
||||
| Stratum | Whole hash |
|
||||
|---|---|
|
||||
| SW | `2850, a962f8cb29383f0e, 3deecb1a8f537726, fa367c6df153636f, 0b0a55b24243bd1e` |
|
||||
| FI | `2249, 1d867417832047f9, 3f5bf03b78621a43, 601921b84046915d, c00d44660831d266` |
|
||||
| TN | `2233, 3c0f10f94c18944d, e213da2d3bc34fe1, 318aaa3a18a997be, 7a1cd0183cc57c07` |
|
||||
| VS | `2329, 3005885856f8adef, 2e3181ca8152f93c, 0cddc16aa45946bb, 69eb58192bab4302` |
|
||||
|
||||
Version 215 pins: [`owner-hash-floating-islands-style2-v215.baseline`](../../../../Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v215.baseline) and [`owner-hash-surface-world-style1-v215.baseline`](../../../../Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v215.baseline). Source stamp: `0d31d588192e30bd`.
|
||||
|
||||
## Failed runs
|
||||
|
||||
- The first fast-tier attempt stopped at stale pre-L99 hash pins; v215 pins were added and the full rerun passed.
|
||||
- A zero-radius smoke configuration aborted with `std::bad_alloc` on both L98 and L99; it was an invalid diagnostic setup. Capture probes first rejected an overly strict mouth-anchor assumption, then hit open-contour clip-cap errors; a camera-coordinate bug also produced an empty render. Those probes were corrected, and the final matched views rendered successfully without clipping.
|
||||
- The displacement/normal diagnostic returned exit 14 because its legacy 1 cm / 2° thresholds were exceeded. This reports the measured world change above, not a passage walkability failure.
|
||||
|
||||
All measured workload processes were capped below 30 minutes and run on CPUs 0–11; headless processes used the 6 GiB address-space limit. Logs and summaries are under `Saved/Linux/l99/`. No install, commit, push, or stash was made.
|
||||
|
||||
## Captures (fixed)
|
||||
|
||||
The first hotspot views exported LOD0, while these maxima come from LOD2. The fixed views export the matching LOD2 audit tile, put the exact audit point at image center, and place the camera 3.25 m from its L98 surface (3.25–3.29 m from the audit point). Each before-and-after surface triangle is within 0.09 m of the reported point in the matching LOD2 tile. Displacement views look obliquely along the passage; normal-change views use normal-color shading. Pixel counts below are measured decoded RGB pixels with any changed channel; each diff image is absolute RGB difference amplified 64×. All ten pairs differ.
|
||||
|
||||
Strata labels: SW = Surface World, FI = Floating Islands, TN = Tunnel Network, VS = Vertical Shafts. Passage distances are measured from the site to the nearest route centerline; surface labels use the nearest LOD2 surface and passage-region class.
|
||||
|
||||
| Site | Strate · change · focus (m) | Surface · nearest passage | Changed pixels | Before / after / diff |
|
||||
|---|---|---|---:|---|
|
||||
| `top-displacement-01` | VS · 25.88 cm · (18.000, -2.000, -817.035) | passage mouth surface · passage/2-3/0, 3.17 m | 557,549 | [L98](captures/top-displacement-01-before-l98.png) / [L99](captures/top-displacement-01-after-l99.png) / [Δ×64](captures/top-displacement-01-diff-amplified.png) |
|
||||
| `top-displacement-02` | TN · 21.74 cm · (26.000, -4.000, -692.765) | passage upper wall/roof · passage/0-1/0, 3.47 m | 565,587 | [L98](captures/top-displacement-02-before-l98.png) / [L99](captures/top-displacement-02-after-l99.png) / [Δ×64](captures/top-displacement-02-diff-amplified.png) |
|
||||
| `top-displacement-03` | SW · 20.11 cm · (-23.127, 4.000, -490.000) | passage upper wall/roof · passage/0-1/0, 3.45 m | 760,804 | [L98](captures/top-displacement-03-before-l98.png) / [L99](captures/top-displacement-03-after-l99.png) / [Δ×64](captures/top-displacement-03-diff-amplified.png) |
|
||||
| `top-displacement-04` | FI · 19.80 cm · (16.000, -17.133, -668.000) | passage upper wall/roof · passage/0-1/0, 3.47 m | 729,645 | [L98](captures/top-displacement-04-before-l98.png) / [L99](captures/top-displacement-04-after-l99.png) / [Δ×64](captures/top-displacement-04-diff-amplified.png) |
|
||||
| `top-displacement-05` | SW · 19.01 cm · (20.000, 15.067, -500.000) | passage wall · passage/0-1/0, 3.51 m | 372,637 | [L98](captures/top-displacement-05-before-l98.png) / [L99](captures/top-displacement-05-after-l99.png) / [Δ×64](captures/top-displacement-05-diff-amplified.png) |
|
||||
| `top-normal-01` | VS · 49.82° · (39.696, 6.000, -820.000) | other strata rock wall outside passage envelope · passage/2-3/0, 24.47 m | 423,185 | [L98](captures/top-normal-01-before-l98.png) / [L99](captures/top-normal-01-after-l99.png) / [Δ×64](captures/top-normal-01-diff-amplified.png) |
|
||||
| `top-normal-02` | TN · 41.79° · (34.000, 5.849, -892.000) | other strata rock wall outside passage envelope · passage/2-3/0, 19.85 m | 539,369 | [L98](captures/top-normal-02-before-l98.png) / [L99](captures/top-normal-02-after-l99.png) / [Δ×64](captures/top-normal-02-diff-amplified.png) |
|
||||
| `top-normal-03` | TN · 35.51° · (38.000, 3.770, -844.000) | other strata rock wall outside passage envelope · passage/2-3/0, 23.26 m | 738,068 | [L98](captures/top-normal-03-before-l98.png) / [L99](captures/top-normal-03-after-l99.png) / [Δ×64](captures/top-normal-03-diff-amplified.png) |
|
||||
| `top-normal-04` | VS · 35.05° · (-6.234, 28.000, -824.000) | rock overhang/underside near passage · passage/2-3/0, 13.06 m | 770,128 | [L98](captures/top-normal-04-before-l98.png) / [L99](captures/top-normal-04-after-l99.png) / [Δ×64](captures/top-normal-04-diff-amplified.png) |
|
||||
| `top-normal-05` | FI · 29.91° · (-39.904, -16.000, -690.000) | rock overhang/island underside outside passage envelope · passage/0-1/0, 24.12 m | 291,241 | [L98](captures/top-normal-05-before-l98.png) / [L99](captures/top-normal-05-after-l99.png) / [Δ×64](captures/top-normal-05-diff-amplified.png) |
|
||||
|
||||
The earlier LOD0 pairs and a first LOD2 placement did not include the audited tile; the latter placed displacement-01's nearest surface 22.73 m away and failed the close-framing check. The final LOD2 tile keys match all ten audit rows and all ten image pairs pass the raw-pixel comparison. Export and render logs plus exact camera, triangle, and pixel-diff metadata are in `Saved/Linux/l99/captures-fix/`.
|
||||
@@ -0,0 +1,380 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture matched L98/L99 tunnel change and passage views."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from array import array
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/captures"
|
||||
IMAGE_DIR = Path(__file__).resolve().parent / "captures"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
LAYOUT_PATH = ROOT / "Saved/Linux/l99/resolved-sw.json"
|
||||
REACH_SUMMARY = ROOT / "Saved/Linux/l99/carve-reach/summary.json"
|
||||
BINARIES = {
|
||||
"before-l98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"after-l99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
}
|
||||
RENDERER = ROOT / "Saved/Linux/l99/bin/VoxelForgeOffscreenCapture-l99-clang"
|
||||
VOXEL_Q = 256.0
|
||||
CORE_UM_PER_VOXEL = 250_000.0
|
||||
|
||||
|
||||
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, s):
|
||||
return tuple(x * s for x in a)
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return sum(x * y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(dot(a, a))
|
||||
if length < 1.0e-8:
|
||||
raise ValueError("degenerate capture direction")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.9f}" for value in values)
|
||||
|
||||
|
||||
def angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
return (math.degrees(math.atan2(delta[0], delta[1])),
|
||||
math.degrees(math.atan2(delta[2], math.hypot(delta[0], delta[1]))))
|
||||
|
||||
|
||||
def interpolate(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(dot(delta, delta))
|
||||
if length < 1.0e-8:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
return (tuple(a[0][i] + delta[i] * t for i in range(3)),
|
||||
a[1] + (b[1] - a[1]) * t)
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate(points, distance)
|
||||
before, _ = interpolate(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate(points, min(sum(
|
||||
math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
|
||||
for a, b in zip(points, points[1:])), 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 route_length(points):
|
||||
return sum(math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0])))
|
||||
for a, b in zip(points, points[1:]))
|
||||
|
||||
|
||||
def nearest_route(points, point):
|
||||
best = None
|
||||
distance_along = 0.0
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length_sq = dot(delta, delta)
|
||||
if length_sq < 1.0e-10:
|
||||
continue
|
||||
segment_length = math.sqrt(length_sq)
|
||||
t = max(0.0, min(1.0, dot(sub(point, a[0]), delta) / length_sq))
|
||||
center = add(a[0], mul(delta, t))
|
||||
tangent = norm(delta)
|
||||
radial = sub(point, center)
|
||||
radial = sub(radial, mul(tangent, dot(radial, tangent)))
|
||||
distance = math.sqrt(dot(sub(point, center), sub(point, center)))
|
||||
if best is None or distance < best[0]:
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
best = (distance, center, tangent, radial, radius,
|
||||
distance_along + t * segment_length)
|
||||
distance_along += segment_length
|
||||
if best is None:
|
||||
raise ValueError("passage route has no non-degenerate segment")
|
||||
return best
|
||||
|
||||
|
||||
def run(name, command, headless):
|
||||
log = OUT / f"{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11"]
|
||||
if headless:
|
||||
wrapped += ["prlimit", "--as=6442450944", "--"]
|
||||
wrapped += ["timeout", "1740s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1745)
|
||||
row = {"name": name, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L99CaptureRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"capture step failed: {name}; see {log}")
|
||||
return row
|
||||
|
||||
|
||||
def nearest_surface_anchor(mesh, point):
|
||||
"""Return the nearest surface vertex and outward mesh normal from a VFMSH scene."""
|
||||
best_distance_sq = float("inf")
|
||||
best_position = best_normal = None
|
||||
with mesh.open("rb") as stream:
|
||||
magic = stream.read(8)
|
||||
has_materials = magic == b"VFMSH002"
|
||||
if magic not in (b"VFMSH001", b"VFMSH002"):
|
||||
raise RuntimeError(f"unsupported mesh scene header: {mesh}")
|
||||
version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12))
|
||||
if version != (2 if has_materials else 1):
|
||||
raise RuntimeError(f"mesh scene version mismatch: {mesh}")
|
||||
stream.seek(40, 1) # stored camera position, yaw, and pitch
|
||||
for _ in range(tile_count):
|
||||
key = struct.unpack("<4i", stream.read(16))
|
||||
surface_count, _, vertex_count, index_count, mask_count = struct.unpack(
|
||||
"<5I", stream.read(20))
|
||||
extent = (32 << key[3]) * voxel_um / 1_000_000.0
|
||||
origin = (key[0] * extent, key[1] * extent, key[2] * extent)
|
||||
if any(point[axis] < origin[axis] - 20.0 or
|
||||
point[axis] > origin[axis] + extent + 20.0 for axis in range(3)):
|
||||
stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0)
|
||||
+ index_count * 4 + mask_count, 1)
|
||||
continue
|
||||
positions = array("f")
|
||||
positions.frombytes(stream.read(vertex_count * 12))
|
||||
normals = array("f")
|
||||
normals.frombytes(stream.read(vertex_count * 12))
|
||||
if has_materials:
|
||||
stream.seek(vertex_count * 10, 1)
|
||||
stream.seek(index_count * 4 + mask_count, 1)
|
||||
if struct.pack("=I", 1) != struct.pack("<I", 1):
|
||||
positions.byteswap()
|
||||
normals.byteswap()
|
||||
for index in range(min(surface_count, vertex_count)):
|
||||
offset = index * 3
|
||||
# VFMSH stores vertex coordinates in world space; tile origin
|
||||
# is metadata for edge rendering and must not be added again.
|
||||
position = tuple(float(positions[offset + axis]) for axis in range(3))
|
||||
delta = sub(position, point)
|
||||
distance_sq = dot(delta, delta)
|
||||
if distance_sq >= best_distance_sq:
|
||||
continue
|
||||
raw_normal = tuple(float(normals[offset + axis]) for axis in range(3))
|
||||
if dot(raw_normal, raw_normal) < 1.0e-10:
|
||||
continue
|
||||
best_distance_sq = distance_sq
|
||||
best_position = position
|
||||
best_normal = norm(raw_normal)
|
||||
if best_position is None:
|
||||
raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}")
|
||||
return {"position_m": best_position, "normal": best_normal,
|
||||
"distance_m": math.sqrt(best_distance_sq)}
|
||||
|
||||
|
||||
def make_passages(layout):
|
||||
q_to_m = layout["core_voxel_size_um"] / (VOXEL_Q * 1_000_000.0)
|
||||
passages = []
|
||||
all_routes = []
|
||||
for passage in layout["passages"]:
|
||||
controls = [(tuple(float(q) * q_to_m for q in p["position_q"]),
|
||||
float(p["radius_q"]) * q_to_m)
|
||||
for p in passage["control_points"]]
|
||||
upper = tuple(float(q) * q_to_m for q in passage["upper_mouth_q"])
|
||||
lower = tuple(float(q) * q_to_m for q in passage["lower_mouth_q"])
|
||||
ui = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], upper),
|
||||
sub(controls[i][0], upper)))
|
||||
li = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], lower),
|
||||
sub(controls[i][0], lower)))
|
||||
points = controls[ui:li + 1]
|
||||
if len(points) < 2:
|
||||
raise RuntimeError(f"invalid mouth path: {passage['stable_id']}")
|
||||
upper_offset = math.sqrt(dot(sub(points[0][0], upper), sub(points[0][0], upper)))
|
||||
lower_offset = math.sqrt(dot(sub(points[-1][0], lower), sub(points[-1][0], lower)))
|
||||
length = route_length(points)
|
||||
passage_views = []
|
||||
for endpoint, distance, camera_distance, outward_sign in (
|
||||
("upper-mouth", 0.0, min(length, 7.4), -1.0),
|
||||
("lower-mouth", length, max(0.0, length - 5.5), 1.0)):
|
||||
center, _, tangent, side = route_frame(points, distance)
|
||||
camera_center, _, _, camera_side = route_frame(points, camera_distance)
|
||||
camera = (camera_center[0] + camera_side[0] * 2.5,
|
||||
camera_center[1] + camera_side[1] * 2.5, camera_center[2])
|
||||
outward = mul(tangent, outward_sign)
|
||||
target = (center[0] + outward[0] * 8.0 + side[0] * 2.5,
|
||||
center[1] + outward[1] * 8.0 + side[1] * 2.5, center[2])
|
||||
yaw, pitch = angles(camera, target)
|
||||
passage_views.append({"name": f"{passage['stable_id']}-{endpoint}",
|
||||
"kind": "mouth", "focus": center, "camera": camera, "target": target,
|
||||
"yaw": yaw, "pitch": pitch, "clip": None, "fov": 80.0,
|
||||
"focus_radius": 20.0, "passage": passage["stable_id"],
|
||||
"camera_location": "passage-air"})
|
||||
mid = length * 0.5
|
||||
center, radius, tangent, side = route_frame(points, mid)
|
||||
camera = (center[0] + side[0] * 0.75, center[1] + side[1] * 0.75,
|
||||
center[2] - radius + 1.6)
|
||||
target_pos, target_radius = interpolate(points, min(length, mid + 14.0))
|
||||
target = (target_pos[0] + side[0] * 2.35, target_pos[1] + side[1] * 2.35,
|
||||
target_pos[2] - target_radius + 1.8)
|
||||
yaw, pitch = angles(camera, target)
|
||||
passage_views.append({"name": f"{passage['stable_id']}-mid-tunnel",
|
||||
"kind": "mid-tunnel", "focus": center, "camera": camera,
|
||||
"target": target, "yaw": yaw, "pitch": pitch, "clip": None,
|
||||
"fov": 78.0, "focus_radius": 24.0, "passage": passage["stable_id"]})
|
||||
passages.append({"stable_id": passage["stable_id"], "route_length_m": length,
|
||||
"control_points": len(points), "upper_mouth_anchor_offset_m": upper_offset,
|
||||
"lower_mouth_anchor_offset_m": lower_offset, "views": passage_views})
|
||||
all_routes.append((passage["stable_id"], points))
|
||||
return passages, all_routes
|
||||
|
||||
|
||||
def make_hotspot_views(routes, reach_summary):
|
||||
views = []
|
||||
for series, rows, value_key, location_key, region_key in (
|
||||
("displacement", reach_summary["top_displacement_tiles"],
|
||||
"max_displacement_cm", "displacement_location_metres", "displacement_region"),
|
||||
("normal", reach_summary["top_normal_tiles"],
|
||||
"max_normal_degrees", "normal_location_metres", "normal_region"),
|
||||
):
|
||||
for index, row in enumerate(rows, 1):
|
||||
point = tuple(row[location_key])
|
||||
candidates = [(nearest_route(route, point), name, route)
|
||||
for name, route in routes]
|
||||
(distance, center, tangent, radial, radius, along), nearest_id, route = min(
|
||||
candidates, key=lambda item: item[0][0])
|
||||
if math.sqrt(dot(radial, radial)) < 0.25:
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
radial = (-horizontal[1], horizontal[0], 0.0)
|
||||
outward = norm(radial)
|
||||
camera = add(point, mul(outward, 20.0))
|
||||
target = point
|
||||
plane = None
|
||||
yaw, pitch = angles(camera, target)
|
||||
views.append({"name": f"top-{series}-{index:02d}", "kind": f"top-{series}",
|
||||
"focus": point, "camera": camera, "target": target,
|
||||
"yaw": yaw, "pitch": pitch, "clip": plane, "fov": 68.0,
|
||||
"focus_radius": 18.0, "strate": row["strate"],
|
||||
"value": row[value_key], "region": row[region_key],
|
||||
"camera_anchor": "nearest-surface-l98-v5", "nearest_passage": nearest_id,
|
||||
"route_distance_m": distance})
|
||||
return views
|
||||
|
||||
|
||||
def export_and_render(view, label, binary):
|
||||
safe = view["name"].replace("/", "-")
|
||||
stem = f"{safe}-{label}"
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
image = IMAGE_DIR / f"{safe}-{label}.png"
|
||||
camera, target = view["camera"], view["target"]
|
||||
yaw, pitch = angles(camera, target)
|
||||
export_args = [binary, DATA, "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--workers=8", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||
f"--camera={fmt((*camera, yaw, pitch))}", f"--mesh-export={mesh}"]
|
||||
export_row = run(f"{stem}-export", export_args, headless=True)
|
||||
export_row["scene_bytes"] = mesh.stat().st_size if mesh.exists() else 0
|
||||
if export_row["scene_bytes"] <= 0:
|
||||
raise RuntimeError(f"empty mesh scene: {mesh}")
|
||||
if label == "before-l98" and view.get("camera_anchor") == "nearest-surface-l98-v5":
|
||||
anchor = nearest_surface_anchor(mesh, view["focus"])
|
||||
camera = add(anchor["position_m"], mul(anchor["normal"], 8.0))
|
||||
view["camera"] = camera
|
||||
view["target"] = anchor["position_m"]
|
||||
view["yaw"], view["pitch"] = angles(camera, view["target"])
|
||||
view["clip"] = None
|
||||
view["surface_anchor"] = anchor
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
render_args = [RENDERER, mesh, image, "--size=1600x900",
|
||||
f"--fov-deg={view['fov']:.1f}", f"--camera-m={fmt(camera)}",
|
||||
f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on", "--tile-edges=off", "--tile-skirts=off",
|
||||
f"--height-center-m={view['focus'][2]:.6f}", "--height-range-m=10",
|
||||
f"--focus-region={fmt((*view['focus'], view['focus_radius']))}"]
|
||||
if view["clip"] is not None:
|
||||
render_args.append(f"--clip-plane={fmt(view['clip'])}")
|
||||
try:
|
||||
render_row = run(f"{stem}-render", render_args, headless=True)
|
||||
except RuntimeError:
|
||||
if view["clip"] is None:
|
||||
raise
|
||||
retry_args = [arg for arg in render_args
|
||||
if not (isinstance(arg, str) and arg.startswith("--clip-plane="))]
|
||||
render_row = run(f"{stem}-render-no-clip-retry", retry_args, headless=True)
|
||||
render_row["clip_fallback"] = True
|
||||
render_row["image"] = str(image.relative_to(ROOT))
|
||||
render_row["image_bytes"] = image.stat().st_size if image.exists() else 0
|
||||
if render_row["image_bytes"] <= 0:
|
||||
raise RuntimeError(f"empty image: {image}")
|
||||
mesh.unlink()
|
||||
return export_row, render_row
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
for path in (*BINARIES.values(), RENDERER, DATA, LAYOUT_PATH, REACH_SUMMARY):
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing input: {path}")
|
||||
layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8"))
|
||||
reach_summary = json.loads(REACH_SUMMARY.read_text(encoding="utf-8"))
|
||||
passages, routes = make_passages(layout)
|
||||
views = [view for passage in passages for view in passage["views"]]
|
||||
views += make_hotspot_views(routes, reach_summary)
|
||||
partial_path = OUT / "runs-partial.json"
|
||||
existing_records = {}
|
||||
if partial_path.is_file():
|
||||
for item in json.loads(partial_path.read_text(encoding="utf-8")):
|
||||
existing_records[item["view"]["name"]] = item
|
||||
records = []
|
||||
for view in views:
|
||||
prior = existing_records.get(view["name"])
|
||||
camera_matches = (prior is not None and
|
||||
prior["view"].get("camera_anchor") == view.get("camera_anchor"))
|
||||
if camera_matches:
|
||||
for key in ("camera", "target", "yaw", "pitch", "clip", "surface_anchor"):
|
||||
if key in prior["view"]:
|
||||
view[key] = prior["view"][key]
|
||||
if camera_matches and all(version in prior and
|
||||
(ROOT / prior[version]["render"]["image"]).is_file()
|
||||
for version in BINARIES):
|
||||
records.append(prior)
|
||||
print(f"[L99CaptureSkip] complete_pair={view['name']}", flush=True)
|
||||
continue
|
||||
pair = {"view": view}
|
||||
for label, binary in BINARIES.items():
|
||||
export_row, render_row = export_and_render(view, label, binary)
|
||||
pair[label] = {"export": export_row, "render": render_row}
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
records.append(pair)
|
||||
summary = {"source_layout_identity": layout["stable_identity"],
|
||||
"view_count": len(views), "paired_png_count": len(views) * 2,
|
||||
"passages": passages, "views": records}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L99CaptureSummary] " + json.dumps({
|
||||
"views": len(views), "paired_png_count": len(views) * 2,
|
||||
"images": [row[version]["render"]["image"] for row in records
|
||||
for version in BINARIES]}, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare the current unbounded passage carve with the L99 four-metre tube."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/carve-reach"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang"
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
SUMMARY = re.compile(r"^\[L93PassageReachSummary\] reach_m=4\.00 (.*)$", re.M)
|
||||
COVERAGE = re.compile(r"^\[L93PassageReachCoverage\] (.*)$", re.M)
|
||||
TILE = re.compile(
|
||||
r"^\[L93PassageReachTile\] reach_m=4\.00 key=\(([-\d]+),([-\d]+),([-\d]+),lod(\d+)\) "
|
||||
r"samples=(\d+) changed=(\d+) sign_flips=(\d+) moved_edges=(\d+) "
|
||||
r"max_displacement_cm=([\deE+.-]+) max_displacement_xyz_vox=\(([^)]*)\) "
|
||||
r"displacement_region=(\d+) normal_vertices=(\d+) max_normal_degrees=([\deE+.-]+) "
|
||||
r"max_normal_xyz_vox=\(([^)]*)\) normal_region=(\d+) changed_mesh=(\d+)$", re.M)
|
||||
VOXEL_SIZE_M = 0.25
|
||||
|
||||
|
||||
def xyz(text: str) -> list[float]:
|
||||
return [float(part) for part in text.split(",")]
|
||||
|
||||
|
||||
def parse_locations(output: str, strate: str) -> tuple[list[dict], list[dict]]:
|
||||
rows = []
|
||||
for match in TILE.finditer(output):
|
||||
(x, y, z, lod, samples, changed, flips, moved, displacement, displacement_xyz,
|
||||
displacement_region, normal_vertices, angle, normal_xyz, normal_region,
|
||||
changed_mesh) = match.groups()
|
||||
disp_vox = xyz(displacement_xyz)
|
||||
normal_vox = xyz(normal_xyz)
|
||||
rows.append({
|
||||
"strate": strate, "tile": [int(x), int(y), int(z), int(lod)],
|
||||
"samples": int(samples), "changed_samples": int(changed),
|
||||
"sign_flips": int(flips), "moved_edges": int(moved),
|
||||
"max_displacement_cm": float(displacement),
|
||||
"displacement_location_voxels": disp_vox,
|
||||
"displacement_location_metres": [value * VOXEL_SIZE_M for value in disp_vox],
|
||||
"displacement_region": int(displacement_region),
|
||||
"normal_vertices": int(normal_vertices), "max_normal_degrees": float(angle),
|
||||
"normal_location_voxels": normal_vox,
|
||||
"normal_location_metres": [value * VOXEL_SIZE_M for value in normal_vox],
|
||||
"normal_region": int(normal_region), "changed_mesh": bool(int(changed_mesh)),
|
||||
})
|
||||
top_displacement = sorted(rows, key=lambda row: row["max_displacement_cm"], reverse=True)[:5]
|
||||
top_normals = sorted(rows, key=lambda row: row["max_normal_degrees"], reverse=True)[:5]
|
||||
return top_displacement, top_normals
|
||||
|
||||
|
||||
def top_unique(rows: list[dict], value_key: str, location_key: str) -> list[dict]:
|
||||
chosen, seen = [], set()
|
||||
for row in sorted(rows, key=lambda item: item[value_key], reverse=True):
|
||||
if row[value_key] <= 0.0:
|
||||
continue
|
||||
location = tuple(round(value, 4) for value in row[location_key])
|
||||
if location in seen:
|
||||
continue
|
||||
seen.add(location)
|
||||
chosen.append(row)
|
||||
if len(chosen) == 5:
|
||||
break
|
||||
return chosen
|
||||
|
||||
|
||||
def run(strate: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"reach-{strate.lower()}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--audit-passage-carve-reach",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
summary_match, coverage_match = SUMMARY.search(output), COVERAGE.search(output)
|
||||
if summary_match is None or coverage_match is None or result.returncode in (-9, 124):
|
||||
raise RuntimeError(f"incomplete carve-reach audit: exit={result.returncode} {log}")
|
||||
reach = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", summary_match.group(1)))
|
||||
coverage = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", coverage_match.group(1)))
|
||||
top_displacement, top_normals = parse_locations(output, strate)
|
||||
row = {
|
||||
"strate": strate, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3), "coverage": coverage,
|
||||
"reach_4m_summary": reach,
|
||||
"reach_4m_tile_rows": len(TILE.findall(output)),
|
||||
"top_displacement_tiles": top_displacement,
|
||||
"top_normal_tiles": top_normals,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L99CarveReachRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not HEADLESS.is_file() or not DATA.is_file():
|
||||
raise SystemExit(f"missing input: {HEADLESS if not HEADLESS.is_file() else DATA}")
|
||||
if "--summarize-only" in sys.argv:
|
||||
rows = json.loads((OUT / "runs-partial.json").read_text(encoding="utf-8"))
|
||||
else:
|
||||
rows = []
|
||||
for strate in CAMERAS:
|
||||
rows.append(run(strate))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
all_displacements = [row for result in rows for row in result["top_displacement_tiles"]]
|
||||
all_normals = [row for result in rows for row in result["top_normal_tiles"]]
|
||||
top_displacement = top_unique(all_displacements, "max_displacement_cm",
|
||||
"displacement_location_metres")
|
||||
top_normals = top_unique(all_normals, "max_normal_degrees", "normal_location_metres")
|
||||
summary = {"runs": rows, "top_displacement_tiles": top_displacement,
|
||||
"top_normal_tiles": top_normals}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Three interleaved L77 passage-share censuses for L98 and L99, all four strata."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/census"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"L98": ROOT / "Saved/Linux/l98/profile-post-bin/VoxelForgeCoreHeadless-l98-post-profile",
|
||||
"L99": ROOT / "Saved/Linux/l99/profile-bin/VoxelForgeCoreHeadless-l99-profile-clang",
|
||||
}
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
SAMPLE = re.compile(
|
||||
r"^\[L77Sample\] pc=(0x\w+) lod=(\d+) .*?phase=(\S+) .*?weight=(\d+)$", re.M)
|
||||
SAMPLER = re.compile(
|
||||
r"^\[L77Sampler\] action=stop .*?samples=(\d+).*?status=PASS$", re.M)
|
||||
|
||||
|
||||
def run(strate: str, label: str, rep: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"{label.lower()}-{strate.lower()}-r{rep}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[label]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
f"--camera={camera}", "--profile-lod-work", "--linux77-profile",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
samples = [(int(lod), phase, int(weight))
|
||||
for _pc, lod, phase, weight in SAMPLE.findall(output)]
|
||||
sampler = SAMPLER.search(output)
|
||||
if result.returncode or not samples or sampler is None:
|
||||
raise RuntimeError(f"L77 census failed: exit={result.returncode} {log}")
|
||||
near = [row for row in samples if row[0] <= 2]
|
||||
passage = sum(weight for _, phase, weight in near if phase == "passages")
|
||||
total = sum(weight for _, _, weight in near)
|
||||
row = {
|
||||
"strate": strate, "binary": label, "replicate": rep,
|
||||
"exit_code": result.returncode, "wall_s": round(time.monotonic() - start, 3),
|
||||
"sampler_samples": int(sampler.group(1)), "near_lod_samples": len(near),
|
||||
"near_lod_weighted_ticks": total,
|
||||
"near_lod_passage_weighted_ticks": passage,
|
||||
"near_lod_passage_share_pct": 100.0 * passage / total,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L99CensusRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
rows = []
|
||||
orders = (("L98", "L99"), ("L99", "L98"), ("L98", "L99"))
|
||||
for rep, order in enumerate(orders, 1):
|
||||
for label in order:
|
||||
for strate in CAMERAS:
|
||||
rows.append(run(strate, label, rep))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = []
|
||||
for strate in CAMERAS:
|
||||
item = {"strate": strate, "pairs": 3}
|
||||
for label in BINARIES:
|
||||
subset = [row for row in rows if row["strate"] == strate
|
||||
and row["binary"] == label]
|
||||
values = [row["near_lod_passage_share_pct"] for row in subset]
|
||||
item[f"{label}_runs_pct"] = values
|
||||
item[f"{label}_mean_pct"] = statistics.mean(values)
|
||||
item["change_pp"] = item["L99_mean_pct"] - item["L98_mean_pct"]
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(json.dumps(
|
||||
{"runs": rows, "near_lod_passage_share": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check whole-view tile hashes across W1/W8/W12 and Clang/GCC."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/hashes"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"Clang": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
"GCC": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-gcc",
|
||||
}
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
TILE_HASH = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) "
|
||||
r"topology=(0x[0-9a-f]+)$", re.M)
|
||||
WHOLE_HASH = re.compile(
|
||||
r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) "
|
||||
r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
FIELDS = ("density", "mesh", "payload", "topology")
|
||||
|
||||
|
||||
def run(strate: str, compiler: str, workers: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"{strate.lower()}-{compiler.lower()}-w{workers}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[compiler]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--stream-hash-tiles",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
hashes = {
|
||||
(int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))):
|
||||
m.group(5, 6, 7, 8) for m in TILE_HASH.finditer(output)
|
||||
}
|
||||
whole = WHOLE_HASH.search(output)
|
||||
stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", output, re.M)
|
||||
if (result.returncode or whole is None or stamp is None
|
||||
or len(hashes) != int(whole.group(1))):
|
||||
raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}")
|
||||
row = {
|
||||
"strate": strate, "compiler": compiler, "workers": workers,
|
||||
"source_stamp": stamp.group(1), "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3), "tile_count": len(hashes),
|
||||
"tiles_by_lod": {str(lod): sum(1 for key in hashes if key[0] == lod)
|
||||
for lod in sorted({key[0] for key in hashes})},
|
||||
"whole_hash": list(whole.groups()), "tile_hashes": hashes,
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L99HashRun] " + json.dumps(
|
||||
{key: value for key, value in row.items() if key != "tile_hashes"},
|
||||
sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def compare(strate: str, reference: dict, candidate: dict) -> dict:
|
||||
keys = set(reference["tile_hashes"]) | set(candidate["tile_hashes"])
|
||||
mismatches = {
|
||||
field: sum(reference["tile_hashes"].get(key, (None,) * 4)[index]
|
||||
!= candidate["tile_hashes"].get(key, (None,) * 4)[index]
|
||||
for key in keys)
|
||||
for index, field in enumerate(FIELDS)
|
||||
}
|
||||
row = {
|
||||
"strate": strate, "reference": f"{reference['compiler']}-w{reference['workers']}",
|
||||
"candidate": f"{candidate['compiler']}-w{candidate['workers']}",
|
||||
"tile_counts_match": reference["tile_count"] == candidate["tile_count"],
|
||||
"whole_hashes_match": reference["whole_hash"] == candidate["whole_hash"],
|
||||
"tile_hashes_match": all(value == 0 for value in mismatches.values())
|
||||
and reference["tile_hashes"].keys() == candidate["tile_hashes"].keys(),
|
||||
"component_mismatch_tiles": mismatches,
|
||||
"tiles_reference": reference["tile_count"], "tiles_candidate": candidate["tile_count"],
|
||||
"reference_whole_hash": reference["whole_hash"],
|
||||
"candidate_whole_hash": candidate["whole_hash"],
|
||||
}
|
||||
print("[L99HashComparison] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
detail, comparisons = [], []
|
||||
for strate in CAMERAS:
|
||||
group = {(compiler, workers): run(strate, compiler, workers)
|
||||
for compiler in BINARIES for workers in (1, 8, 12)}
|
||||
reference = group[("Clang", 1)]
|
||||
for key, candidate in group.items():
|
||||
if key == ("Clang", 1):
|
||||
continue
|
||||
comparisons.append(compare(strate, reference, candidate))
|
||||
detail.extend({key: value for key, value in row.items() if key != "tile_hashes"}
|
||||
for row in group.values())
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(
|
||||
{"runs": detail, "comparisons": comparisons}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = {"runs": detail, "comparisons": comparisons}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0 if all(row["tile_counts_match"] and row["whole_hashes_match"]
|
||||
and row["tile_hashes_match"] for row in comparisons) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""L80 cold-readiness protocol: three interleaved L98/L99 pairs at W8 and W12."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/readiness"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARIES = {
|
||||
"L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19",
|
||||
"L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang",
|
||||
}
|
||||
# Resolved owner camera index -> stratum. Keep the labels tied to the canonical layout.
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) "
|
||||
r"cold_ready_s=([0-9.]+)$", re.M)
|
||||
|
||||
|
||||
def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log"
|
||||
camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"env", "timeout", "1740s", str(BINARIES[label]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output)
|
||||
if result.returncode or pipeline_match is None or near_match is None:
|
||||
raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}")
|
||||
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||
row = {
|
||||
"strate": strate, "workers": workers, "binary": label, "phase": phase,
|
||||
"replicate": rep, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - start, 3),
|
||||
"lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}",
|
||||
"lod02_ready_s": float(near_match.group(3)),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": pipeline["whole_tiles"].split("/")[0],
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L99ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for binary in (*BINARIES.values(), DATA):
|
||||
if not binary.is_file():
|
||||
raise SystemExit(f"missing input: {binary}")
|
||||
rows = []
|
||||
orders = (("L98", "L99"), ("L99", "L98"), ("L98", "L99"))
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
for label in BINARIES:
|
||||
run(strate, workers, label, "warmup", 0)
|
||||
for strate in CAMERAS:
|
||||
for rep, order in enumerate(orders, 1):
|
||||
for label in order:
|
||||
rows.append(run(strate, workers, label, "measured", rep))
|
||||
(OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
summary = []
|
||||
for workers in (8, 12):
|
||||
for strate in CAMERAS:
|
||||
group = [row for row in rows
|
||||
if row["workers"] == workers and row["strate"] == strate]
|
||||
item = {"workers": workers, "strate": strate, "measured_pairs": 3}
|
||||
for label in BINARIES:
|
||||
subset = [row for row in group if row["binary"] == label]
|
||||
for metric in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[metric] for row in subset]
|
||||
item[f"{label}_{metric}_runs"] = values
|
||||
item[f"{label}_{metric}_median"] = statistics.median(values)
|
||||
item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset]
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(json.dumps(
|
||||
{"runs": rows, "summary": summary}, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Count bounded-sweep candidates, outside-volume rejects, and cache lookups."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l99/sweep-counts"
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
HEADLESS = ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang"
|
||||
CAMERAS = {
|
||||
"SW": (8.0, 16.0, -372.2626953125),
|
||||
"FI": (25.8369140625, 3.19921875, -737.0),
|
||||
"TN": (19.166015625, -14.2001953125, -753.3740234375),
|
||||
"VS": (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
LINE = re.compile(r"^\[PassageComposerTile\] key=\([^,]+,[^,]+,[^,]+,lod(\d+)\) (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
COUNT_KEYS = (
|
||||
"route_candidates", "sweep_segments", "sweep_pairs", "sweep_outside_pairs",
|
||||
"sweep_min_updates", "sweep_base_samples", "sweep_detail_samples",
|
||||
"sweep_grid_samples", "sweep_no_influence_samples", "sweep_lookups",
|
||||
"sweep_cache_hits", "sweep_base_hits", "sweep_outside_hits", "sweep_missing",
|
||||
)
|
||||
|
||||
|
||||
def run(strate: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log = OUT / f"profile-sweep-{strate.lower()}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--camera={camera}",
|
||||
"--profile-passage-composer",
|
||||
]
|
||||
start = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"sweep count run failed: exit={result.returncode} {log}")
|
||||
rows = []
|
||||
for level_text, tail in LINE.findall(output):
|
||||
level = int(level_text)
|
||||
if level > 2:
|
||||
continue
|
||||
fields = dict(FIELDS.findall(tail))
|
||||
rows.append({"lod": level, **{key: int(fields.get(key, 0)) for key in COUNT_KEYS}})
|
||||
if not rows:
|
||||
raise RuntimeError(f"no profiled near-set passage tiles: {log}")
|
||||
totals = {key: sum(row[key] for row in rows) for key in COUNT_KEYS}
|
||||
row = {
|
||||
"strate": strate, "workers": 8, "near_profiled_tiles": len(rows),
|
||||
"near_tiles_by_lod": {str(level): sum(1 for item in rows if item["lod"] == level)
|
||||
for level in range(3)},
|
||||
"counters": totals, "wall_s": round(time.monotonic() - start, 3),
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L99SweepCounts] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rows = [run(strate) for strate in CAMERAS]
|
||||
(OUT / "summary.json").write_text(json.dumps({"runs": rows}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,44 @@
|
||||
# L99 matched captures
|
||||
|
||||
Each row links a before image from L98 Clang and an after image from L99 Clang. Images are 1600×900; focus coordinates are world metres.
|
||||
|
||||
| View | Focus / measured change | Before | After |
|
||||
|---|---|---|---|
|
||||
| `passage-0-1-0-upper-mouth` | passage/0-1/0 · (8.000, 16.000, -371.893) | [L98](passage-0-1-0-upper-mouth-before-l98.png) | [L99](passage-0-1-0-upper-mouth-after-l99.png) |
|
||||
| `passage-0-1-0-lower-mouth` | passage/0-1/0 · (25.837, 3.199, -733.630) | [L98](passage-0-1-0-lower-mouth-before-l98.png) | [L99](passage-0-1-0-lower-mouth-after-l99.png) |
|
||||
| `passage-0-1-0-mid-tunnel` | passage/0-1/0 · (-5.507, 20.753, -552.274) | [L98](passage-0-1-0-mid-tunnel-before-l98.png) | [L99](passage-0-1-0-mid-tunnel-after-l99.png) |
|
||||
| `passage-1-2-0-upper-mouth` | passage/1-2/0 · (19.166, -14.200, -728.831) | [L98](passage-1-2-0-upper-mouth-before-l98.png) | [L99](passage-1-2-0-upper-mouth-after-l99.png) |
|
||||
| `passage-1-2-0-lower-mouth` | passage/1-2/0 · (19.166, -14.200, -750.004) | [L98](passage-1-2-0-lower-mouth-before-l98.png) | [L99](passage-1-2-0-lower-mouth-after-l99.png) |
|
||||
| `passage-1-2-0-mid-tunnel` | passage/1-2/0 · (-8.533, 12.539, -740.857) | [L98](passage-1-2-0-mid-tunnel-before-l98.png) | [L99](passage-1-2-0-mid-tunnel-after-l99.png) |
|
||||
| `passage-2-3-0-upper-mouth` | passage/2-3/0 · (-20.062, -5.068, -809.025) | [L98](passage-2-3-0-upper-mouth-before-l98.png) | [L99](passage-2-3-0-upper-mouth-after-l99.png) |
|
||||
| `passage-2-3-0-lower-mouth` | passage/2-3/0 · (-20.062, -5.068, -896.983) | [L98](passage-2-3-0-lower-mouth-before-l98.png) | [L99](passage-2-3-0-lower-mouth-after-l99.png) |
|
||||
| `passage-2-3-0-mid-tunnel` | passage/2-3/0 · (15.521, 4.241, -854.448) | [L98](passage-2-3-0-mid-tunnel-before-l98.png) | [L99](passage-2-3-0-mid-tunnel-after-l99.png) |
|
||||
| `top-displacement-01` | VS · 25.88 cm · (18.000, -2.000, -817.035) | [L98](top-displacement-01-before-l98.png) | [L99](top-displacement-01-after-l99.png) |
|
||||
| `top-displacement-02` | TN · 21.74 cm · (26.000, -4.000, -692.765) | [L98](top-displacement-02-before-l98.png) | [L99](top-displacement-02-after-l99.png) |
|
||||
| `top-displacement-03` | SW · 20.11 cm · (-23.127, 4.000, -490.000) | [L98](top-displacement-03-before-l98.png) | [L99](top-displacement-03-after-l99.png) |
|
||||
| `top-displacement-04` | FI · 19.80 cm · (16.000, -17.133, -668.000) | [L98](top-displacement-04-before-l98.png) | [L99](top-displacement-04-after-l99.png) |
|
||||
| `top-displacement-05` | SW · 19.01 cm · (20.000, 15.067, -500.000) | [L98](top-displacement-05-before-l98.png) | [L99](top-displacement-05-after-l99.png) |
|
||||
| `top-normal-01` | VS · 49.82° · (39.696, 6.000, -820.000) | [L98](top-normal-01-before-l98.png) | [L99](top-normal-01-after-l99.png) |
|
||||
| `top-normal-02` | TN · 41.79° · (34.000, 5.849, -892.000) | [L98](top-normal-02-before-l98.png) | [L99](top-normal-02-after-l99.png) |
|
||||
| `top-normal-03` | TN · 35.51° · (38.000, 3.770, -844.000) | [L98](top-normal-03-before-l98.png) | [L99](top-normal-03-after-l99.png) |
|
||||
| `top-normal-04` | VS · 35.05° · (-6.234, 28.000, -824.000) | [L98](top-normal-04-before-l98.png) | [L99](top-normal-04-after-l99.png) |
|
||||
| `top-normal-05` | FI · 29.91° · (-39.904, -16.000, -690.000) | [L98](top-normal-05-before-l98.png) | [L99](top-normal-05-after-l99.png) |
|
||||
|
||||
## Captures (fixed)
|
||||
|
||||
The ten hotspot pairs now export their matching LOD2 audit tile. Each audit point is centered and 3.25–3.29 m from the camera; the nearest before/after surface triangles lie within 0.09 m of it. Displacement images use an oblique passage view; normal-change images use normal-color shading. Changed-pixel counts are measured on decoded 1600×900 RGB pixels (any channel differs); each difference image is absolute RGB diff amplified 64×. No pair is identical.
|
||||
|
||||
| View | Strate | Surface; nearest passage distance | Changed pixels | L98 | L99 | Abs diff ×64 |
|
||||
|---|---|---|---:|---|---|---|
|
||||
| `top-displacement-01` | VS | passage mouth; passage/2-3/0 · 3.17 m | 557,549 | [Before](top-displacement-01-before-l98.png) | [After](top-displacement-01-after-l99.png) | [Diff](top-displacement-01-diff-amplified.png) |
|
||||
| `top-displacement-02` | TN | passage upper wall/roof; passage/0-1/0 · 3.47 m | 565,587 | [Before](top-displacement-02-before-l98.png) | [After](top-displacement-02-after-l99.png) | [Diff](top-displacement-02-diff-amplified.png) |
|
||||
| `top-displacement-03` | SW | passage upper wall/roof; passage/0-1/0 · 3.45 m | 760,804 | [Before](top-displacement-03-before-l98.png) | [After](top-displacement-03-after-l99.png) | [Diff](top-displacement-03-diff-amplified.png) |
|
||||
| `top-displacement-04` | FI | passage upper wall/roof; passage/0-1/0 · 3.47 m | 729,645 | [Before](top-displacement-04-before-l98.png) | [After](top-displacement-04-after-l99.png) | [Diff](top-displacement-04-diff-amplified.png) |
|
||||
| `top-displacement-05` | SW | passage wall; passage/0-1/0 · 3.51 m | 372,637 | [Before](top-displacement-05-before-l98.png) | [After](top-displacement-05-after-l99.png) | [Diff](top-displacement-05-diff-amplified.png) |
|
||||
| `top-normal-01` | VS | other strata rock wall outside envelope; passage/2-3/0 · 24.47 m | 423,185 | [Before](top-normal-01-before-l98.png) | [After](top-normal-01-after-l99.png) | [Diff](top-normal-01-diff-amplified.png) |
|
||||
| `top-normal-02` | TN | other strata rock wall outside envelope; passage/2-3/0 · 19.85 m | 539,369 | [Before](top-normal-02-before-l98.png) | [After](top-normal-02-after-l99.png) | [Diff](top-normal-02-diff-amplified.png) |
|
||||
| `top-normal-03` | TN | other strata rock wall outside envelope; passage/2-3/0 · 23.26 m | 738,068 | [Before](top-normal-03-before-l98.png) | [After](top-normal-03-after-l99.png) | [Diff](top-normal-03-diff-amplified.png) |
|
||||
| `top-normal-04` | VS | rock overhang/underside near passage; passage/2-3/0 · 13.06 m | 770,128 | [Before](top-normal-04-before-l98.png) | [After](top-normal-04-after-l99.png) | [Diff](top-normal-04-diff-amplified.png) |
|
||||
| `top-normal-05` | FI | rock overhang/island underside outside envelope; passage/0-1/0 · 24.12 m | 291,241 | [Before](top-normal-05-before-l98.png) | [After](top-normal-05-after-l99.png) | [Diff](top-normal-05-diff-amplified.png) |
|
||||
|
||||
**Complete:** 19 matched views, 38 before/after PNGs, and 10 amplified hotspot diffs.
|
||||
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 62 KiB |