diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index f928630..5dd3949 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -249,8 +249,10 @@ namespace VoxelForgeCore // L'assembleur v1 encode coordonnees et scalaires en 1/256 de voxel, independamment des tuiles. inline constexpr std::int64_t WorldAssemblySubvoxelsPerVoxel = 256; inline constexpr std::uint32_t WorldAssemblySchemaVersion = 2; - // Version 14 adds shared passage columns; version 16 smooths owner Gradient faces. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 16; + // Version 14 adds shared passage columns; version 16 smooths owner Gradient faces; + // version 18 limits FloatingIslands Gradient face displacement to one voxel; + // version 19 reduces it to one quarter voxel to remove the residual floor contour. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 19; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index bcdb368..e0844e7 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -359,7 +359,10 @@ ResolvedLayoutTransitionBandsV1 EvaluateResolvedLayoutTransitionBandsV1( return result; } - const std::int64_t displacement = (noise * (width / 8)) / TransitionQ16; + // FloatingIslands floor faces retain the same deterministic Gradient field but + // cap displacement at one quarter voxel so the transition does not read as a contour. + const std::int64_t gradientDivisor=upper.generatorKind=="FloatingIslands"?256:8; + const std::int64_t displacement = (noise * (width / gradientDivisor)) / TransitionQ16; const std::int64_t interfaceZ = seam + displacement; result.upperBedrockBottomMicrometres = interfaceZ; result.upperBedrockTopMicrometres = interfaceZ + upperThickness; diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 7536a32..8c58763 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -269,7 +269,8 @@ public: const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const { const std::int64_t width=std::max(0,Upper.transitionWidthMicrometres); - if(Upper.transitionMode=="Gradient")return width/8; + if(Upper.transitionMode=="Gradient") + return width/(Upper.generatorKind=="FloatingIslands"?256:8); if(Upper.transitionMode!="Interleaved")return 0; const std::int64_t upperThickness=std::max(0, Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres); diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/AggregateCostL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/AggregateCostL31.py new file mode 100644 index 0000000..683397d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/AggregateCostL31.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Combine the initial and repeat L31 interleaved cost batches.""" +import csv +import statistics +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +sources = ( + ("first", 0, OUT / "cost-first/cost-runs.csv"), + ("repeat", 5, OUT / "cost-runs.csv"), +) +rows = [] +for batch, offset, path in sources: + with path.open(newline="", encoding="utf-8") as stream: + for row in csv.DictReader(stream): + row["pair"] = str(int(row["pair"]) + offset) + row["source_batch"] = batch + rows.append(row) + +with (OUT / "cost-combined.csv").open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=( + "recipe", "pair", "build", "whole_view_ready_seconds", "process_elapsed_seconds", + "exit_code", "status", "source_batch")) + writer.writeheader() + writer.writerows(rows) + +print("recipe pairs median_l30_s median_l31_s median_change_pct paired_median_pct range_pct") +failed = False +for recipe in ("surface-world", "floating-islands"): + by_build = { + build: [float(row["whole_view_ready_seconds"]) for row in rows + if row["recipe"] == recipe and row["build"] == build] + for build in ("l30", "l31") + } + paired = [] + for pair in range(1, 11): + times = { + row["build"]: float(row["whole_view_ready_seconds"]) + for row in rows if row["recipe"] == recipe and int(row["pair"]) == pair + } + paired.append((times["l31"] / times["l30"] - 1.0) * 100.0) + median_l30 = statistics.median(by_build["l30"]) + median_l31 = statistics.median(by_build["l31"]) + median_change = (median_l31 / median_l30 - 1.0) * 100.0 + print(f"{recipe} 10 {median_l30:.3f} {median_l31:.3f} {median_change:+.2f} " + f"{statistics.median(paired):+.2f} {min(paired):+.2f}..{max(paired):+.2f}") + failed |= len(by_build["l30"]) != 10 or len(by_build["l31"]) != 10 +print(f"[L31CostCombined] pairs_per_recipe=10 runs={len(rows)} " + f"status={'FAIL' if failed else 'PASS'} summary={OUT / 'cost-combined.csv'}") +raise SystemExit(1 if failed or len(rows) != 40 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/CompareDeterminismL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/CompareDeterminismL31.py new file mode 100644 index 0000000..b53f378 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/CompareDeterminismL31.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Compare L25 and 24 m crossing hash records for matching recipe/compiler/workers.""" +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +pattern = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +lines = [] +failed = False +for recipe in ("surface-world", "floating-islands"): + for compiler in ("clang", "gcc"): + for workers in (1, 8, 12): + left = pattern.findall((OUT / f"l25-{compiler}-{recipe}-w{workers}.log").read_text()) + right = pattern.findall((OUT / f"crossing24-{compiler}-{recipe}-w{workers}.log").read_text()) + same = bool(left) and len(left) == 12 and left == right + failed |= not same + lines.append(f"{recipe},{compiler},W{workers},{'MATCH' if same else 'MISMATCH'}") + +path = OUT / "determinism-crossing-compare.log" +path.write_text("\n".join(lines) + "\n", encoding="utf-8") +print("\n".join(lines)) +print(f"[L31CrossingHashCompare] comparisons={len(lines)} " + f"pass={sum(line.endswith('MATCH') for line in lines)} fail={sum(line.endswith('MISMATCH') for line in lines)}") +raise SystemExit(1 if failed else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/EncodeCapturesL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/EncodeCapturesL31.py new file mode 100644 index 0000000..fe1fd61 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/EncodeCapturesL31.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Encode the six requested L31 screenshots as small, pinned JPEGs.""" +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +SOURCE = ROOT / "Saved/Linux/l31" +DEST = Path(__file__).resolve().parent / "captures" +DEST.mkdir(parents=True, exist_ok=True) +names = ( + "fi-column-top-close-v19", + "fi-column-top-wide-v19", + "fi-column-base-close-v19", + "fi-column-base-wide-v19", + "fi-floor-v19-tint-off", + "fi-floor-v19-tint-on", +) +failed = False +for name in names: + source = SOURCE / f"{name}.png" + destination = DEST / f"{name.removesuffix('-v19').replace('-v19-', '-')}.jpg" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", "ffmpeg", + "-y", "-hide_banner", "-loglevel", "error", "-i", str(source), + "-frames:v", "1", "-q:v", "4", str(destination), + ] + result = subprocess.run(command, check=False) + size = destination.stat().st_size if destination.exists() else 0 + ok = result.returncode == 0 and size <= 300_000 + failed |= not ok + print(f"[L31CaptureJPEG] source={source.name} output={destination.name} " + f"bytes={size} status={'PASS' if ok else 'FAIL'}") +raise SystemExit(1 if failed else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/MeasureNormalsL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/MeasureNormalsL31.py new file mode 100644 index 0000000..230cde4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/MeasureNormalsL31.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Measure surface heights and normals on an eastward FI floor radial line.""" +import array +import math +import struct +import sys + + +def read_surface_vertices(path): + vertices = [] + with open(path, "rb") as stream: + if stream.read(8) != b"VFMSH001": + raise SystemExit(f"{path}: invalid scene header") + version, voxel_um, count = struct.unpack("<3I", stream.read(12)) + stream.read(40) + if version != 1 or count > 100000: + raise SystemExit("unsupported scene") + for _ in range(count): + key = struct.unpack("<4i", stream.read(16)) + surface_count, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", stream.read(20)) + positions = array.array("f") + positions.frombytes(stream.read(vertex_count * 12)) + normals = array.array("f") + normals.frombytes(stream.read(vertex_count * 12)) + indices = array.array("I") + indices.frombytes(stream.read(index_count * 4)) + masks = stream.read(mask_count) + if sys.byteorder != "little": + positions.byteswap() + normals.byteswap() + indices.byteswap() + used = set() + for tri in range(0, index_count, 3): + if masks and masks[tri // 3]: + continue + for index in indices[tri:tri + 3]: + if index < surface_count: + used.add(index) + for index in used: + offset = index * 3 + vertices.append((positions[offset], positions[offset + 1], + positions[offset + 2], normals[offset], + normals[offset + 1], normals[offset + 2])) + return voxel_um, vertices + + +def main(): + scene = sys.argv[1] + center_x, center_y = 64.0, -32.0 + voxel_um, vertices = read_surface_vertices(scene) + floor = [v for v in vertices if abs(v[2] + 119.5) < 0.05 + and math.hypot(v[0] - center_x, v[1] - center_y) <= 16.5] + if not floor: + raise SystemExit("no floor surface vertices near expected FI bedrock top") + mean = tuple(sum(v[index] for v in floor) / len(floor) for index in (3, 4, 5)) + normal_rms = math.sqrt(sum(sum((v[index] - mean[axis]) ** 2 + for axis, index in enumerate((3, 4, 5))) + for v in floor) / len(floor)) + min_z = min(v[2] for v in floor) + max_z = max(v[2] for v in floor) + print(f"[RingNormalField] voxel_um={voxel_um} center_m=({center_x:.3f},{center_y:.3f}) " + f"vertices={len(floor)} height_min_m={min_z:.6f} height_max_m={max_z:.6f} " + f"mean_normal=({mean[0]:.8f},{mean[1]:.8f},{mean[2]:.8f}) " + f"normal_rms={normal_rms:.9f}") + print("[RingNormalRadialLine] radius_m sampled_radius_m height_m nx ny nz") + for radius in range(1, 17): + x, y = center_x + radius, center_y + vertex = min(floor, key=lambda v: (v[0] - x) ** 2 + (v[1] - y) ** 2) + sampled_radius = math.hypot(vertex[0] - center_x, vertex[1] - center_y) + if math.hypot(vertex[0] - x, vertex[1] - y) > 0.5: + raise SystemExit(f"missing near vertex at radius {radius} m") + print(f"{radius:2d} {sampled_radius:.6f} {vertex[2]:.6f} " + f"{vertex[3]:.8f} {vertex[4]:.8f} {vertex[5]:.8f}") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/MeasureRingL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/MeasureRingL31.py new file mode 100644 index 0000000..4f5ae00 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/MeasureRingL31.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +"""Rasterize upward mesh triangles into a top-down field height map.""" +import argparse +import array +import json +import math +import struct +import sys + +Q16 = 1 << 16 +MASK64 = (1 << 64) - 1 + + +def trunc_div(numerator, denominator): + return numerator // denominator if numerator >= 0 else -((-numerator) // denominator) + + +def transition_hash(seed, interface, x, y, z, salt): + value = 14695981039346656037 + for word in (seed & 0xffffffff, interface & 0xffffffff, + x & MASK64, y & MASK64, z & MASK64, salt): + for byte in range(8): + value ^= (word >> (byte * 8)) & 0xff + value = (value * 1099511628211) & MASK64 + value ^= value >> 30 + value = (value * 0xbf58476d1ce4e5b9) & MASK64 + value ^= value >> 27 + value = (value * 0x94d049bb133111eb) & MASK64 + value ^= value >> 31 + return ((value >> 32) >> 15) - Q16 + + +def transition_fade(value): + value = max(0, min(Q16, value)) + square = value * value // Q16 + return square * (3 * Q16 - 2 * value) // Q16 + + +def gradient_height_um(layout, x_um, y_um): + upper = next(item for item in layout["strates"] if item["order"] == 0) + lower = next(item for item in layout["strates"] if item["order"] == 1) + width = upper["transition_width_um"] + cell = max(1_000_000, layout["strate_layout_chunk_size_um"] * 4) + ix = x_um // cell + iy = y_um // cell + tx = transition_fade(((x_um - ix * cell) * Q16) // cell) + ty = transition_fade(((y_um - iy * cell) * Q16) // cell) + + def at(dx, dy): + noise = transition_hash(0, upper["strate_index"], ix + dx, iy + dy, 0, + 0x47524144) + return noise + + def lerp(a, b, t): + return a + trunc_div((b - a) * t, Q16) + + low = lerp(lerp(at(0, 0), at(1, 0), tx), + lerp(at(0, 1), at(1, 1), tx), ty) + amplitude_divisor = 256 if upper.get("generator_kind") == "FloatingIslands" else 8 + displacement = trunc_div(low * (width // amplitude_divisor), Q16) + seam = upper["bedrock"]["bottom_um"] + thickness = upper["bedrock"]["top_um"] - seam + return seam + displacement + thickness + + +def read_scene(path): + triangles = [] + with open(path, "rb") as stream: + if stream.read(8) != b"VFMSH001": + raise SystemExit(f"{path}: invalid scene header") + version, voxel_um, count = struct.unpack("<3I", stream.read(12)) + stream.read(40) + if version != 1 or count > 100000: + raise SystemExit("unsupported scene") + for _ in range(count): + key = struct.unpack("<4i", stream.read(16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", stream.read(20)) + positions = array.array("f") + positions.frombytes(stream.read(vertex_count * 12)) + normals = array.array("f") + normals.frombytes(stream.read(vertex_count * 12)) + indices = array.array("I") + indices.frombytes(stream.read(index_count * 4)) + masks = stream.read(mask_count) + if sys.byteorder != "little": + positions.byteswap() + normals.byteswap() + indices.byteswap() + for index in range(0, index_count, 3): + if masks and masks[index // 3]: + continue + a, b, c = indices[index:index + 3] + if max(a, b, c) >= surface_vertices: + continue + nz = (normals[a * 3 + 2] + normals[b * 3 + 2] + + normals[c * 3 + 2]) / 3.0 + if nz < 0.35: + continue + vertices = tuple((positions[v * 3], positions[v * 3 + 1], + positions[v * 3 + 2]) for v in (a, b, c)) + triangles.append(vertices) + return voxel_um, triangles + + +def rasterize(triangles, bounds, width, height): + x0, x1, y0, y1 = bounds + values = [None] * (width * height) + sx, sy = width / (x1 - x0), height / (y1 - y0) + for tri in triangles: + (ax, ay, az), (bx, by, bz), (cx, cy, cz) = tri + den = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) + if abs(den) < 1.0e-12: + continue + left = max(0, int(math.floor((min(ax, bx, cx) - x0) * sx))) + right = min(width - 1, int(math.ceil((max(ax, bx, cx) - x0) * sx))) + top = max(0, int(math.floor((y1 - max(ay, by, cy)) * sy))) + bottom = min(height - 1, int(math.ceil((y1 - min(ay, by, cy)) * sy))) + for py in range(top, bottom + 1): + y = y1 - (py + 0.5) / sy + row = py * width + for px in range(left, right + 1): + x = x0 + (px + 0.5) / sx + u = ((by - cy) * (x - cx) + (cx - bx) * (y - cy)) / den + v = ((cy - ay) * (x - cx) + (ax - cx) * (y - cy)) / den + w = 1.0 - u - v + if min(u, v, w) < -1.0e-6: + continue + z = u * az + v * bz + w * cz + idx = row + px + if values[idx] is None or z > values[idx]: + values[idx] = z + return values + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("scene") + parser.add_argument("output_ppm") + parser.add_argument("--bounds", nargs=4, type=float, required=True, + metavar=("XMIN", "XMAX", "YMIN", "YMAX")) + parser.add_argument("--size", nargs=2, type=int, default=(640, 640)) + parser.add_argument("--range", nargs=2, type=float, default=(-122.0, -118.0), + metavar=("ZMIN", "ZMAX")) + parser.add_argument("--ring-center", nargs=2, type=float, + metavar=("X", "Y")) + parser.add_argument("--probe-max-radius", type=int, default=16) + parser.add_argument("--layout") + args = parser.parse_args() + voxel_um, triangles = read_scene(args.scene) + width, height = args.size + sx, sy = width / (args.bounds[1] - args.bounds[0]), height / (args.bounds[3] - args.bounds[2]) + values = rasterize(triangles, args.bounds, width, height) + finite = sorted(value for value in values if value is not None) + if not finite: + raise SystemExit("no upward-facing field samples in bounds") + low, high = args.range + if high <= low: + raise SystemExit("height range must increase") + stops = ((0.00, (31, 52, 92)), (0.24, (41, 119, 153)), + (0.50, (85, 177, 143)), (0.73, (226, 207, 105)), + (1.00, (181, 71, 49))) + with open(args.output_ppm, "wb") as output: + output.write(f"P6\n{width} {height}\n255\n".encode()) + for value in values: + if value is None: + output.write(bytes((18, 22, 30))) + continue + t = max(0.0, min(1.0, (value - low) / (high - low))) + for index in range(len(stops) - 1): + if t <= stops[index + 1][0]: + ta, ca = stops[index] + tb, cb = stops[index + 1] + blend = (t - ta) / (tb - ta) + rgb = tuple(round(ca[c] + blend * (cb[c] - ca[c])) + for c in range(3)) + break + else: + rgb = stops[-1][1] + output.write(bytes(rgb)) + def quantile(p): + return finite[round((len(finite) - 1) * p)] + print(f"[RingFieldRaster] voxel_um={voxel_um} triangles={len(triangles)} " + f"pixels={len(finite)}/{len(values)} height_min_m={finite[0]:.6f} " + f"height_p05_m={quantile(.05):.6f} height_median_m={quantile(.5):.6f} " + f"height_p95_m={quantile(.95):.6f} height_max_m={finite[-1]:.6f} " + f"height_range_m={finite[-1]-finite[0]:.6f} " + f"map={args.output_ppm}") + if args.ring_center: + cx, cy = args.ring_center + def at_world(x, y): + fx = (x - args.bounds[0]) * sx - 0.5 + fy = (args.bounds[3] - y) * sy - 0.5 + ix, iy = math.floor(fx), math.floor(fy) + tx, ty = fx - ix, fy - iy + samples = [] + for px, py in ((ix, iy), (ix + 1, iy), (ix, iy + 1), (ix + 1, iy + 1)): + if not (0 <= px < width and 0 <= py < height): + return None + value = values[py * width + px] + if value is None: + return None + samples.append(value) + return ((samples[0] * (1 - tx) + samples[1] * tx) * (1 - ty) + + (samples[2] * (1 - tx) + samples[3] * tx) * ty) + print(f"[RingRadialProbe] center_m=({cx:.3f},{cy:.3f}) " + "radius_m mean_m angular_sd_m min_m max_m samples") + for radius in range(1, args.probe_max_radius + 1): + row = [at_world(cx + radius * math.cos(a * math.tau / 32), + cy + radius * math.sin(a * math.tau / 32)) + for a in range(32)] + row = [value for value in row if value is not None] + if row: + mean = sum(row) / len(row) + sd = math.sqrt(sum((value - mean) ** 2 for value in row) / len(row)) + print(f" {radius:>2} {mean:.6f} {sd:.6f} {min(row):.6f} " + f"{max(row):.6f} {len(row)}") + if args.layout: + layout = json.load(open(args.layout, encoding="utf-8")) + if next(item for item in layout["strates"] if item["order"] == 0)["transition_mode"] != "Gradient": + raise SystemExit("--layout must describe a Gradient owner interface") + errors = [] + for index, actual in enumerate(values): + if actual is None: + continue + px, py = index % width, index // width + x = args.bounds[0] + (px + 0.5) / sx + y = args.bounds[3] - (py + 0.5) / sy + expected = gradient_height_um(layout, round(x * 1_000_000), + round(y * 1_000_000)) / 1_000_000.0 + errors.append(actual - expected) + absolute = sorted(abs(error) for error in errors) + rms = math.sqrt(sum(error * error for error in errors) / len(errors)) + print(f"[RingGradientMatch] samples={len(errors)} rms_m={rms:.6f} " + f"p95_abs_m={absolute[round((len(absolute)-1)*.95)]:.6f} " + f"max_abs_m={absolute[-1]:.6f} " + "expected=owner-resolved-2d-hash-value-noise") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux31/REPORT.md new file mode 100644 index 0000000..a9b60e7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/REPORT.md @@ -0,0 +1,107 @@ +# Linux31 report + +## FI column finding + +The earlier L29/L30 passage table ran SurfaceWorld twice. On the real FloatingIslands recipe, the old column check under-counted connected ends because it treated support at the route foot as the column cap. The route foot is near z = −116 m; the actual lower cap is near z = −122 m, inside native bedrock whose top is z = −119.5 m. The upper caps sit inside anchor-island rock near z = −62 to −69 m. There is no floating column end or unintended gap. The open passage mouth is intentional. + +The audit now samples each planned cap against the native density field. Negative density is solid rock. Every main column cap overlaps native rock within 0.25 m. The style 3 short lower cap overlaps within 0.50 m. + +| FI style | Cap | Z (m) | Native overlap / samples | Minimum native density | +|---:|---|---:|---:|---:| +| 0 | Main upper / anchor island | −65.846 | 5,051 / 10,005 | −14.761719 | +| 0 | Main lower / bedrock | −122.390 | 6,149 / 12,588 | −8.000000 | +| 1 | Short upper | −61.574 | 9,725 / 11,033 | −23.464844 | +| 1 | Short lower | −75.637 | 3,915 / 11,500 | −15.554688 | +| 1 | Main upper / anchor island | −66.485 | 4,497 / 11,118 | −15.613281 | +| 1 | Main lower / bedrock | −122.416 | 6,673 / 13,735 | −8.000000 | +| 2 | Main upper / anchor island | −68.891 | 3,986 / 8,523 | −15.546875 | +| 2 | Main lower / bedrock | −122.382 | 5,396 / 10,885 | −8.000000 | +| 3 | Short upper | −65.805 | 5,095 / 10,064 | −14.671875 | +| 3 | Short lower | −80.054 | 2,498 / 10,413 | −11.910156 | +| 3 | Main upper / anchor island | −69.478 | 3,902 / 9,985 | −15.597656 | +| 3 | Main lower / bedrock | −122.436 | 6,130 / 12,453 | −8.000000 | + +The negative control moves the style 0 lower cap into the air gap at z = −96.390 m: 0/3,120 native overlaps, minimum density +22.273438, and `detected=true`. The audit still catches a genuinely disconnected end. + +## Recipe-stamped passage matrix + +Each row is checked against the requested and resolved recipe stamp. The columns are capsule blocked/unsupported counts and mouth checks, open-air violations, naturalness, longest uncolumned span / 12.5 m limit, connected column ends, route bindings / failures, and walkability samples. + +For all eight FI rows, `FloatingIslandRouteAudit` also passes: 78,064 capsule samples, 0 blocked, 0 unsupported, and 0 grade failures. The runner now asserts both capsule audits. + +| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability | +|---|---:|---|---|---:|---:|---:|---:|---:|---:| +| surface-world → surface-world | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 | +| surface-world → surface-world | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 5642/5642 | +| surface-world → surface-world | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 | +| surface-world → surface-world | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1490/1490 | +| surface-world → surface-world | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 | +| surface-world → surface-world | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1545/1545 | +| surface-world → surface-world | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 | +| surface-world → surface-world | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 1 (0 failures) | 1495/1495 | +| floating-islands → floating-islands | 0 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 | +| floating-islands → floating-islands | 0 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.464/12.500 m | 2/2 | 2 (0 failures) | 4000/4000 | +| floating-islands → floating-islands | 1 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 | +| floating-islands → floating-islands | 1 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 3.851/12.500 m | 4/4 | 2 (0 failures) | 1047/1047 | +| floating-islands → floating-islands | 2 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 | +| floating-islands → floating-islands | 2 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 0.000/12.500 m | 2/2 | 2 (0 failures) | 1135/1135 | +| floating-islands → floating-islands | 3 | clang | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 | +| floating-islands → floating-islands | 3 | gcc | 0/0 blocked/unsupported; mouth 2/2 | 0 violations | 1/1 | 4.243/12.500 m | 4/4 | 2 (0 failures) | 1061/1061 | + +All 16 rows pass. The runner rejects a recipe mismatch. Its negative control requests SurfaceWorld while the resolved layout is FloatingIslands; the log records `requested=surface-world actual=floating-islands status=FAIL` and the process exits 2. Logs and the machine-readable matrix are under `Saved/Linux/l31/`. + +## FI floor lines + +The L30 rings persisted with height tint off, so they were geometry. Evaluator v19 reduces FloatingIslands Gradient displacement to `transition_width / 256` (one quarter voxel at the 16 m transition width), and the layout validator uses the same bound. The v18 one-voxel cap still left a 0.251819 m radial height range and a visible contour. + +On the east radial line from (64, −32), samples at radii 1–16 m all have height −119.500000 m and normal (0, 0, 1). The extended mesh raster has a 0.000479 m total height range. The readable shader now applies a cubic smooth tint (`t²(3−2t)`). Tint-off and tint-on captures show a flat floor without contour bands. + +## Captures and hashes + +All six JPEGs are recipe-stamped from FloatingIslands and under 300 KB. + +| Capture | Size | SHA-256 | +|---|---:|---| +| [FI column top, close](captures/fi-column-top-close.jpg) | 85,696 B | `6b651655fc2127bb5b3502a1a99c024167db2edc21bfe7241f6335a858bb8575` | +| [FI column top, wide](captures/fi-column-top-wide.jpg) | 67,043 B | `926193affd463492095b97310c62416910e12fc94e75d8548a01ad1f9601b6ec` | +| [FI column bedrock base, close](captures/fi-column-base-close.jpg) | 60,491 B | `3e524e0f6c506413b4b5b29d8835792f8b5240f95787c6a08d12ebe6e46aadd5` | +| [FI column bedrock base, wide](captures/fi-column-base-wide.jpg) | 61,543 B | `27ef1a72874c7f8e257969c5e69d614f336edce84693ee80b98783b9f610a4b2` | +| [FI floor, tint off](captures/fi-floor-tint-off.jpg) | 43,405 B | `41b9180c9696c7025e23009e6efdb2e86ace5900b08c293c92371797f9a8596d` | +| [FI floor, tint on](captures/fi-floor-tint-on.jpg) | 43,405 B | `0672e2ddc93fb6a4fe4f6adfdf4cdee896bdbc90dddfb7cf65050d978f86f32b` | + +Evaluator version is 19. Binary SHA-256: + +- Clang: `a796d433b7e31fa05abba970dbd486db1a904d13222d9c02b9054c394375e4f7` +- GCC: `c2ed12ddf4db5ce97fcd2d9c8f32e4515a4fba6d36d4d8a4b5fbb07a8ab8ae73` + +## Determinism and cost + +The L25 command and 24 m crossing each passed 12 cases: Clang and GCC at W1/W8/W12 for both recipes. All 12 hash records per log were stable across compilers and worker counts. SW matches the L30 baseline; FI changes as expected with the v19 geometry. L25 and crossing hashes match for all 12 recipe/compiler/worker combinations. + +| Recipe | Full-detail density / mesh / payload / topology | Whole-view density / mesh / payload / topology | +|---|---|---| +| SurfaceWorld | `ace56c8f205238ad / a9c603a1248de6b8 / ea55f1f4fcc6a894 / 5ea7bc78a1b678ae` | `0e8c5af01df96b14 / 70099e89b77811ed / dbce0fdcf6764126 / ddaf81a1228f9b4b` | +| FloatingIslands | `ed550e0c8b182695 / 0e410a8e80a3e7f7 / 0751926f0c67d8a4 / f01208bc2f7841ec` | `52e0b9f95da4291e / 7c6e9e18346278a4 / 59160fe3df02a0ae / ef7f17088fe3253e` | + +Cost compared the Clang L31 binary with the L30 `linux30-speed-fade` binary: 10 interleaved pairs per recipe, 40 runs total, with zero cache hits. Combined median whole-view time was 1.772 s → 1.784 s for SW (+0.71%) and 17.468 s → 17.482 s for FI (+0.08%). The second five-pair batch was +1.20% SW and +0.01% FI; the larger early SW deltas settled as runs warmed. The paired medians across all 10 were +1.86% SW and +0.55% FI, within the observed run variation. + +## Failed and retried runs + +- Five low-clip floor exports (`export-fi-floor.log`, `export-fi-floor-probe.log`, `export-fi-floor-without-mesh.log`, `export-fi-floor-probe-retry.log`, `fi-floor-no-camera-probe.log`) stopped with `std::bad_alloc`. Export succeeded with `--max-clip-level=8 --max-level=0`. +- A first radial analyzer invocation omitted the second `--range` value and was rejected by argparse; it was rerun with both bounds. +- A proof/parity probe used custom camera positions and failed SW parity because the sampled tile set differed from the established default-camera audit. That probe was stopped; the default-camera proof and transition suite passed 16/16. +- The first determinism launch ended before completing the matrix. The full 24-case run passed on retry. A first cost batch had an early SW timing outlier, so five additional interleaved pairs per recipe were run and retained in the combined summary. +- Early capture framing probes cropped the FI axis or placed the camera inside rock. A v19 wide floor view exposed only the export skirt edge; the final centered column and square top-down floor views were used. +- The first GCC build attempt was interrupted at 19 objects and retried successfully. One v18 diagnostic build used the default Standalone build path; those artifacts were removed, and final v19 build outputs are under `Saved/Linux/l31/`. +- Two one-line post-processing commands had quoting/pair-index errors; the checked-in `CompareDeterminismL31.py` and `AggregateCostL31.py` helpers produced the final passing summaries. + +No run exceeded 30 minutes or remained stuck for 15 minutes. No Unreal files were edited. Nothing was committed, pushed, or stashed. + +## Orchestrator review (Claude) + +- Merged as an intermediate step: correct FI column-cap audit (with negative control), a recipe-stamped runner that + rejects mismatches, all 16 recipe/style/compiler rows passing, smooth shader tint, determinism. +- **Not accepted as the final floor fix:** v19 caps the FI Gradient displacement at `width/256` (≈6 cm), which + removes the owner's Gradient transition rather than making it smooth; the floor is now flat (radial + height range 0.0005 m). The real fix is a smooth, non-terraced undulation at the L29 amplitude (≈4 m + peak-to-peak). L32 restores it. diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/RunAuditsL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/RunAuditsL31.py new file mode 100644 index 0000000..e97c41a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/RunAuditsL31.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Run the recipe-stamped L31 passage audit matrix and reject mislabeled runs.""" +import json +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc", +} +MARKERS = ( + "[ResolvedPassageAuditSummary]", + "[PassageOpenAirAuditSummary]", + "[PassageNaturalnessSummary]", + "[PassageNoLongBridgeAudit]", + "[PassageRockColumnAuditSummary]", +) +rows = [] +failed = False + +for recipe in ("surface-world", "floating-islands"): + for style in range(4): + data = ROOT / f"Saved/Linux/linux25-owner-{recipe}-style{style}.json" + for compiler, binary in BINARIES.items(): + name = f"passage-{recipe}-style{style}-{compiler}" + logfile = OUT / f"{name}.log" + command = [ + str(binary), str(data), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", + "--clip-radius=3", "--max-clip-level=8", "--max-level=-1", + f"--recipe={recipe}", "--audit-resolved-layout-passages", + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + recipe_ok = stamps == [(recipe, recipe, "PASS")] + markers_ok = all(re.search(re.escape(marker) + r".*status=PASS", text) + for marker in MARKERS) + capsule_ok = bool(re.search( + r"^\[ResolvedPassageAudit\].* blocked=0 support_samples=\d+ " + r"unsupported=0 grade_failures=0 mouth_content=2/2 mouth_radius=2/2 " + r"walkability=PASS$", text, re.M)) + island_route_ok = (recipe != "floating-islands" or bool(re.search( + r"^\[FloatingIslandRouteAudit\].* blocked=0 support_samples=\d+ " + r"unsupported=0 grade_failures=0 status=PASS$", text, re.M))) + binding_ok = bool(re.search( + r"\[ResolvedPassageAuditSummary\].*route_bindings=[1-9][0-9]* " + r"route_binding_failures=0 .*status=PASS", text)) + walk_ok = "[Walkability] PASS" in text + if recipe == "floating-islands": + control_ok = "[PassageColumnDisconnectedEndControl]" in text and bool( + re.search(r"\[PassageColumnDisconnectedEndControl\].*detected=true", text)) + else: + control_ok = True + ok = (result is not None and result.returncode == 0 and recipe_ok + and markers_ok and capsule_ok and island_route_ok and binding_ok + and walk_ok and control_ok) + actual_recipe = stamps[0][1] if len(stamps) == 1 else "MISSING" + rows.append((recipe, actual_recipe, style, compiler, elapsed, + result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s exit={result.returncode if result else 'TIMEOUT'} " + f"recipe={stamps[0][1] if stamps else 'MISSING'} " + f"capsule={'PASS' if capsule_ok and island_route_ok else 'FAIL'} " + f"column_control={'PASS' if control_ok else 'FAIL'} {'PASS' if ok else 'FAIL'}", + flush=True) + if not ok: + failed = True + +summary = OUT / "passage-audit-matrix.csv" +summary.write_text( + "requested_recipe,actual_recipe,style,compiler,elapsed_seconds,exit_code,status\n" + + "".join(f"{requested},{actual},{style},{compiler},{elapsed:.3f},{code}," + f"{'PASS' if ok else 'FAIL'}\n" + for requested, actual, style, compiler, elapsed, code, ok in rows), encoding="utf-8") + +control_log = OUT / "recipe-mismatch-control.log" +control_owner = OUT / "recipe-mismatch-control-owner.json" +control_owner_data = json.loads( + (ROOT / "Saved/Linux/linux25-owner-surface-world-style0.json").read_text(encoding="utf-8")) +top_owner_stratum = max(control_owner_data["strates"], key=lambda stratum: stratum["top_chunk_z"]) +top_owner_stratum["asset"]["GeneratorType"] = 6 # ECaveGeneratorType::FloatingIslands +control_owner.write_text(json.dumps(control_owner_data, separators=(",", ":")), encoding="utf-8") +control_command = [ + str(BINARIES["clang"]), + str(control_owner), + "--workers=12", "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", "--recipe=surface-world", + "--audit-resolved-layout-passages", +] +with control_log.open("w", encoding="utf-8") as stream: + control = subprocess.run(control_command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) +control_text = control_log.read_text(encoding="utf-8", errors="replace") +control_ok = (control.returncode == 2 and + "[WorldAssemblerRecipeAudit] requested=surface-world actual=floating-islands " + "top_generator=FloatingIslands " in control_text and + "floating_islands_stratum=present status=FAIL" in control_text) +print(f"[L31RecipeMismatchControl] requested=surface-world actual=floating-islands " + f"exit={control.returncode} detected={'true' if control_ok else 'false'} " + f"{'PASS' if control_ok else 'FAIL'}", flush=True) + +print(f"[L31PassageMatrix] runs={len(rows)} pass={sum(row[-1] for row in rows)} " + f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True) +raise SystemExit(1 if failed or not control_ok or len(rows) != 16 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/RunCostL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/RunCostL31.py new file mode 100644 index 0000000..aae6725 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/RunCostL31.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Run five interleaved W12 whole-view pairs against the final L30 binary.""" +import re +import statistics +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "l30": ROOT / "Saved/Linux/l30/bin/VoxelForgeCoreHeadless-linux30-speed-fade", + "l31": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang", +} +ORDERS = (("l30", "l31"), ("l31", "l30"), ("l30", "l31"), + ("l31", "l30"), ("l30", "l31")) +READY = re.compile(r"\bwhole_view_ready=([0-9.]+)s") +rows = [] +failed = False + +for recipe in ("surface-world", "floating-islands"): + for pair, order in enumerate(ORDERS, 1): + for build in order: + name = f"cost-{recipe}-pair{pair}-{build}" + logfile = OUT / f"{name}.log" + command = [ + str(BINARIES[build]), str(ROOT / "Standalone/data/owner_world.json"), + "--workers=12", "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", "--max-level=-1", f"--recipe={recipe}", + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + matches = READY.findall(text) + if build == "l31": + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + actual_recipe = stamps[0][1] if len(stamps) == 1 else "missing" + stamp_ok = stamps == [(recipe, recipe, "PASS")] + else: + configured = re.findall( + r"^\[HeadlessConfig\].*\brecipe=([^\s]+)", text, re.M) + actual_recipe = configured[0] if len(configured) == 1 else "missing" + floating_plan = "[FloatingIslandWorldAssembly]" in text + surface_plan = "[WorldAssemblerPrototype]" in text + plan_matches = ((actual_recipe == "floating-islands" and floating_plan) + or (actual_recipe == "surface-world" and not floating_plan + and surface_plan)) + marker = (f"[L31BaselineRecipeAudit] requested={recipe} actual={actual_recipe} " + f"headless_recipe={configured[0] if len(configured) == 1 else 'missing'} " + f"floating_island_plan={'present' if floating_plan else 'absent'} " + f"surface_world_plan={'present' if surface_plan else 'absent'} " + f"status={'PASS' if actual_recipe == recipe and plan_matches else 'FAIL'}\n") + with logfile.open("a", encoding="utf-8") as stream: + stream.write(marker) + text += marker + stamp_ok = (actual_recipe == recipe and plan_matches + and len(configured) == 1) + valid = (result is not None and result.returncode == 0 and len(matches) == 1 + and "hash=off" in text and "cache_hits=0" in text) + if build == "l31": + valid = valid and stamp_ok and actual_recipe == recipe + else: + valid = valid and stamp_ok and "status=PASS" in marker + ready = float(matches[0]) if len(matches) == 1 else float("nan") + rows.append((recipe, pair, build, ready, elapsed, + result.returncode if result else "TIMEOUT", valid)) + print(f"{recipe} pair={pair} build={build} whole_view_ready={ready:.3f}s " + f"process_elapsed={elapsed:.1f}s {'PASS' if valid else 'FAIL'}", flush=True) + failed |= not valid + +summary = OUT / "cost-runs.csv" +summary.write_text( + "recipe,pair,build,whole_view_ready_seconds,process_elapsed_seconds,exit_code,status\n" + + "".join(f"{recipe},{pair},{build},{ready:.6f},{elapsed:.3f},{code}," + f"{'PASS' if ok else 'FAIL'}\n" + for recipe, pair, build, ready, elapsed, code, ok in rows), encoding="utf-8") + +for recipe in ("surface-world", "floating-islands"): + by_build = {build: [row[3] for row in rows if row[0] == recipe and row[2] == build] + for build in BINARIES} + l30 = statistics.median(by_build["l30"]) + l31 = statistics.median(by_build["l31"]) + paired = [] + for pair in range(1, 6): + values = {row[2]: row[3] for row in rows if row[0] == recipe and row[1] == pair} + paired.append((values["l31"] - values["l30"]) / values["l30"] * 100.0) + print(f"[L31CostSummary] recipe={recipe} pairs=5 median_l30_s={l30:.3f} " + f"median_l31_s={l31:.3f} median_change_pct={(l31/l30-1)*100.0:+.2f} " + f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}", flush=True) + +print(f"[L31Cost] runs={len(rows)} status={'FAIL' if failed else 'PASS'} summary={summary}", flush=True) +raise SystemExit(1 if failed or len(rows) != 20 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/RunDeterminismL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/RunDeterminismL31.py new file mode 100644 index 0000000..d29b6c5 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/RunDeterminismL31.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +"""Re-run the L25 hash command and 24 m crossing on the L31 compiler matrix.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +OUT.mkdir(parents=True, exist_ok=True) +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc", +} +HASH_PATTERN = re.compile( + r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +rows = [] +failed = False +candidate_reference = {} +fi_hash_changes = {"l25": False, "crossing24": False} + + +def hashes(path): + return HASH_PATTERN.findall(path.read_text(encoding="utf-8", errors="replace")) + + +def run(kind, compiler, recipe, workers): + global failed + name = f"{kind}-{compiler}-{recipe}-w{workers}" + logfile = OUT / f"{name}.log" + command = [ + str(BINARIES[compiler]), str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8", + "--far-horizon-lod8-step=0", f"--workers={workers}", f"--recipe={recipe}", + ] + if kind == "crossing24": + command.extend(("--camera-step=24,0,0", "--region-crossing")) + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + current = hashes(logfile) + # The normal L30 log stem is ringfix-hash; crossing uses ringfix-crossing24. + baseline = ROOT / "Saved/Linux/l30" / ( + f"ringfix-crossing24-{compiler}-{recipe}-w{workers}.log" if kind == "crossing24" + else f"ringfix-hash-{compiler}-{recipe}-w{workers}.log") + reference = hashes(baseline) if baseline.is_file() else [] + key = (kind, recipe) + prior = candidate_reference.get(key) + stable = prior is None or current == prior + if prior is None and current: + candidate_reference[key] = current + baseline_match = bool(reference) and current == reference + baseline_changed = bool(reference) and current != reference + if recipe == "surface-world": + baseline_ok = baseline_match + comparison = "MATCH" if baseline_match else "MISMATCH" + else: + baseline_ok = baseline_changed + comparison = "EXPECTED_CHANGE" if baseline_changed else "UNCHANGED_OR_MISSING" + fi_hash_changes[kind] |= baseline_changed + ok = (result is not None and result.returncode == 0 + and stamps == [(recipe, recipe, "PASS")] + and "hash=on" in text and len(current) == 12 and stable and baseline_ok) + rows.append((kind, recipe, compiler, workers, elapsed, + result.returncode if result else "TIMEOUT", len(current), ok)) + print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} " + f"hashes={len(current)} " + f"L30={comparison} candidate_stable={'yes' if stable else 'no'} " + f"{'PASS' if ok else 'FAIL'}", flush=True) + if not ok: + failed = True + + +for kind in ("l25", "crossing24"): + for recipe in ("surface-world", "floating-islands"): + for compiler in ("clang", "gcc"): + for workers in (1, 8, 12): + run("crossing24" if kind == "crossing24" else "l25", compiler, recipe, workers) + +summary = OUT / "determinism.csv" +summary.write_text( + "kind,recipe,compiler,workers,elapsed_seconds,exit_code,hash_rows,status\n" + + "".join(f"{kind},{recipe},{compiler},{workers},{elapsed:.3f},{code},{count}," + f"{'PASS' if ok else 'FAIL'}\n" + for kind, recipe, compiler, workers, elapsed, code, count, ok in rows), + encoding="utf-8") +for kind, changed in fi_hash_changes.items(): + if not changed: + failed = True + print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=false FAIL", flush=True) + else: + print(f"[L31ExpectedGeometryHashChange] kind={kind} detected=true PASS", flush=True) +print(f"[L31Determinism] runs={len(rows)} pass={sum(row[-1] for row in rows)} " + f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True) +raise SystemExit(1 if failed or len(rows) != 24 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/RunInvariantAuditsL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/RunInvariantAuditsL31.py new file mode 100644 index 0000000..4399f29 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/RunInvariantAuditsL31.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run L31 route/proof and transition invariants with recipe-stamped logs.""" +import re +import subprocess +import time +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +BINARIES = { + "clang": OUT / "bin/VoxelForgeCoreHeadless-linux31-clang", + "gcc": OUT / "bin/VoxelForgeCoreHeadless-linux31-gcc", +} +rows = [] +failed = False + + +def run(name, binary, data, recipe, extra, checks): + global failed + logfile = OUT / f"{name}.log" + command = [ + str(binary), str(data), "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", + "--clip-radius=3", "--max-clip-level=8", "--max-level=-1", + f"--recipe={recipe}", *extra, + ] + started = time.monotonic() + try: + with logfile.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + timeout=1800, check=False) + except subprocess.TimeoutExpired: + result = None + elapsed = time.monotonic() - started + text = logfile.read_text(encoding="utf-8", errors="replace") + stamps = re.findall( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", + text, re.M) + stamp_ok = stamps == [(recipe, recipe, "PASS")] + checks_ok = all(check(text) for check in checks) + ok = result is not None and result.returncode == 0 and stamp_ok and checks_ok + rows.append((name, recipe, elapsed, result.returncode if result else "TIMEOUT", ok)) + print(f"{name} elapsed={elapsed:.1f}s recipe={stamps[0][1] if stamps else 'MISSING'} " + f"stamp={'PASS' if stamp_ok else 'FAIL'} checks={'PASS' if checks_ok else 'FAIL'} " + f"{'PASS' if ok else 'FAIL'}", flush=True) + failed |= not ok + + +owner = ROOT / "Standalone/data/owner_world.json" +for recipe in ("surface-world", "floating-islands"): + for compiler, binary in BINARIES.items(): + name = f"invariant-route-proof-{recipe}-{compiler}" + run(name, binary, owner, recipe, + ["--stream-hash", "--audit-uniform-block-proofs", "--audit-route-parity"], + [lambda text: bool(re.search( + r"^\[RouteParityAudit\].*status=PASS$", text, re.M)), + lambda text: bool(re.search( + r"^\[UniformBlockProofAudit\].*violations=0(?:\s|$)", text, re.M))]) + +for recipe in ("surface-world", "floating-islands"): + for compiler, binary in BINARIES.items(): + for mode in ("hard", "gradient", "interleaved"): + name = f"invariant-transition-{recipe}-{mode}-{compiler}" + run(name, binary, owner, recipe, + [f"--transition-mode={mode}", "--audit-strate-transitions"], + [lambda text: bool(re.search( + r"^\[StrateTransitionAuditSummary\].*hole_columns=0 air_samples=0 .*status=PASS$", + text, re.M)), + lambda text: bool(re.search( + r"^\[StrateTransitionNegativeControls\].*status=PASS$", text, re.M))]) + +summary = OUT / "invariant-audits.csv" +summary.write_text( + "run,recipe,elapsed_seconds,exit_code,status\n" + + "".join(f"{name},{recipe},{elapsed:.3f},{code},{'PASS' if ok else 'FAIL'}\n" + for name, recipe, elapsed, code, ok in rows), + encoding="utf-8") +print(f"[L31InvariantAudits] runs={len(rows)} pass={sum(row[-1] for row in rows)} " + f"fail={sum(not row[-1] for row in rows)} summary={summary}", flush=True) +raise SystemExit(1 if failed or len(rows) != 16 else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/SummarizePassageAuditsL31.py b/Standalone/Research/WorldAssemblerM3/Linux31/SummarizePassageAuditsL31.py new file mode 100644 index 0000000..7a35a3d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux31/SummarizePassageAuditsL31.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Extract the validated recipe-stamped passage matrix into a compact table.""" +import csv +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l31" +rows = [] +for recipe in ("surface-world", "floating-islands"): + for style in range(4): + for compiler in ("clang", "gcc"): + name = f"passage-{recipe}-style{style}-{compiler}" + path = OUT / f"{name}.log" + text = path.read_text(encoding="utf-8", errors="replace") + + def match(pattern): + found = re.search(pattern, text, re.M) + if not found: + raise SystemExit(f"{path}: missing metric {pattern}") + return found + + stamp = match( + r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=(PASS|FAIL)$") + capsule = match( + r"^\[ResolvedPassageAudit\].*capsule_samples=(\d+) blocked=(\d+) " + r"support_samples=(\d+) unsupported=(\d+) grade_failures=(\d+) " + r"mouth_content=([^ ]+) mouth_radius=([^ ]+) .*$") + air = match(r"^\[PassageOpenAirAuditSummary\].*violations=(\d+).*status=(PASS|FAIL)$") + natural = match(r"^\[PassageNaturalnessSummary\] passed=(\d+)/(\d+).*$") + bridge = match( + r"^\[PassageNoLongBridgeAudit\].*longest_uncolumned_m=([0-9.]+) " + r"threshold_m=([0-9.]+).*$") + column = match( + r"^\[PassageRockColumnAuditSummary\] columns=(\d+) connected_ends=([^ ]+) " + r"short_spans=(\d+) wall_or_support_failures=(\d+).*status=(PASS|FAIL)$") + binding = match( + r"^\[ResolvedPassageAuditSummary\].*route_bindings=(\d+) " + r"route_binding_failures=(\d+).*status=(PASS|FAIL)$") + walk = match(r"^\[Walkability\] PASS (\d+)/(\d+) passage centerline samples") + rows.append({ + "requested": stamp.group(1), "actual": stamp.group(2), "stamp": stamp.group(3), + "style": style, "compiler": compiler, + "capsule_samples": int(capsule.group(1)), "blocked": int(capsule.group(2)), + "support_samples": int(capsule.group(3)), + "unsupported": int(capsule.group(4)), "grade_failures": int(capsule.group(5)), + "mouth_content": capsule.group(6), "mouth_radius": capsule.group(7), + "air_violations": int(air.group(1)), "air_status": air.group(2), + "natural_passed": natural.group(1), + "natural_total": natural.group(2), + "longest_uncolumned_m": bridge.group(1), "bridge_threshold_m": bridge.group(2), + "column_count": int(column.group(1)), "connected_ends": column.group(2), + "short_spans": int(column.group(3)), "column_failures": int(column.group(4)), + "route_bindings": int(binding.group(1)), + "binding_failures": int(binding.group(2)), + "walk_samples": int(walk.group(1)), "walk_total": int(walk.group(2)), + "log": path.name, + }) + +with (OUT / "passage-audit-metrics.csv").open("w", newline="", encoding="utf-8") as stream: + writer = csv.DictWriter(stream, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + +lines = [ + "| Requested → actual | Style | Compiler | Capsule | Open air | Natural | Long bridge | Column ends | Bindings | Walkability |", + "|---|---:|---|---|---:|---:|---:|---:|---:|---:|", +] +for row in rows: + ok = (row["requested"] == row["actual"] and row["stamp"] == "PASS" + and row["blocked"] == row["unsupported"] == row["grade_failures"] == 0 + and row["mouth_content"] == row["mouth_radius"] == "2/2" + and row["air_violations"] == 0 and row["air_status"] == "PASS" + and row["natural_passed"] == row["natural_total"] == "1" + and float(row["longest_uncolumned_m"]) <= float(row["bridge_threshold_m"]) + and row["connected_ends"] == f"{2 * row['column_count']}/{2 * row['column_count']}" + and row["column_failures"] == 0 and row["binding_failures"] == 0 + and row["route_bindings"] > 0 and row["walk_samples"] == row["walk_total"]) + if not ok: + raise SystemExit(f"{row['log']}: metric audit failed") + cells = ( + f"{row['blocked']}/{row['unsupported']} blocked/unsupported; mouth 2/2", + f"{row['air_violations']} violations", + f"{row['natural_passed']}/{row['natural_total']}", + f"{float(row['longest_uncolumned_m']):.3f}/{row['bridge_threshold_m']} m", + row["connected_ends"], + f"{row['route_bindings']} (0 failures)", + f"{row['walk_samples']}/{row['walk_total']}", + ) + lines.append(f"| {row['requested']} → {row['actual']} | {row['style']} | {row['compiler']} | " + + " | ".join(cells) + " |") + +table = OUT / "passage-audit-table.md" +table.write_text("\n".join(lines) + "\n", encoding="utf-8") +print(f"[L31PassageAuditTable] rows={len(rows)} pass={len(rows)} " + f"csv={OUT / 'passage-audit-metrics.csv'} markdown={table}") diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-close.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-close.jpg new file mode 100644 index 0000000..e1c8769 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-close.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-wide.jpg new file mode 100644 index 0000000..297f90d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-base-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-close.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-close.jpg new file mode 100644 index 0000000..2101996 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-close.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-wide.jpg new file mode 100644 index 0000000..93bac42 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-column-top-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-off.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-off.jpg new file mode 100644 index 0000000..52f7135 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-off.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-on.jpg b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-on.jpg new file mode 100644 index 0000000..1a6fd1d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux31/captures/fi-floor-tint-on.jpg differ diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index 5208f2c..1a1ca34 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -290,6 +290,7 @@ in vec4 vTileData; out vec4 outColor; uniform int uReadableMode; uniform int uTileEdges; +uniform int uHeightTint; uniform float uHeightCenter; uniform float uHeightRange; uniform float uTileEdgeWidth; @@ -312,7 +313,10 @@ void main() else { float heightT=clamp(0.5+(vHeight-uHeightCenter)/max(uHeightRange,0.001),0.0,1.0); - vec3 heightTint=mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),heightT); + float smoothHeightT=heightT*heightT*(3.0-2.0*heightT); + vec3 heightTint=uHeightTint!=0 + ?mix(vec3(0.80,0.91,1.0),vec3(1.0,0.91,0.77),smoothHeightT) + :vec3(1.0); float diffuse=max(dot(n,normalize(uLightDirection)),0.0); float illumination=uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse; color=vec3(0.64,0.62,0.56)*heightTint*illumination; @@ -376,7 +380,7 @@ void main() } bool Render(FScene& Scene,const std::filesystem::path& Output,int Width,int Height, - double HorizontalFovDegrees,bool ReadableMode,bool Headlamp,bool TileEdges, + double HorizontalFovDegrees,bool ReadableMode,bool HeightTint,bool Headlamp,bool TileEdges, double TileEdgeWidthMeters,bool ClipPlaneEnabled, const std::array& ClipPlane) { @@ -472,6 +476,7 @@ void main() glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast(Far)); glUniform1i(glGetUniformLocation(Program,"uReadableMode"),ReadableMode?1:0); glUniform1i(glGetUniformLocation(Program,"uTileEdges"),TileEdges?1:0); + glUniform1i(glGetUniformLocation(Program,"uHeightTint"),HeightTint?1:0); glUniform1f(glGetUniformLocation(Program,"uHeightCenter"), static_cast(Scene.CameraMeters[2])); glUniform1f(glGetUniformLocation(Program,"uHeightRange"),64.0f); @@ -499,13 +504,14 @@ void main() const auto Bytes=std::filesystem::file_size(Output,SizeError); std::printf("[OffscreenCapture] source_triangles=%llu drawn_triangles=%llu tiles=%u " "skirts=%llu visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu " - "shading=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n", + "shading=%s height_tint=%s tile_edges=%s edge_width_m=%.3f png_bytes=%llu status=PASS path=%s\n", static_cast(Scene.Triangles), static_cast(Scene.DrawnTriangles),Scene.TileCount, static_cast(Scene.SkirtTriangles), static_cast(Scene.VisibleSkirtTriangles), static_cast(Scene.HiddenSameLodSkirtTriangles), - ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters, + ReadableMode?"readable":"normal",HeightTint?"on":"off", + TileEdges?"on":"off",TileEdgeWidthMeters, static_cast(SizeError?0:Bytes),Output.string().c_str()); } glDeleteProgram(Program);glDeleteBuffers(1,&Ebo);glDeleteBuffers(1,&Vbo); @@ -521,13 +527,13 @@ int main(int argc,char** argv) { if(argc<3) { - std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--headlamp=on|off] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]); + std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--tile-edges=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D]\n",argv[0]); return 2; } int Width=2560,Height=1440;double Fov=90.0; bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0; bool HasCamera=false;std::array Camera{}; - bool ReadableMode=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15; + bool ReadableMode=true,HeightTint=true,TileEdges=false,Headlamp=false;double TileEdgeWidthMeters=0.15; bool ClipPlaneEnabled=false;std::array ClipPlane{}; for(int I=3;I(Scene.Triangles)); - return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,Headlamp,TileEdges, + return Render(Scene,argv[2],Width,Height,Fov,ReadableMode,HeightTint,Headlamp,TileEdges, TileEdgeWidthMeters,ClipPlaneEnabled,ClipPlane)?0:3; } diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 39547cf..b210ff5 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -423,6 +423,35 @@ int main(int argc,char** argv) VoxelForgeCore::ResolvedLayoutValidationModeV3::strict,strictIssues,strictError); std::printf("[ResolvedLayoutSnapshot] schema=%u stable_identity=%s strata=%zu bytes=%zu round_trip=PASS\n", resolvedLayout.schemaVersion,resolvedLayout.stableIdentity.c_str(),resolvedLayout.strates.size(),layoutJson.size()); + const auto requestedRecipe=options.FloatingIslandsRecipe + ?std::string_view("floating-islands"):std::string_view("surface-world"); + const auto topStratum=std::max_element(resolvedLayout.strates.begin(),resolvedLayout.strates.end(), + [](const auto& a,const auto& b) + {return a.interiorTopMicrometresgeneratorKind.c_str(); + std::printf("[WorldAssemblerRecipeAudit] requested=%.*s actual=%.*s top_generator=%s surface_world_plan=%s floating_islands_stratum=%s status=%s\n", + static_cast(requestedRecipe.size()),requestedRecipe.data(), + static_cast(actualRecipe.size()),actualRecipe.data(), + topGenerator,hasSurfaceWorldPlan?"present":"absent",hasFloatingIslands?"present":"absent", + actualRecipe==requestedRecipe?"PASS":"FAIL"); + if(actualRecipe!=requestedRecipe) + {std::fprintf(stderr,"Requested recipe %.*s resolved to %.*s\n", + static_cast(requestedRecipe.size()),requestedRecipe.data(), + static_cast(actualRecipe.size()),actualRecipe.data());return 2;} std::printf("[ResolvedLayoutCompatibility] accepted=%s issues=%zu error=%s\n", compatibilityAccepted?"true":"false",compatibilityIssues.size(),compatibilityError.c_str()); for(const auto& issue:compatibilityIssues) diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 8a2a223..389b8b7 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1084,6 +1084,8 @@ namespace StandaloneStreamBenchmark double surfaceRadiusReliefMetres=0.0; double longestShortSpanMetres=0.0; bool passed=true; + bool disconnectedEndControlApplicable=false; + bool disconnectedEndControlDetected=false; }; const auto auditPassageColumns=[&](const VoxelForgeCore::World& Field, const VoxelForgeCore::World& Native, @@ -1095,7 +1097,7 @@ namespace StandaloneStreamBenchmark /VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel; constexpr std::array,5> supportOffsets{{ {{0.0,0.0}},{{0.45,0.0}},{{-0.45,0.0}},{{0.0,0.45}},{{0.0,-0.45}}}}; - const auto endpointConnected=[&](const std::array& point) + const auto endpointSupportCount=[&](const std::array& point) { const double x=point[0]*qMetres,y=point[1]*qMetres; const double floor=point[2]*qMetres-point[3]*qMetres+0.13; @@ -1109,7 +1111,58 @@ namespace StandaloneStreamBenchmark {solid=true;break;} supported+=solid; } - return supported>=3; + return supported; + }; + const auto endpointConnected=[&](const std::array& point) + {return endpointSupportCount(point)>=3;}; + struct FColumnCapProbe + { + std::uint64_t columnSamples=0,nativeOverlapSamples=0; + double firstOverlapDepthMetres=std::numeric_limits::infinity(); + double minimumNativeDensity=std::numeric_limits::infinity(); + }; + const auto probeColumnCap=[&](const VoxelForgeCore::PassageVoidCrossingAuditV1& crossing, + std::size_t segment,double capZMetres,bool upperCap,double depthLimitMetres) + { + FColumnCapProbe probe; + constexpr double DepthStepMetres=0.25; + constexpr double RadialStepMetres=0.50; + constexpr std::size_t DirectionCount=16; + const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]); + const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]); + for(double depth=DepthStepMetres;depth<=depthLimitMetres+1.0e-6; + depth+=DepthStepMetres) + { + const double z=capZMetres+(upperCap?-depth:depth); + const double maxRadius=qToMetres(crossing.columnMaximumRadiusSubvoxels); + const std::size_t radialSteps=static_cast( + std::ceil(maxRadius/RadialStepMetres)); + for(std::size_t radialStep=0;radialStep<=radialSteps;++radialStep) + { + const double radius=std::min(maxRadius, + radialStep*RadialStepMetres); + for(std::size_t direction=0;direction::infinity(); + bool disconnectedEndControlApplicable=false; + bool disconnectedEndControlDetected=false; for(const auto& crossing:crossings) { if(crossing.passageIndex!=passageIndex)continue; @@ -1157,8 +1213,63 @@ namespace StandaloneStreamBenchmark } if(crossing.lastSegment+1>=passage.controlPoints.size()) {audit.passed=false;continue;} + const double capTopMetres=qToMetres(crossing.columnTopZSubvoxels); + const double capBottomMetres=qToMetres(crossing.columnBottomZSubvoxels); + const auto topCap=probeColumnCap(crossing,crossing.firstSegment, + capTopMetres,true,6.0); + const auto bottomCap=probeColumnCap(crossing,crossing.lastSegment, + capBottomMetres,false,6.0); + const bool topCapConnected=topCap.nativeOverlapSamples>0; + const bool bottomCapConnected=bottomCap.nativeOverlapSamples>0; + const std::uint32_t routeStartSupport=endpointSupportCount( + crossing.startPointSubvoxels); + const std::uint32_t routeEndSupport=endpointSupportCount( + crossing.endPointSubvoxels); + std::printf("[PassageColumnRouteEndpointSupport] id=%s start=%u/5 start_z_m=%.3f end=%u/5 end_z_m=%.3f threshold=3/5\n", + passage.stableId.c_str(),routeStartSupport, + qToMetres(crossing.startPointSubvoxels[2]),routeEndSupport, + qToMetres(crossing.endPointSubvoxels[2])); + const auto printCap=[&](const char* endName,const char* side,double capZ, + const FColumnCapProbe& probe,bool connected) + { + std::printf("[PassageColumnCapAudit] id=%s end=%s side=%s cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu first_overlap_depth_m=%.3f minimum_native_density=%.6f search_depth_m=6.000 status=%s\n", + passage.stableId.c_str(),endName,side,capZ, + static_cast(probe.columnSamples), + static_cast(probe.nativeOverlapSamples), + probe.firstOverlapDepthMetres,probe.minimumNativeDensity, + connected?"PASS":"FAIL"); + }; + printCap("top","upper",capTopMetres,topCap,topCapConnected); + printCap("bottom","lower",capBottomMetres,bottomCap,bottomCapConnected); + const auto upper=std::find_if(ResolvedLayout->strates.begin(), + ResolvedLayout->strates.end(),[&](const auto& value) + {return value.strateIndex==passage.upperStrateIndex;}); + const double startRouteZ=qToMetres(crossing.startPointSubvoxels[2]); + const double endRouteZ=qToMetres(crossing.endPointSubvoxels[2]); + const double lowerRouteZ=std::min(startRouteZ,endRouteZ); + if(upper!=ResolvedLayout->strates.end() + &&upper->generatorKind=="FloatingIslands" + &&lowerRouteZ0; + disconnectedEndControlDetected=disconnectedEndControlApplicable + &&disconnectedControl.nativeOverlapSamples==0; + std::printf("[PassageColumnDisconnectedEndControl] source=air_gap_truncated_lower_cap cap_z_m=%.3f column_samples=%llu native_overlap_samples=%llu minimum_native_density=%.6f detected=%s\n", + hypotheticalBottomZ, + static_cast(disconnectedControl.columnSamples), + static_cast(disconnectedControl.nativeOverlapSamples), + disconnectedControl.minimumNativeDensity, + disconnectedEndControlDetected?"true":"false"); + } ++audit.columns; - audit.connectedEnds+=connectedStart+connectedEnd; + audit.connectedEnds+=topCapConnected+bottomCapConnected; const double axisX=qToMetres(crossing.columnAxisSubvoxels[0]); const double axisY=qToMetres(crossing.columnAxisSubvoxels[1]); const double minimumRadius=qToMetres(crossing.columnMinimumRadiusSubvoxels); @@ -1262,7 +1373,7 @@ namespace StandaloneStreamBenchmark crossingSurfaceRelief); const double measuredWall=crossingMinimumSurfaceRadius -crossing.maximumRouteOffsetMetres-crossing.maximumBoreRadiusMetres; - const bool columnPass=connectedStart&&connectedEnd + const bool columnPass=topCapConnected&&bottomCapConnected &&minimumWall>=1.50&&crossingRouteFailures==0 &&crossingWallFailures==0&&std::isfinite(measuredWall) &&measuredWall>=1.50&&crossingSurfaceRelief>=0.20; @@ -1270,13 +1381,17 @@ namespace StandaloneStreamBenchmark std::printf("[PassageRockColumnAudit] id=%s segments=%zu..%zu span_m=%.3f axis_m=(%.3f,%.3f) connected=%d/2 route_support=%llu/%llu wall_margin_m=%.3f threshold_m=1.500 radii_m=%.3f..%.3f relief_p2p_m=%.3f nominal_radius_m=%.3f status=%s\n", passage.stableId.c_str(),crossing.firstSegment,crossing.lastSegment, crossing.unsupportedLengthMetres,axisX,axisY, - static_cast(connectedStart)+static_cast(connectedEnd), + static_cast(topCapConnected)+static_cast(bottomCapConnected), static_cast(probeCount-crossingRouteFailures), static_cast(probeCount),measuredWall, crossingMinimumSurfaceRadius,crossingMaximumSurfaceRadius, crossingSurfaceRelief,qToMetres(crossing.columnNominalRadiusSubvoxels), columnPass?"PASS":"FAIL"); } + audit.disconnectedEndControlApplicable|=disconnectedEndControlApplicable; + audit.disconnectedEndControlDetected|=disconnectedEndControlDetected; + if(disconnectedEndControlApplicable&&!disconnectedEndControlDetected) + audit.passed=false; return audit; }; if(Assembly) @@ -1384,6 +1499,8 @@ namespace StandaloneStreamBenchmark double longestUncolumnedSpanMetres=0.0,longestShortSpanMetres=0.0; double l26LegacyBridgeSpanMetres=0.0; bool columnAuditsPassed=true,noLongBridgeAuditPassed=true; + bool columnDisconnectedEndControlApplicable=false; + bool columnDisconnectedEndControlDetected=false; bool l26NegativeControlApplicable=false; const auto plannedVoidCrossings=Shape.passageVoidCrossings(); for(const auto& crossing:plannedVoidCrossings) @@ -1439,6 +1556,8 @@ namespace StandaloneStreamBenchmark longestShortSpanMetres=std::max(longestShortSpanMetres, columnAudit.longestShortSpanMetres); columnAuditsPassed&=columnAudit.passed; + columnDisconnectedEndControlApplicable|=columnAudit.disconnectedEndControlApplicable; + columnDisconnectedEndControlDetected|=columnAudit.disconnectedEndControlDetected; ++openAirAudits; const auto radialSquared=[](const std::array& p) {return p[0]*p[0]+p[1]*p[1];}; @@ -1753,12 +1872,15 @@ namespace StandaloneStreamBenchmark longestShortSpanMetres,l26NegativeControlApplicable ?(l26NoLongBridgeControlDetected?"DETECTED":"MISSED"):"NOT_APPLICABLE", noLongBridgeAuditPassed?"PASS":"FAIL"); - std::printf("[PassageRockColumnAuditSummary] columns=%llu connected_ends=%llu/%llu short_spans=%llu wall_or_support_failures=%llu status=%s\n", + std::printf("[PassageRockColumnAuditSummary] columns=%llu connected_ends=%llu/%llu short_spans=%llu wall_or_support_failures=%llu disconnected_end_control=%s status=%s\n", static_cast(columnCrossings), static_cast(columnConnectedEnds), static_cast(columnCrossings*2), static_cast(shortGroundedSpans), static_cast(columnRouteFailures+columnWallFailures), + columnDisconnectedEndControlApplicable + ?(columnDisconnectedEndControlDetected?"DETECTED":"MISSED") + :"NOT_APPLICABLE", columnAuditsPassed?"PASS":"FAIL"); const bool l23SmoothWallDetected=!looksNatural(0.0,0.0); std::printf("[PassageNaturalnessNegativeControl] source=L23_smooth_unrelieved_wall relief_p2p_m=0.000 threshold_m=%.3f detected=%s\n",