wip(assembler): round L31 -- FI recipe column audit corrected; recipe-stamped audit runner
On the real FloatingIslands recipe, column caps sit inside anchor-island rock and native bedrock; the old check sampled the route foot. The audit now samples each planned cap against the native field (negative control: a cap moved into air is detected). The audit runner records requested vs actual recipe and exits on mismatch. All 16 passage rows (4 styles x 2 recipes x 2 compilers) pass. The readable shader uses a smooth tint. Interim: FI Gradient displacement capped at width/256 (flat floor, no contour rings); restoring real undulation is L32. Evaluator 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSFYgsdW8ejZSyiMf4qRSX
This commit is contained in:
1 parent
103d8b471e
commit
28069f0d98
23 files changed
+1235
-19
No files matched your search
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -269,7 +269,8 @@ public:
|
||||
const VoxelForgeCore::ResolvedLayoutStratumV2& Lower) const
|
||||
{
|
||||
const std::int64_t width=std::max<std::int64_t>(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<std::int64_t>(0,
|
||||
Upper.bedrock.topMicrometres-Upper.bedrock.bottomMicrometres);
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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}")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -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<double,4>& ClipPlane)
|
||||
{
|
||||
@@ -472,6 +476,7 @@ void main()
|
||||
glUniform1f(glGetUniformLocation(Program,"uFar"),static_cast<float>(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<float>(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<unsigned long long>(Scene.Triangles),
|
||||
static_cast<unsigned long long>(Scene.DrawnTriangles),Scene.TileCount,
|
||||
static_cast<unsigned long long>(Scene.SkirtTriangles),
|
||||
static_cast<unsigned long long>(Scene.VisibleSkirtTriangles),
|
||||
static_cast<unsigned long long>(Scene.HiddenSameLodSkirtTriangles),
|
||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",TileEdgeWidthMeters,
|
||||
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||
TileEdges?"on":"off",TileEdgeWidthMeters,
|
||||
static_cast<unsigned long long>(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<double,3> 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<double,4> ClipPlane{};
|
||||
for(int I=3;I<argc;++I)
|
||||
{
|
||||
@@ -569,6 +575,13 @@ int main(int argc,char** argv)
|
||||
else if(Mode=="normal")ReadableMode=false;
|
||||
else {std::fprintf(stderr,"Invalid shading mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--height-tint="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(14);
|
||||
if(Mode=="on")HeightTint=true;
|
||||
else if(Mode=="off")HeightTint=false;
|
||||
else {std::fprintf(stderr,"Invalid height-tint mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--headlamp="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(11);
|
||||
@@ -610,12 +623,13 @@ int main(int argc,char** argv)
|
||||
if(HasCamera)Scene.CameraMeters=Camera;
|
||||
if(HasYaw)Scene.YawDegrees=Yaw;
|
||||
if(HasPitch)Scene.PitchDegrees=Pitch;
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||
ReadableMode?"readable":"normal",TileEdges?"on":"off",Scene.Vertices.size(),
|
||||
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||
TileEdges?"on":"off",Scene.Vertices.size(),
|
||||
static_cast<unsigned long long>(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;
|
||||
}
|
||||
|
||||
@@ -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.interiorTopMicrometres<b.interiorTopMicrometres;});
|
||||
const bool hasFloatingIslands=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||
[](const auto& stratum){return stratum.generatorKind=="FloatingIslands";});
|
||||
const bool hasSurfaceWorldPlan=std::any_of(resolvedLayout.strates.begin(),resolvedLayout.strates.end(),
|
||||
[&](const auto& stratum)
|
||||
{
|
||||
return stratum.generatorKind=="SurfaceWorld"
|
||||
&&stratum.recipeId=="SurfaceWorld.Heightfield2D"
|
||||
&&std::any_of(worldAssembly.plan.strates.begin(),worldAssembly.plan.strates.end(),
|
||||
[&](const auto& plan){return plan.strateIndex==stratum.strateIndex;});
|
||||
});
|
||||
const auto actualRecipe=hasFloatingIslands?std::string_view("floating-islands")
|
||||
:hasSurfaceWorldPlan?std::string_view("surface-world")
|
||||
:std::string_view("missing");
|
||||
const char* topGenerator=topStratum==resolvedLayout.strates.end()
|
||||
?"missing":topStratum->generatorKind.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<int>(requestedRecipe.size()),requestedRecipe.data(),
|
||||
static_cast<int>(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<int>(requestedRecipe.size()),requestedRecipe.data(),
|
||||
static_cast<int>(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)
|
||||
|
||||
@@ -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<std::array<double,2>,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<double,4>& point)
|
||||
const auto endpointSupportCount=[&](const std::array<double,4>& 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<double,4>& point)
|
||||
{return endpointSupportCount(point)>=3;};
|
||||
struct FColumnCapProbe
|
||||
{
|
||||
std::uint64_t columnSamples=0,nativeOverlapSamples=0;
|
||||
double firstOverlapDepthMetres=std::numeric_limits<double>::infinity();
|
||||
double minimumNativeDensity=std::numeric_limits<double>::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::size_t>(
|
||||
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<DirectionCount;++direction)
|
||||
{
|
||||
const double angle=2.0*3.14159265358979323846
|
||||
*direction/DirectionCount;
|
||||
const VoxelForgeCore::Vec3d sample{
|
||||
(axisX+radius*std::cos(angle))/coreMetres,
|
||||
(axisY+radius*std::sin(angle))/coreMetres,z/coreMetres};
|
||||
if(!Field.isPassageColumnSample(passageIndex,segment,sample))continue;
|
||||
++probe.columnSamples;
|
||||
const double nativeDensity=Native.densityAt(sample);
|
||||
probe.minimumNativeDensity=std::min(probe.minimumNativeDensity,
|
||||
nativeDensity);
|
||||
if(nativeDensity<0.0)
|
||||
{
|
||||
++probe.nativeOverlapSamples;
|
||||
probe.firstOverlapDepthMetres=std::min(
|
||||
probe.firstOverlapDepthMetres,depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return probe;
|
||||
};
|
||||
const auto columnSolidAt=[&](std::size_t segment,double x,double y,double z)
|
||||
{
|
||||
@@ -1122,6 +1175,9 @@ namespace StandaloneStreamBenchmark
|
||||
{x/coreMetres,y/coreMetres,z/coreMetres});
|
||||
};
|
||||
const auto crossings=Field.passageVoidCrossings();
|
||||
double controlLowestRouteZ=std::numeric_limits<double>::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<unsigned long long>(probe.columnSamples),
|
||||
static_cast<unsigned long long>(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"
|
||||
&&lowerRouteZ<controlLowestRouteZ)
|
||||
{
|
||||
controlLowestRouteZ=lowerRouteZ;
|
||||
// Move the hypothetical lower cap 20 m upward into the FI air gap.
|
||||
// Its first 2 m are still inside the original column bounds, but a
|
||||
// truncated column there has no native substrate to join.
|
||||
const double hypotheticalBottomZ=lowerRouteZ+20.0;
|
||||
const auto disconnectedControl=probeColumnCap(crossing,
|
||||
crossing.lastSegment,hypotheticalBottomZ,false,2.0);
|
||||
disconnectedEndControlApplicable=disconnectedControl.columnSamples>0;
|
||||
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<unsigned long long>(disconnectedControl.columnSamples),
|
||||
static_cast<unsigned long long>(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<int>(connectedStart)+static_cast<int>(connectedEnd),
|
||||
static_cast<int>(topCapConnected)+static_cast<int>(bottomCapConnected),
|
||||
static_cast<unsigned long long>(probeCount-crossingRouteFailures),
|
||||
static_cast<unsigned long long>(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<std::int64_t,3>& 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<unsigned long long>(columnCrossings),
|
||||
static_cast<unsigned long long>(columnConnectedEnds),
|
||||
static_cast<unsigned long long>(columnCrossings*2),
|
||||
static_cast<unsigned long long>(shortGroundedSpans),
|
||||
static_cast<unsigned long long>(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",
|
||||
|
||||
Reference in new issue
Block a user