captures: readable player-view captures with a coverage gate (L69); R36 native-generator survey
- Capture exports now cover a 90-degree player frustum (radius 3, LOD 0-8, overlap patches); a 17x9 ray coverage probe aborts before rendering if tiles are missing (truncated-mesh control: 480 uncovered samples, rejected). Clear colour is sky blue, so un-meshed space can't pass as dark rock. 16 views of the owner's four-strate plan. - Explore commandlet: finite XYZ spawn override for traces (Source/, Linux-compiled). - In-engine four-strate trace NOT obtained: the engine-generated plan fails passage validation at a solid centerline voxel (~(-43,-34.5,-448.5) m) while the headless owner_world.json plan passes. Engine and headless plans diverge. - R36 (read-only): order TN -> VS -> FI for native migration; native FI is reachable from the adapter but not selected in the owner run, cause unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,725 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render arrival, departure, and mid-tunnel views for the four-strate owner plan."""
|
||||
import json
|
||||
import importlib.util
|
||||
import argparse
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / "Saved/Linux/l69"
|
||||
BIN = OUT / "bin"
|
||||
CAPTURES = Path(__file__).resolve().parent / "captures"
|
||||
HEADLESS = OUT / "bin/VoxelForgeCoreHeadless-l69"
|
||||
RENDERER = BIN / "VoxelForgeOffscreenCapture-l69"
|
||||
PNG_DIR = OUT / "capture-png"
|
||||
RECIPE_INPUTS = (
|
||||
("passage-0-1", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "SurfaceWorld", "upper"),
|
||||
("passage-1-2", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "FloatingIslands", "upper"),
|
||||
("passage-2-3", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world", "TunnelNetwork", "upper"),
|
||||
)
|
||||
SUBVOXELS_PER_VOXEL = 256.0
|
||||
RING_AUDIT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux32/AuditRingTerraceL32.py"
|
||||
spec = importlib.util.spec_from_file_location("ring_profile_l32", RING_AUDIT)
|
||||
ring_profile = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(ring_profile)
|
||||
|
||||
for directory in (OUT, BIN, PNG_DIR, CAPTURES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
ENV = os.environ.copy()
|
||||
ENV["LP_NUM_THREADS"] = "12"
|
||||
ENV["XDG_CACHE_HOME"] = str(OUT / "xdg-cache")
|
||||
PARSER = argparse.ArgumentParser(description=__doc__)
|
||||
PARSER.add_argument("--scene", choices=[value[0] for value in RECIPE_INPUTS])
|
||||
PARSER.add_argument("--view", choices=("arrival-mouth", "departure-mouth", "mid-tunnel"))
|
||||
OPTIONS = PARSER.parse_args()
|
||||
|
||||
|
||||
def run(name, command):
|
||||
log = OUT / f"capture-{name}.log"
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", *map(str, command)]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, env=ENV, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L69CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{name} failed; see {log}")
|
||||
return log
|
||||
|
||||
|
||||
def add(a, b):
|
||||
return tuple(x + y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def sub(a, b):
|
||||
return tuple(x - y for x, y in zip(a, b))
|
||||
|
||||
|
||||
def mul(a, scale):
|
||||
return tuple(x * scale for x in a)
|
||||
|
||||
|
||||
def norm(a):
|
||||
length = math.sqrt(sum(x * x for x in a))
|
||||
if length <= 1.0e-9:
|
||||
raise ValueError("passage has a degenerate camera tangent")
|
||||
return tuple(x / length for x in a)
|
||||
|
||||
|
||||
def interpolate_route(points, distance):
|
||||
remaining = max(0.0, distance)
|
||||
for a, b in zip(points, points[1:]):
|
||||
delta = sub(b[0], a[0])
|
||||
length = math.sqrt(sum(x * x for x in delta))
|
||||
if length <= 1.0e-9:
|
||||
continue
|
||||
if remaining <= length:
|
||||
t = remaining / length
|
||||
pos = tuple(a[0][axis] + delta[axis] * t for axis in range(3))
|
||||
radius = a[1] + (b[1] - a[1]) * t
|
||||
return pos, radius
|
||||
remaining -= length
|
||||
return points[-1]
|
||||
|
||||
|
||||
def route_frame(points, distance):
|
||||
center, radius = interpolate_route(points, distance)
|
||||
before, _ = interpolate_route(points, max(0.0, distance - 0.25))
|
||||
after, _ = interpolate_route(points, distance + 0.25)
|
||||
tangent = norm(sub(after, before))
|
||||
horizontal = norm((tangent[0], tangent[1], 0.0))
|
||||
side = (-horizontal[1], horizontal[0], 0.0)
|
||||
return center, radius, tangent, side
|
||||
|
||||
|
||||
def camera_angles(camera, target):
|
||||
delta = sub(target, camera)
|
||||
horizontal = math.hypot(delta[0], delta[1])
|
||||
yaw = math.degrees(math.atan2(delta[0], delta[1]))
|
||||
pitch = math.degrees(math.atan2(delta[2], horizontal))
|
||||
return yaw, pitch
|
||||
|
||||
|
||||
def fmt(values):
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def load_reference_cameras():
|
||||
log = ROOT / "Saved/Linux/l34/run-capture-standard-set-final-radius3.log"
|
||||
pattern = re.compile(
|
||||
r"^\[L34CaptureCamera\] view=(\S+).*?camera_m=\(([^)]*)\) "
|
||||
r"target_m=\(([^)]*)\) clip=(\S+)", re.M)
|
||||
values = {}
|
||||
for name, camera, target, clip in pattern.findall(
|
||||
log.read_text(encoding="utf-8", errors="replace")):
|
||||
parse = lambda value: tuple(float(part) for part in value.split(","))
|
||||
values[name] = (parse(camera), parse(target), None if clip == "none" else parse(clip))
|
||||
return values
|
||||
|
||||
|
||||
REFERENCE_CAMERAS = {}
|
||||
|
||||
|
||||
def terrain_surface_near(mesh, x, y, expected_height):
|
||||
bounds = (x - 2.0, x + 2.0, y - 2.0, y + 2.0)
|
||||
_, triangles, _ = ring_profile.read_scene(
|
||||
str(mesh), None, -1.0e9, 1.0e9, bounds)
|
||||
buckets = ring_profile.build_buckets(triangles, True, bounds, cell_size=1.0)
|
||||
hits = []
|
||||
for dy in range(-4, 5):
|
||||
for dx in range(-4, 5):
|
||||
sample_x, sample_y = x + dx * 0.5, y + dy * 0.5
|
||||
hit = ring_profile.sample_surface(buckets, sample_x, sample_y, True, 1.0,
|
||||
preferred_height=expected_height)
|
||||
if hit is not None:
|
||||
hits.append((math.hypot(dx * 0.5, dy * 0.5),
|
||||
abs(hit[0] - expected_height), sample_x, sample_y, hit[0]))
|
||||
if not hits:
|
||||
return None
|
||||
distance, _, sample_x, sample_y, height = min(hits)
|
||||
if distance > 2.0 or abs(height - expected_height) > 8.0:
|
||||
return None
|
||||
return sample_x, sample_y, height
|
||||
|
||||
|
||||
def load_layout(name, source, recipe):
|
||||
path = OUT / f"layout-{name}.json"
|
||||
run(f"layout-{name}", [HEADLESS, source, "--workers=12", "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--force-generate", "--clip-radius=2",
|
||||
"--max-clip-level=0", "--max-level=0", f"--recipe={recipe}",
|
||||
f"--resolved-layout-output={path}", "--resolved-layout-only"])
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def q_to_meters(layout):
|
||||
return layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
|
||||
|
||||
def q_position(value, scale):
|
||||
return tuple(float(component) * scale for component in value)
|
||||
|
||||
|
||||
def q_route(points, scale):
|
||||
return [(q_position(point["position_q"], scale), point["radius_q"] * scale)
|
||||
for point in points]
|
||||
|
||||
|
||||
def vector_length(value):
|
||||
return math.sqrt(sum(component * component for component in value))
|
||||
|
||||
|
||||
def unit(value):
|
||||
length = vector_length(value)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError("capture camera direction is degenerate")
|
||||
return tuple(component / length for component in value)
|
||||
|
||||
|
||||
def add_scaled(point, direction, scale):
|
||||
return tuple(point[axis] + direction[axis] * scale for axis in range(3))
|
||||
|
||||
|
||||
def make_player_views(layout):
|
||||
scale = q_to_meters(layout)
|
||||
passages = {(int(item["upper_strate_index"]), int(item["lower_strate_index"])): item
|
||||
for item in layout["passages"]}
|
||||
p01, p12, p23 = passages[(0, 1)], passages[(1, 2)], passages[(2, 3)]
|
||||
strate0 = next(item for item in layout["strates"] if int(item["strate_index"]) == 0)
|
||||
surface_anchors = [item["values"] for item in strate0.get("anchors", [])
|
||||
if item.get("kind") == "route_point"]
|
||||
if len(surface_anchors) < 2:
|
||||
raise RuntimeError("strate 0 has no resolved surface arrival route")
|
||||
surface_route = [(q_position(value[:3], scale), value[3] * scale)
|
||||
for value in surface_anchors]
|
||||
|
||||
route_specs = [
|
||||
(0, surface_route, True, surface_route[-1][0], p01, "upper", False),
|
||||
(1, q_route(p01["control_points"], scale), True,
|
||||
q_position(p01["lower_mouth_q"], scale), p12, "upper", True),
|
||||
(2, q_route(p12["control_points"], scale), True,
|
||||
q_position(p12["lower_mouth_q"], scale), p23, "upper", False),
|
||||
(3, q_route(p23["control_points"], scale), True,
|
||||
q_position(p23["lower_mouth_q"], scale), p23, "lower", True),
|
||||
]
|
||||
views = {}
|
||||
for strate, source_route, reverse_route, spawn, departure, departure_end, reverse_departure in route_specs:
|
||||
route = list(reversed(source_route)) if reverse_route else list(source_route)
|
||||
if vector_length(sub(route[0][0], spawn)) > 5.0:
|
||||
raise RuntimeError(f"strate {strate} spawn is {vector_length(sub(route[0][0], spawn)):.2f} m from its arrival route")
|
||||
path_length = sum(vector_length(sub(b[0], a[0])) for a, b in zip(route, route[1:]))
|
||||
arrival_target_pos, arrival_target_radius = interpolate_route(route, min(20.0, path_length))
|
||||
arrival_camera = (spawn[0], spawn[1], spawn[2] + 1.6)
|
||||
arrival_target = (arrival_target_pos[0], arrival_target_pos[1],
|
||||
arrival_target_pos[2] - arrival_target_radius + 1.8)
|
||||
ayaw, apitch = camera_angles(arrival_camera, arrival_target)
|
||||
views[(strate, "arrival")] = {
|
||||
"camera": arrival_camera, "target": arrival_target, "yaw": ayaw,
|
||||
"pitch": apitch, "fov_deg": 90.0,
|
||||
"description": "player eye at the strate arrival, facing into its route"}
|
||||
|
||||
route_distance = min(10.0, path_length * 0.2)
|
||||
route_center, route_radius, _, _ = route_frame(route, route_distance)
|
||||
route_camera = (route_center[0], route_center[1],
|
||||
route_center[2] - route_radius + 1.6)
|
||||
route_target_pos, route_target_radius = interpolate_route(
|
||||
route, min(path_length, route_distance + 28.0))
|
||||
route_target = (route_target_pos[0], route_target_pos[1],
|
||||
route_target_pos[2] - route_target_radius + 1.8)
|
||||
ryaw, rpitch = camera_angles(route_camera, route_target)
|
||||
views[(strate, "route")] = {
|
||||
"camera": route_camera, "target": route_target, "yaw": ryaw,
|
||||
"pitch": rpitch, "fov_deg": 90.0,
|
||||
"description": "player-height view along the passage or surface route"}
|
||||
|
||||
mouth_path = q_route(departure["control_points"], scale)
|
||||
if departure_end == "lower":
|
||||
mouth_path.reverse()
|
||||
mouth, mouth_radius, inward, _ = route_frame(mouth_path, 0.0)
|
||||
departure_distance = min(7.4, sum(vector_length(sub(b[0], a[0]))
|
||||
for a, b in zip(mouth_path, mouth_path[1:])))
|
||||
inside, inside_radius, _, _ = route_frame(mouth_path, departure_distance)
|
||||
departure_camera = (inside[0], inside[1], inside[2] - inside_radius + 1.6)
|
||||
outward = mul(inward, -1.0)
|
||||
departure_target = (mouth[0] + outward[0] * 12.0,
|
||||
mouth[1] + outward[1] * 12.0,
|
||||
mouth[2] - mouth_radius + 1.8)
|
||||
dyaw, dpitch = camera_angles(departure_camera, departure_target)
|
||||
views[(strate, "departure-mouth")] = {
|
||||
"camera": departure_camera, "target": departure_target,
|
||||
"yaw": dyaw, "pitch": dpitch, "fov_deg": 90.0,
|
||||
"description": f"inside the {departure_end} mouth looking out"}
|
||||
|
||||
overview_camera = (spawn[0] - 12.0, spawn[1] - 16.0, spawn[2] + 70.0)
|
||||
overview_target = (spawn[0], spawn[1], spawn[2] - 4.0)
|
||||
oyaw, opitch = camera_angles(overview_camera, overview_target)
|
||||
views[(strate, "overview")] = {
|
||||
"camera": overview_camera, "target": overview_target,
|
||||
"yaw": oyaw, "pitch": opitch, "fov_deg": 90.0,
|
||||
"description": "overview from 70 m above the arrival"}
|
||||
return views
|
||||
|
||||
|
||||
def scene_records(path, include_offsets=False):
|
||||
records = []
|
||||
keys = set()
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(60)
|
||||
if len(header) != 60 or header[:7] != b"VFMSH00":
|
||||
raise RuntimeError(f"{path} has an invalid mesh header")
|
||||
version, voxel_um, count = struct.unpack_from("<III", header, 8)
|
||||
if version not in (1, 2) or voxel_um == 0:
|
||||
raise RuntimeError(f"{path} has an unsupported mesh header")
|
||||
for _ in range(count):
|
||||
start = stream.tell()
|
||||
key_data = stream.read(16)
|
||||
count_data = stream.read(20)
|
||||
if len(key_data) != 16 or len(count_data) != 20:
|
||||
raise RuntimeError(f"{path} has a truncated tile header")
|
||||
key = struct.unpack("<iiii", key_data)
|
||||
_, _, vertices, indices, masks = struct.unpack("<IIIII", count_data)
|
||||
payload_bytes = vertices * 24 + indices * 4 + masks
|
||||
if version == 2:
|
||||
payload_bytes += vertices * 10
|
||||
stream.seek(payload_bytes, os.SEEK_CUR)
|
||||
end = stream.tell()
|
||||
if end > path.stat().st_size:
|
||||
raise RuntimeError(f"{path} ends inside a tile record")
|
||||
keys.add(key)
|
||||
if include_offsets:
|
||||
records.append((key, start, end))
|
||||
if stream.read(1):
|
||||
raise RuntimeError(f"{path} has trailing data after {count} tiles")
|
||||
return header, voxel_um, keys, records
|
||||
|
||||
|
||||
def tile_keys_at(point, keys, voxel_um):
|
||||
voxel_m = voxel_um / 1_000_000.0
|
||||
found = set()
|
||||
for level in range(9):
|
||||
extent = 32.0 * (1 << level) * voxel_m
|
||||
key = (math.floor(point[0] / extent), math.floor(point[1] / extent),
|
||||
math.floor(point[2] / extent), level)
|
||||
if key in keys:
|
||||
found.add(key)
|
||||
return found
|
||||
|
||||
|
||||
def frame_basis(yaw, pitch):
|
||||
yaw, pitch = math.radians(yaw), math.radians(pitch)
|
||||
forward = (math.sin(yaw) * math.cos(pitch),
|
||||
math.cos(yaw) * math.cos(pitch), math.sin(pitch))
|
||||
right = (math.cos(yaw), -math.sin(yaw), 0.0)
|
||||
up = (-math.sin(yaw) * math.sin(pitch),
|
||||
-math.cos(yaw) * math.sin(pitch), math.cos(pitch))
|
||||
return unit(forward), unit(right), unit(up)
|
||||
|
||||
|
||||
def coverage_audit(camera, yaw, pitch, fov, keys, voxel_um):
|
||||
forward, right, up = frame_basis(yaw, pitch)
|
||||
tan_h = math.tan(math.radians(fov) * 0.5)
|
||||
tan_v = tan_h * 900.0 / 1600.0
|
||||
uncovered = []
|
||||
missing_points = []
|
||||
rays = 0
|
||||
distance_samples = tuple(range(1, 200, 4))
|
||||
for row in range(9):
|
||||
sy = (row / 8.0) * 2.0 - 1.0
|
||||
for column in range(17):
|
||||
sx = (column / 16.0) * 2.0 - 1.0
|
||||
ray = unit(tuple(forward[axis] + right[axis] * sx * tan_h
|
||||
+ up[axis] * sy * tan_v for axis in range(3)))
|
||||
rays += 1
|
||||
for distance in distance_samples:
|
||||
point = add_scaled(camera, ray, distance)
|
||||
if not tile_keys_at(point, keys, voxel_um):
|
||||
uncovered.append((row, column, distance))
|
||||
missing_points.append(point)
|
||||
return {"rays": rays, "samples_per_ray": len(distance_samples),
|
||||
"missing_samples": len(uncovered), "first_missing": uncovered[:8],
|
||||
"missing_points": missing_points}
|
||||
|
||||
|
||||
def merge_scene_exports(destination, sources):
|
||||
unique = {}
|
||||
reference_header = None
|
||||
reference_version = None
|
||||
reference_voxel_um = None
|
||||
for source in sources:
|
||||
header, voxel_um, _, records = scene_records(source, include_offsets=True)
|
||||
version = struct.unpack_from("<I", header, 8)[0]
|
||||
if reference_header is None:
|
||||
reference_header, reference_version, reference_voxel_um = header, version, voxel_um
|
||||
elif version != reference_version or voxel_um != reference_voxel_um:
|
||||
raise RuntimeError("mesh export fragments use incompatible voxel formats")
|
||||
for record in records:
|
||||
unique.setdefault(record[0], (source, record[1], record[2]))
|
||||
|
||||
merged_header = bytearray(reference_header)
|
||||
struct.pack_into("<I", merged_header, 16, len(unique))
|
||||
temporary = destination.with_suffix(destination.suffix + ".merge-tmp")
|
||||
with temporary.open("wb") as output_stream:
|
||||
output_stream.write(merged_header)
|
||||
for source, start, end in unique.values():
|
||||
with source.open("rb") as input_stream:
|
||||
input_stream.seek(start)
|
||||
remaining = end - start
|
||||
while remaining:
|
||||
block = input_stream.read(min(1024 * 1024, remaining))
|
||||
if not block:
|
||||
raise RuntimeError(f"{source} changed while merging mesh exports")
|
||||
output_stream.write(block)
|
||||
remaining -= len(block)
|
||||
os.replace(temporary, destination)
|
||||
return len(unique)
|
||||
|
||||
|
||||
def write_truncated_scene(source, destination, header, keys, records, remove_keys):
|
||||
retained = [record for record in records if record[0] not in remove_keys]
|
||||
new_header = bytearray(header)
|
||||
struct.pack_into("<I", new_header, 16, len(retained))
|
||||
with source.open("rb") as input_stream, destination.open("wb") as output_stream:
|
||||
output_stream.write(new_header)
|
||||
for _, start, end in retained:
|
||||
input_stream.seek(start)
|
||||
remaining = end - start
|
||||
while remaining:
|
||||
block = input_stream.read(min(1024 * 1024, remaining))
|
||||
if not block:
|
||||
raise RuntimeError("source mesh changed while writing the negative control")
|
||||
output_stream.write(block)
|
||||
remaining -= len(block)
|
||||
return {key for key in keys if key not in remove_keys}
|
||||
|
||||
|
||||
def run_negative_control(mesh, stem, view, header, voxel_um, keys, records):
|
||||
camera = view["camera"]
|
||||
forward, right, up = frame_basis(view["yaw"], view["pitch"])
|
||||
remove = set()
|
||||
for distance in range(89, 110, 4):
|
||||
center = add_scaled(camera, forward, distance)
|
||||
for side in (-8.0, 0.0, 8.0):
|
||||
for vertical in (-8.0, 0.0, 8.0):
|
||||
point = add_scaled(add_scaled(center, right, side), up, vertical)
|
||||
remove.update(tile_keys_at(point, keys, voxel_um))
|
||||
if not remove:
|
||||
raise RuntimeError("negative-control ray did not intersect any exported tiles")
|
||||
truncated = OUT / "controls" / f"{stem}.truncated.vfmesh"
|
||||
truncated.parent.mkdir(parents=True, exist_ok=True)
|
||||
remaining = write_truncated_scene(mesh, truncated, header, keys, records, remove)
|
||||
audit = coverage_audit(camera, view["yaw"], view["pitch"], view["fov_deg"],
|
||||
remaining, voxel_um)
|
||||
if audit["missing_samples"] == 0:
|
||||
raise RuntimeError("negative control unexpectedly passed frustum coverage")
|
||||
print(f"[L69NegativeControl] file={truncated.relative_to(ROOT)} removed_tiles={len(remove)} "
|
||||
f"missing_samples={audit['missing_samples']} flagged_before_render=1 status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_capture(strate, label, view, negative_control=False):
|
||||
stem = f"strate{strate}-{label}"
|
||||
camera = view["camera"]
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
mesh = OUT / "meshes" / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
mesh.parent.mkdir(parents=True, exist_ok=True)
|
||||
source = ROOT / "Standalone/data/owner_world.json"
|
||||
parts = [mesh]
|
||||
|
||||
def export_part(part_name, export_camera):
|
||||
part = mesh if part_name.endswith("-base") else OUT / "mesh-patches" / f"{part_name}.vfmesh"
|
||||
part.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(f"{part_name}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
"--render-distance-chunks=0", "--mesh-coverage", "--capture-skip-walkability",
|
||||
"--recipe=surface-world",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={part}"])
|
||||
return part
|
||||
|
||||
export_part(f"{stem}-base", camera)
|
||||
header, voxel_um, keys, records = scene_records(mesh, include_offsets=negative_control)
|
||||
audit = coverage_audit(camera, yaw, pitch, view["fov_deg"], keys, voxel_um)
|
||||
patch_index = 0
|
||||
while audit["missing_samples"] and patch_index < 3:
|
||||
patch_index += 1
|
||||
points = audit["missing_points"]
|
||||
patch_camera = tuple(sum(point[axis] for point in points) / len(points)
|
||||
for axis in range(3))
|
||||
patch_name = f"strate{strate}-{label}-patch{patch_index}"
|
||||
part = export_part(patch_name, patch_camera)
|
||||
parts.append(part)
|
||||
merged_tiles = merge_scene_exports(mesh, parts)
|
||||
print(f"[L69CoveragePatch] view={stem} patch={patch_index} "
|
||||
f"camera_m=({fmt(patch_camera)}) merged_tiles={merged_tiles}", flush=True)
|
||||
header, voxel_um, keys, records = scene_records(mesh, include_offsets=negative_control)
|
||||
audit = coverage_audit(camera, yaw, pitch, view["fov_deg"], keys, voxel_um)
|
||||
print(f"[L69Coverage] view={stem} tiles={len(keys)} rays={audit['rays']} "
|
||||
f"samples_per_ray={audit['samples_per_ray']} distance_m=200 "
|
||||
f"missing_samples={audit['missing_samples']} "
|
||||
f"status={'PASS' if audit['missing_samples'] == 0 else 'FAIL'}", flush=True)
|
||||
if audit["missing_samples"]:
|
||||
raise SystemExit(f"{stem} lacks tile coverage within the 200 m camera frustum: "
|
||||
f"{audit['first_missing']}")
|
||||
if negative_control:
|
||||
run_negative_control(mesh, stem, view, header, voxel_um, keys, records)
|
||||
print(f"[L69CaptureCamera] view={stem} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(view['target'])}) yaw={yaw:.3f} pitch={pitch:.3f} "
|
||||
f"fov_deg={view['fov_deg']:.1f} description={view['description']}", flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900",
|
||||
f"--fov-deg={view['fov_deg']:.1f}", f"--camera-m={fmt(camera)}",
|
||||
f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off", "--shade=readable", "--headlamp=on",
|
||||
"--height-tint=on", "--tile-edges=off"]
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "3", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB (bytes={size})")
|
||||
print(f"[L69CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
if not HEADLESS.is_file():
|
||||
raise SystemExit(f"missing current L68 headless binary: {HEADLESS}")
|
||||
if not RENDERER.is_file():
|
||||
raise SystemExit(f"missing L69 offscreen renderer: {RENDERER}")
|
||||
layout = load_layout("owner-four-strate", ROOT / "Standalone/data/owner_world.json",
|
||||
"surface-world")
|
||||
views = make_player_views(layout)
|
||||
print(f"[L69CapturePlan] strates=4 views={len(views)} frustum_m=200 "
|
||||
f"clip_radius=3 max_clip_level=8 max_tile_level=8 patches=up-to-3", flush=True)
|
||||
first = True
|
||||
for strate in range(4):
|
||||
for label in ("arrival", "route", "departure-mouth", "overview"):
|
||||
export_capture(strate, label, views[(strate, label)], negative_control=first)
|
||||
first = False
|
||||
print(f"[L69CaptureSet] files={len(list(CAPTURES.glob('*.jpg')))} "
|
||||
f"directory={CAPTURES.relative_to(ROOT)} status=PASS", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
def select_passage(layout, mouth_generator, preferred_side):
|
||||
strata = {int(value["strate_index"]): value for value in layout["strates"]}
|
||||
preferred_candidates = []
|
||||
fallback_candidates = []
|
||||
for passage in layout["passages"]:
|
||||
upper = strata[int(passage["upper_strate_index"])]
|
||||
lower = strata[int(passage["lower_strate_index"])]
|
||||
sides = []
|
||||
if upper["generator_kind"] == mouth_generator:
|
||||
sides.append("upper")
|
||||
if lower["generator_kind"] == mouth_generator:
|
||||
sides.append("lower")
|
||||
if preferred_side in sides:
|
||||
preferred_candidates.append((passage, preferred_side == "lower"))
|
||||
elif sides:
|
||||
fallback_candidates.append((passage, sides[0] == "lower"))
|
||||
candidates = preferred_candidates or fallback_candidates
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no passage mouth in generator {mouth_generator}")
|
||||
return sorted(candidates, key=lambda item: item[0]["stable_id"])[0]
|
||||
|
||||
|
||||
def make_views(layout, passage, reverse):
|
||||
q_to_m = layout["core_voxel_size_um"] / (SUBVOXELS_PER_VOXEL * 1_000_000.0)
|
||||
controls = [
|
||||
(tuple(value * q_to_m for value in point["position_q"]),
|
||||
point["radius_q"] * q_to_m)
|
||||
for point in passage["control_points"]
|
||||
]
|
||||
lower_mouth = tuple(value * q_to_m for value in passage["lower_mouth_q"])
|
||||
lower_index = min(range(len(controls)), key=lambda i: sum(
|
||||
(controls[i][0][axis] - lower_mouth[axis]) ** 2 for axis in range(3)))
|
||||
path = controls[:lower_index + 1]
|
||||
if len(path) < 2:
|
||||
raise RuntimeError(f"{passage['stable_id']} has no mouth-to-mouth route")
|
||||
if reverse:
|
||||
path.reverse()
|
||||
lengths = [math.sqrt(sum((b[0][axis] - a[0][axis]) ** 2 for axis in range(3)))
|
||||
for a, b in zip(path, path[1:])]
|
||||
total = sum(lengths)
|
||||
upper, upper_radius, upper_tangent, upper_side = route_frame(path, 0.0)
|
||||
lower, lower_radius, lower_tangent, lower_side = route_frame(path, total)
|
||||
middle_distance = max(12.0, min(total - 12.0, total * 0.4)) if total > 24.0 else total * 0.4
|
||||
if (int(passage["upper_strate_index"],) == 1
|
||||
and int(passage["lower_strate_index"]) == 2 and not reverse):
|
||||
# The old 7 m authored-radius beat sat near the middle of this audit window.
|
||||
middle_distance = min(total - 12.0, 90.8)
|
||||
entry_distance = min(6.0, total * 0.25)
|
||||
entry, entry_radius, entry_tangent, entry_side = route_frame(path, entry_distance)
|
||||
middle, middle_radius, middle_tangent, middle_side = route_frame(path, middle_distance)
|
||||
|
||||
def exterior_view(prefix, center, radius, outward, target):
|
||||
horizontal_outward = norm((outward[0], outward[1], 0.0))
|
||||
eye_height = center[2] - radius + 1.6
|
||||
eye = (center[0] + horizontal_outward[0] * 5.5,
|
||||
center[1] + horizontal_outward[1] * 5.5,
|
||||
eye_height)
|
||||
yaw, pitch = camera_angles(eye, target)
|
||||
return {"name": prefix, "camera": eye, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": eye[:2],
|
||||
"focus": center, "mouth": center, "eye_height_z": eye_height}
|
||||
|
||||
entrance, entrance_radius, entrance_tangent = upper, upper_radius, upper_tangent
|
||||
exit_point, exit_radius, exit_tangent = lower, lower_radius, lower_tangent
|
||||
ground_target_pos, ground_target_radius = interpolate_route(path, min(total, 1.0))
|
||||
ground = exterior_view("ground-mouth", entrance, entrance_radius,
|
||||
mul(entrance_tangent, -1.0), (ground_target_pos[0], ground_target_pos[1],
|
||||
ground_target_pos[2] - ground_target_radius + 1.0))
|
||||
ground["focus"] = entrance
|
||||
ground["outside_camera"] = True
|
||||
|
||||
def cutaway_view(prefix, center, radius, tangent, side, distance, target_distance):
|
||||
camera = (center[0] - side[0] * 7.0, center[1] - side[1] * 7.0,
|
||||
center[2] - radius + 1.6)
|
||||
target_pos, target_radius = interpolate_route(path, target_distance)
|
||||
target = (target_pos[0], target_pos[1],
|
||||
target_pos[2] - target_radius + 1.6)
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
plane = (side[0], side[1], 0.0,
|
||||
-(side[0] * center[0] + side[1] * center[1]))
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": plane, "camera_xy": camera[:2],
|
||||
"focus": center, "mouth": center}
|
||||
|
||||
def inside_mouth_view(prefix, at_start):
|
||||
endpoint_distance = 0.0 if at_start else total
|
||||
mouth, _, tangent, mouth_side = route_frame(path, endpoint_distance)
|
||||
camera_distance = min(total, 7.4) if at_start else max(0.0, total - 5.5)
|
||||
center, _, _, side = route_frame(path, camera_distance)
|
||||
camera = (center[0] + side[0] * 2.5,
|
||||
center[1] + side[1] * 2.5, center[2])
|
||||
outward = mul(tangent, -1.0) if at_start else tangent
|
||||
target = (mouth[0] + outward[0] * 8.0 + mouth_side[0] * 2.5,
|
||||
mouth[1] + outward[1] * 8.0 + mouth_side[1] * 2.5,
|
||||
mouth[2])
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return {"name": prefix, "camera": camera, "yaw": yaw, "pitch": pitch,
|
||||
"target": target, "clip": None, "camera_xy": camera[:2],
|
||||
"focus": mouth, "mouth": mouth, "outside_camera": False,
|
||||
"camera_location": "passage-air", "focus_radius": 30.0,
|
||||
"eye_height_z": camera[2], "fov_deg": 80.0}
|
||||
|
||||
# Place each interface view at its true route endpoint. The endpoint camera
|
||||
# is a side cutaway so the solid terrain in front of an underground mouth
|
||||
# cannot hide the floor-to-tunnel connection.
|
||||
arrival_distance = 0.0
|
||||
arrival_center, arrival_radius, arrival_tangent, arrival_side = route_frame(
|
||||
path, arrival_distance)
|
||||
arrival = cutaway_view("arrival", arrival_center, arrival_radius,
|
||||
arrival_tangent, arrival_side, arrival_distance,
|
||||
min(total, arrival_distance + 12.0))
|
||||
arrival["camera_location"] = "passage-air"
|
||||
arrival["focus_radius"] = 20.0
|
||||
arrival["yaw"], arrival["pitch"] = camera_angles(
|
||||
arrival["camera"], arrival["target"])
|
||||
|
||||
entry_camera_distance = min(8.0, total * 0.1)
|
||||
entry_center, entry_radius, entry_tangent, entry_side = route_frame(
|
||||
path, entry_camera_distance)
|
||||
# Player-height view from just inside the passage, aimed through its actual
|
||||
# interface mouth so the floor, bore, and opening share one frame.
|
||||
entry_camera = (entry_center[0], entry_center[1],
|
||||
entry_center[2] - entry_radius + 1.6)
|
||||
entry_target_pos, entry_target_radius = interpolate_route(
|
||||
path, min(total, entry_distance + 14.0))
|
||||
entry_target = (entry_target_pos[0], entry_target_pos[1],
|
||||
entry_camera[2] - 2.5)
|
||||
entry_yaw, entry_pitch = camera_angles(entry_camera, entry_target)
|
||||
entry_view = {"name": "rock-entry", "camera": entry_camera,
|
||||
"yaw": entry_yaw, "pitch": entry_pitch, "target": entry_target,
|
||||
"clip": None, "camera_xy": entry_camera[:2],
|
||||
"focus": entry_center, "mouth": entrance, "outside_camera": False,
|
||||
"camera_location": "passage-air", "focus_radius": 20.0,
|
||||
"eye_height_z": entry_camera[2]}
|
||||
middle_view = cutaway_view("mid-tunnel", middle, middle_radius, middle_tangent,
|
||||
middle_side, middle_distance, min(total, middle_distance + 2.0))
|
||||
ground["focus_radius"] = 20.0
|
||||
middle_view["focus_radius"] = 20.0
|
||||
middle_camera_center, _ = interpolate_route(path, middle_distance)
|
||||
middle_view["camera"] = (middle_camera_center[0] + middle_side[0] * 0.75,
|
||||
middle_camera_center[1] + middle_side[1] * 0.75,
|
||||
middle[2] - middle_radius + 1.6)
|
||||
middle_target_pos, middle_target_radius = interpolate_route(
|
||||
path, min(total, middle_distance + 14.0))
|
||||
middle_view["target"] = (middle_target_pos[0] + middle_side[0] * 2.35,
|
||||
middle_target_pos[1] + middle_side[1] * 2.35,
|
||||
middle_target_pos[2] - middle_target_radius + 1.8)
|
||||
# Keep the camera inside the bore, close to one wall, and look down the
|
||||
# route so the old beat location is visible at player height.
|
||||
middle_view["clip"] = None
|
||||
middle_view["camera_location"] = "passage-air"
|
||||
middle_view["yaw"], middle_view["pitch"] = camera_angles(
|
||||
middle_view["camera"], middle_view["target"])
|
||||
return (ground, entry_view, middle_view, arrival,
|
||||
inside_mouth_view("inside-mouth-start", True),
|
||||
inside_mouth_view("inside-mouth-end", False)), total, lower_index
|
||||
|
||||
|
||||
def export_capture(scene_name, source, recipe, passage, endpoint, view):
|
||||
stem = f"{passage['stable_id'].replace('/', '-')}-{endpoint}"
|
||||
view["name"] = stem
|
||||
mesh = OUT / f"{stem}.vfmesh"
|
||||
png = PNG_DIR / f"{stem}.png"
|
||||
jpg = CAPTURES / f"{stem}.jpg"
|
||||
yaw, pitch = view["yaw"], view["pitch"]
|
||||
camera = view["camera"]
|
||||
export_camera = view.get("focus", camera)
|
||||
run(f"{stem}-export", [HEADLESS, source, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", "--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0", f"--recipe={recipe}",
|
||||
f"--camera={fmt((*export_camera, yaw, pitch))}", f"--mesh-export={mesh}"])
|
||||
render_clip = view["clip"]
|
||||
surface = None
|
||||
if view.get("outside_camera", False):
|
||||
surface = terrain_surface_near(mesh, *view["camera_xy"], view["target"][2])
|
||||
if surface is None:
|
||||
camera = (view["camera_xy"][0], view["camera_xy"][1], view["eye_height_z"])
|
||||
else:
|
||||
camera = (surface[0], surface[1], surface[2] + 1.6)
|
||||
elif render_clip is not None:
|
||||
camera = view["camera"]
|
||||
yaw, pitch = camera_angles(camera, view["target"])
|
||||
fov_deg = view.get("fov_deg", 80.0)
|
||||
print(f"[L68CaptureCamera] view={stem} camera_m=({fmt(camera)}) "
|
||||
f"target_m=({fmt(view['target'])}) "
|
||||
f"clip={fmt(render_clip) if render_clip is not None else 'none'} fov_deg={fov_deg:.1f}",
|
||||
flush=True)
|
||||
render = [RENDERER, mesh, png, "--size=1600x900", f"--fov-deg={fov_deg:.1f}",
|
||||
f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", f"--pitch-deg={pitch:.6f}",
|
||||
"--tile-skirts=off", "--shade=readable", "--headlamp=on", "--height-tint=on",
|
||||
f"--height-center-m={view['target'][2]:.6f}", "--height-range-m=8", "--tile-edges=off"]
|
||||
if view.get("focus_radius", 24.0) is not None:
|
||||
render.append(f"--focus-region={fmt((*view['focus'], view.get('focus_radius', 24.0)))}")
|
||||
if render_clip is not None:
|
||||
render.append(f"--clip-plane={fmt(render_clip)}")
|
||||
run(f"{stem}-render", render)
|
||||
run(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error",
|
||||
"-y", "-i", png, "-frames:v", "1", "-q:v", "4", jpg])
|
||||
size = jpg.stat().st_size if jpg.exists() else 0
|
||||
print(f"[L68CaptureJPEG] file={jpg.relative_to(ROOT)} bytes={size} "
|
||||
f"status={'PASS' if 0 < size <= 300_000 else 'FAIL'}", flush=True)
|
||||
if not 0 < size <= 300_000:
|
||||
raise SystemExit(f"{jpg.name} is missing or exceeds 300 KB")
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Linux69 — owner four-strata captures and spawn trace
|
||||
|
||||
## Captures
|
||||
|
||||
The L68 exports were limited to a small clip radius and LOD0 (max-clip-level=0, max-level=0). The camera frustum extended beyond those tiles, while the renderer cleared empty pixels to RGB (0.035, 0.055, 0.075). Unmeshed space therefore looked nearly black.
|
||||
|
||||
L69 exports use a 90° player-view frustum, radius 3 and LODs through 8. A coverage probe samples a 17×9 ray grid at 50 points per ray, every 4 m from 1 m through 197 m. Nine views needed one overlapping mesh patch. All 16 ended with zero uncovered probe samples. The renderer clear color is now sky blue (RGB (0.30, 0.48, 0.62)). A tile coverage failure aborts before rendering, so a missing export cannot silently become a black capture. This is a sampled coverage check, not a per-pixel proof.
|
||||
|
||||
The negative control removes 8 tiles from the strate 0 arrival mesh. It reports 480 uncovered samples and fails before rendering. Control mesh: Saved/Linux/l69/controls/strate0-arrival.truncated.vfmesh.
|
||||
|
||||
The coverage probe only answers whether mesh tiles cover its sampled frustum points. No real hole was confirmed in the captures. The engine trace blocker below is a solid passage sample. Blue pixels indicate an unoccluded ray through covered tiles; this round did not add a separate classifier for open sky versus a terrain breach.
|
||||
|
||||
All JPEGs are below 300 KB; combined size is 447,288 bytes.
|
||||
|
||||
| Strate | View | File | Bytes | What it shows |
|
||||
|---:|---|---|---:|---|
|
||||
| 0 | Arrival | [strate0-arrival.jpg](captures/strate0-arrival.jpg) | 40,064 | Player-eye view at the surface arrival, facing into the route. |
|
||||
| 0 | Route | [strate0-route.jpg](captures/strate0-route.jpg) | 38,567 | Player-height view along the surface route toward the passage. |
|
||||
| 0 | Departure mouth | [strate0-departure-mouth.jpg](captures/strate0-departure-mouth.jpg) | 35,055 | Inside the upper mouth looking outward. |
|
||||
| 0 | Overview | [strate0-overview.jpg](captures/strate0-overview.jpg) | 23,395 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||
| 1 | Arrival | [strate1-arrival.jpg](captures/strate1-arrival.jpg) | 24,524 | Player-eye view at the strate 1 arrival, facing into its route. |
|
||||
| 1 | Route | [strate1-route.jpg](captures/strate1-route.jpg) | 29,602 | Player-height view along the passage. |
|
||||
| 1 | Departure mouth | [strate1-departure-mouth.jpg](captures/strate1-departure-mouth.jpg) | 26,090 | Inside the upper mouth looking outward. |
|
||||
| 1 | Overview | [strate1-overview.jpg](captures/strate1-overview.jpg) | 25,960 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||
| 2 | Arrival | [strate2-arrival.jpg](captures/strate2-arrival.jpg) | 20,339 | Player-eye view at the strate 2 arrival, facing into its route. |
|
||||
| 2 | Route | [strate2-route.jpg](captures/strate2-route.jpg) | 20,244 | Player-height view along the passage. |
|
||||
| 2 | Departure mouth | [strate2-departure-mouth.jpg](captures/strate2-departure-mouth.jpg) | 33,125 | Inside the upper mouth looking outward. |
|
||||
| 2 | Overview | [strate2-overview.jpg](captures/strate2-overview.jpg) | 25,668 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||
| 3 | Arrival | [strate3-arrival.jpg](captures/strate3-arrival.jpg) | 24,307 | Player-eye view at the strate 3 arrival, facing into its route. |
|
||||
| 3 | Route | [strate3-route.jpg](captures/strate3-route.jpg) | 23,966 | Player-height view along the passage. |
|
||||
| 3 | Departure mouth | [strate3-departure-mouth.jpg](captures/strate3-departure-mouth.jpg) | 35,034 | Inside the lower mouth looking outward. |
|
||||
| 3 | Overview | [strate3-overview.jpg](captures/strate3-overview.jpg) | 21,348 | Elevated view, 70 m above the arrival, looking down at the route. |
|
||||
|
||||
## In-engine spawn trace
|
||||
|
||||
The host rebuilt successfully on Linux. The trace harness was prepared for 3 rotated/interleaved trials per strate, spawning at each arrival. No strate 0 attempt produced a valid Core trace; the other strata were not run. Engine metrics are therefore unavailable, including walkable ring, LOD0 ring, full set, worker-seconds, LOD0 p50/p95, and generateTile / mesh-copy / RMC timings.
|
||||
|
||||
| Strate | Arrival spawn (m) | Headless whole-view median | Valid engine trials | Walkable ring | LOD0 ring | Full set | Worker-s | LOD0 p50 / p95 | generateTile / mesh copy / RMC |
|
||||
|---:|---|---:|---:|---:|---:|---:|---:|---|---|
|
||||
| 0 | (0, 0, -374.939) | 1.074 s | 0/3 (attempts blocked) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 1 | (25.837, 3.199, -737.000) | 1.503 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 2 | (19.166, -14.200, -753.374) | 0.577 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
| 3 | (-20.063, -5.068, -900.354) | 0.210 s | 0/3 (not run) | N/A | N/A | N/A | N/A | N/A | N/A |
|
||||
|
||||
No engine/headless percentage comparison is available, so no >30% gap can be assessed. The default engine-generated four-strate plan failed passage validation at the solid Core voxel (-172, -138, -1794) (25 cm per voxel; approximately (-43.0, -34.5, -448.5) m). This is a blocked centerline point, not a hole. The saved owner manifest lacked layout_chunk_width_q; regenerating and combining its current SurfaceWorld plan with the saved FloatingIslands plan got past parsing, but UE rejected the island plan bounds. The imported slot is strate 1 chunks [-79, -16], while the live slot resolves to [-93, -64]. A floating-islands-only headless export also failed because it could not resolve a SurfaceWorld strate. I stopped without bypassing the production checks.
|
||||
|
||||
## Failed runs and build notes
|
||||
|
||||
- Radius 4 exports, with max clip level 8 and then 7, exceeded the 160-million-node shape safety bound. Radius 3 plus overlapping patches covered the views.
|
||||
- The UE host toolchain failed under the 6 GiB address-space limit during CoreCLR startup, so host build and UE runs used the requested 6 GiB RSS limit. The host build passed after using the local VFHost/PluginDeps plugin mirror; the final incremental build compiled the spawn-coordinate parser.
|
||||
- The first UE trace launch exposed comma parsing in the new spawn argument. The parser was corrected and the host rebuilt. One RSS-capped launch exited when AndroidFileServer failed to map; disabling that unrelated plugin let the commandlet run. The generated-settings plan hit the solid-centerline failure above. Manifest attempts then encountered the stale schema and stale island bounds; none reached valid spawn tracing.
|
||||
- No attempted run exceeded 30 minutes. Logs are under Saved/Linux/l69.
|
||||
|
||||
No world recipe, geometry, or peer-determinism inputs changed. The only Source/ edit is Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp, adding a finite XYZ spawn override for the trace; it compiled in the Linux host build. Standalone/Source/StreamBenchmarkHeadless.cpp also writes the canonical manifest after plan assembly for trace setup; its Linux rebuild passed. Other capture-only edits are in Standalone/Source/LinuxOffscreenCapture.cpp and Standalone/Source/StreamBenchmarkPipeline.h.
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run three interleaved UE startup traces at each owner-plan arrival."""
|
||||
import subprocess
|
||||
import time
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
HOST = Path("/home/jahni/VFHost")
|
||||
PLUGIN_SAVED = ROOT / "Saved/Linux/l69"
|
||||
HOST_PLUGIN_SAVED = HOST / "Plugins/VoxelForge/Saved/Linux/l69"
|
||||
OWNER_WORLD = ROOT / "Standalone/data/owner_world.json"
|
||||
OWNER_MANIFEST = HOST_PLUGIN_SAVED / "owner_world_manifest.json"
|
||||
EDITOR = Path("/home/jahni/UnrealEngine/Engine/Binaries/Linux/UnrealEditor-Cmd")
|
||||
TRACES = PLUGIN_SAVED / "traces"
|
||||
HOST_SAVED = HOST / "Saved/Linux/l69"
|
||||
SPAWNS_M = {
|
||||
0: (0.0, 0.0, -374.939453),
|
||||
1: (25.836914, 3.199219, -737.0),
|
||||
2: (19.166016, -14.200195, -753.374023),
|
||||
3: (-20.0625, -5.068359, -900.353516),
|
||||
}
|
||||
ORDER = ((0, 1), (1, 1), (2, 1), (3, 1),
|
||||
(1, 2), (2, 2), (3, 2), (0, 2),
|
||||
(2, 3), (3, 3), (0, 3), (1, 3))
|
||||
|
||||
|
||||
def run(strate, trial):
|
||||
label = f"strate{strate}-trial{trial}"
|
||||
trace = TRACES / f"{label}.trace.jsonl"
|
||||
abslog = TRACES / f"{label}.log"
|
||||
stdout = TRACES / f"{label}.stdout.log"
|
||||
output = HOST_PLUGIN_SAVED / "output" / label
|
||||
userdir = HOST_SAVED / label / "User"
|
||||
zendata = HOST_SAVED / label / "Zen"
|
||||
ddc = HOST_SAVED / "DDC"
|
||||
for directory in (output, userdir, zendata, ddc, TRACES):
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
spawn_cm = ",".join(f"{value * 100.0:.3f}" for value in SPAWNS_M[strate])
|
||||
command = [
|
||||
str(EDITOR), str(HOST / "VFHost.uproject"), "-nullrhi", "-unattended",
|
||||
"-nop4", "-nosplash", "-NoSound", "-corelimit=12", "-NoZenAutoLaunch",
|
||||
"-notraceserver", "-nocrashreports", "-ddc=NoZenLocalFallback",
|
||||
f"-userdir={userdir}", f"-ZenDataPath={zendata}", f"-LocalDataCachePath={ddc}",
|
||||
"-run=VoxelForgeExplore", "-seed=0", "-archetype=TunnelNetwork", "-slot=0",
|
||||
"-modes=export", "-gameconfig=1", "-coreworldtest=1", "-ownerfourstrateplan=1",
|
||||
"-corevoxelsizecm=25", "-coreislandtest=0", "-corefallbacktest=0",
|
||||
"-coreofftest=0", "-coreplantest=0",
|
||||
"-settingsref=/Game/VoxelForge/DA_Settings.DA_Settings", "-corehybridmesher=0",
|
||||
f"-coreworldassembly={OWNER_MANIFEST}",
|
||||
f"-out={output}",
|
||||
"-voxel.StreamingCoarseFirst=0", "-voxel.StreamingDynamicNearReserve=1",
|
||||
"-voxel.StreamingLegacyPendingResultCap=0", "-voxel.CoreRegionConcurrentAdmission=1",
|
||||
"-voxel.StreamingResultBacklogCap=256", "-voxel.StreamingReadyQueueDepth=8",
|
||||
"-voxel.StreamingBackgroundWorkerCap=1", "-voxel.CoreColumnCacheDirectMapped=0",
|
||||
"-voxel.CoreMCGradientMemoization=1", "-voxel.StreamingCollisionLookahead=1",
|
||||
"-coreworldstreamtrace=1", "-voxel.TestMove=0",
|
||||
"-voxel.TestMoveSpeedCmPerSecond=0", f"-voxel.StartupTraceSpawnCm={spawn_cm}",
|
||||
f"-voxel.StartupTraceFile={trace}", "-voxel.StartupTraceUntilEndPlay=1",
|
||||
"-voxel.StartupTraceMaxTiles=16384", "-voxel.CollisionGate=0",
|
||||
f"-abslog={abslog}",
|
||||
]
|
||||
wrapped = ["taskset", "-c", "0-11", "prlimit", "--rss=6442450944", "--",
|
||||
"timeout", "1740s", *command]
|
||||
started = time.monotonic()
|
||||
with stdout.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(wrapped, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, timeout=1740, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L69EngineTrace] label={label} spawn_cm={spawn_cm} elapsed_s={elapsed:.2f} "
|
||||
f"exit={result.returncode} trace={trace} stdout={stdout}", flush=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{label} failed; inspect {stdout} and {abslog}")
|
||||
if not trace.is_file() or trace.stat().st_size == 0:
|
||||
raise SystemExit(f"{label} exited successfully but produced no startup trace")
|
||||
|
||||
|
||||
def main():
|
||||
if not EDITOR.is_file():
|
||||
raise SystemExit(f"missing UE commandlet executable: {EDITOR}")
|
||||
HOST_PLUGIN_SAVED.mkdir(parents=True, exist_ok=True)
|
||||
owner = json.loads(OWNER_WORLD.read_text(encoding="utf-8"))
|
||||
assembly = owner.get("settings_asset", {}).get("WorldAssemblyManifestJson")
|
||||
if not isinstance(assembly, str) or not assembly:
|
||||
raise SystemExit(f"{OWNER_WORLD} has no serialized world-assembly manifest")
|
||||
OWNER_MANIFEST.write_text(assembly + "\n", encoding="utf-8")
|
||||
print(f"[L69EngineTracePlan] source={OWNER_WORLD} manifest={OWNER_MANIFEST} "
|
||||
f"bytes={OWNER_MANIFEST.stat().st_size}", flush=True)
|
||||
for strate, trial in ORDER:
|
||||
run(strate, trial)
|
||||
print("[L69EngineTraceSet] strata=4 trials_each=3 total=12 status=PASS", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
@@ -0,0 +1,77 @@
|
||||
## Ranked recommendation
|
||||
|
||||
**Keep the order TN → VS → FI.** The order is unchanged; I’ve corrected the explanation for FI and tightened the first-round gate.
|
||||
|
||||
| Rank | Generator | Recommendation |
|
||||
|---|---|---|
|
||||
| **1. TN** | Best first native-family step for caves and liminal variety. **[CODE]** A native cave graph, field evaluator and adapter recipe exist. **[ASSET]** The owner’s TN asset does not select that recipe or provide its graph. **[INFERENCE]** The parameter-to-graph work is a substantial migration, and a speed gain is unproven: the owner’s legacy TN is 0.577 s; the native cave fixture was 0.706 s. |
|
||||
| **2. VS** | Strong visual payoff, but the existing native lattice is not yet a convincing shaft generator. **[CODE]** The adapter accepts `NativeLattice` for `VerticalShafts` and evaluates explicit room/tunnel graphs. **[REPORT]** The fixture looked like a corkscrew tunnel and called for an open shaft core; it took 1.694 s. This makes VS the highest geometry and gameplay risk. |
|
||||
| **3. FI** | Keep last in a performance-led sequence, **not** because native FI is fixture-only. **[CODE]** Its native field, adapter planner and runtime dispatch path exist. **[REPORT]** The current owner run still reports compiled analytic FI at 1.503 s, and other FI fixtures are slower. **[INFERENCE]** It is the most mature native implementation, but its speed payoff is unclear. |
|
||||
|
||||
**What changed in this re-audit:** the FI adapter can auto-plan for an owner world without an imported assembly; an imported assembly must contain a matching plan for every FI stratum or validation fails. I cannot identify from the inspected code why the reported owner run still dispatches compiled analytic FI, so I withdraw the earlier suggestion that an imported override explains it. I also cannot attribute the owner’s FI time to the native mouth filter: that code runs only on the native path, while L67 reports the owner path as compiled analytic. Finally, changing TN’s field could disturb a passage landing; the first-round brief now makes the L68 route audits a promotion gate.
|
||||
|
||||
Labels below distinguish **[CODE]** source inspection, **[ASSET]** checked-in owner export, **[REPORT]** recorded run evidence, and **[INFERENCE]** an estimate or conclusion rather than a verified implementation fact.
|
||||
|
||||
## Evidence
|
||||
|
||||
### The native paths are reachable from the runtime adapter
|
||||
|
||||
**[CODE] They are not reachable only through a fixture flag.** The production route in source is:
|
||||
|
||||
1. `AVoxelWorld::InitializeVoxelForgeCoreWorld` calls `SerializeResolvedWorld`, parses the returned snapshot and constructs the Core `World`. [VoxelWorld.cpp:7591–7623](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7591) [VoxelWorld.cpp:7661–7664](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7661)
|
||||
2. The Unreal adapter attaches a validated assembly. On the generated-manifest branch it calls `BuildFloatingIslandStratumPlan` for FI slots; it also writes resolved layout entries, configures each `CoreGeometry` recipe, plans passages and resolves authored room/tunnel records. [VoxelForgeCoreAdapter.cpp:1552–1664](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1552) [VoxelForgeCoreAdapter.cpp:1836–1850](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1836) [VoxelForgeCoreAdapter.cpp:1980–2000](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1980)
|
||||
3. Core’s regular tile-region build passes the manifest and resolved layout to `FStandaloneShapeWorld::Build`; that build sets `bUseNativeManifestFields=true` and binds the resolved fields. This is the normal build path, separate from `bNativeOnly`, the audit mode. [VoxelForgeCore.cpp:1667–1673](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1667) [ShapeWorldInternal.h:2332–2352](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2332)
|
||||
4. `DensityAt` tries native FI, then the bound native cave/surface field; `DensityDispatchPathAt` probes those same evaluators and reports the selected path. Native cave composition applies the resolved graph and shared composition. [ShapeWorldInternal.h:3068–3099](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3068) [ShapeWorldInternal.h:3123–3140](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3123) [ShapeWorldInternal.h:1892–1920](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1892)
|
||||
|
||||
**[REPORT] Reachable does not mean active in the owner run.** L67 says the owner’s FI, TN and VS paths use the compiled analytic base evaluator. [L67 report:5](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux67/REPORT.md:5) **[ASSET]** The checked-in export selects `CoreGeometry.GeneratorRecipe=None`, so TN and VS are not opted into their graph recipes. [owner_world.json:1](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/data/owner_world.json:1) FI is different: its planner is automatic on one adapter branch, but the L67 dispatch evidence says the current run did not select it. The precise runtime reason remains unresolved.
|
||||
|
||||
There is one boundary to this finding: **[CODE]** the adapter has a CoreGeometry-only layout path when there are no SurfaceWorld or FI strata, but native density evaluation requires a `WorldAssembly` as well as a resolved layout. That may leave a caves-only world unable to use this path without an assembly. The owner’s four-strata world includes SurfaceWorld, so this caveat does not by itself explain its present dispatch. [VoxelForgeCoreAdapter.cpp:1602–1606](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1602) [ShapeWorldInternal.h:6570–6583](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6570)
|
||||
|
||||
### Exists / missing, checked against code
|
||||
|
||||
| Generator | Exists: what I verified | Missing: what I verified or inferred |
|
||||
|---|---|---|
|
||||
| **FI** | **[CODE]** `FloatingIslandFieldV1` evaluates island plans and provides feature bounds and intervals. The Unreal adapter maps island settings and creates plans; `ShapeWorldInternal.h` binds and dispatches the field when its plan is present. [FloatingIslandsInternal.h:2286–2315](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:2286) [FloatingIslandsInternal.h:2492–2540](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:2492) [VoxelForgeCoreAdapter.cpp:857–950](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:857) | **[CODE + REPORT]** No island-field algorithm port is missing. What is missing from the current owner run is successful native selection; the cause is not established. **[INFERENCE]** Likely work is path instrumentation and, only if it exposes a defect, a small adapter/runtime or verification change—not a new field family. |
|
||||
| **TN** | **[CODE]** `NativeCaves` selects `Core.Caves.PrimitiveField` v1. The Unreal adapter converts authored rooms and tunnels; the Core resolver and shape evaluator resolve and compose that graph. [VoxelForgeCoreAdapter.cpp:145–158](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:145) [VoxelForgeCoreAdapter.cpp:1288–1414](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1288) [ShapeWorldInternal.h:7381–7430](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:7381) | **[CODE]** The inspected adapter resolves authored Core records; it does not translate the owner’s 82-field `FStrateGenerationParams` into them. **[ASSET]** The owner recipe is None and its graph arrays are empty. A parameter compiler and explicit opt-in are therefore missing. **[INFERENCE]** Likely touch points are `VoxelStrateTypes.h`, `VoxelForgeCoreAdapter.cpp`, Core graph/schema or resolver code if needed, `ShapeWorldInternal.h` if the field needs more semantics, plus owner verification. |
|
||||
| **VS** | **[CODE]** `NativeLattice` is accepted for `VerticalShafts`, and uses resolved rooms and explicit tunnel paths. **[REPORT]** A native lattice fixture proves reachability and interval checks. [VoxelForgeCoreAdapter.cpp:159–171](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:159) [L58 report:9–13](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux58/REPORT.md:9) | **[CODE SURVEY + REPORT]** The inspected recipe/planner is generic lattice graph support, not a shaft-specific planner for `FVerticalShaftParams`; the fixture report says its result is not an open vertical shaft. A shaft plan/open core and safe landings remain. The claim that there is no other dedicated planner is limited to the inspected adapter/Core route; the report independently confirms the visual gap. |
|
||||
|
||||
**[INFERENCE] File counts from my prior answer were estimates, not counted changes.** For FI, the relevant implementation is already in `FloatingIslandsInternal.h`, `ShapeWorldInternal.h` and `VoxelForgeCoreAdapter.cpp`; first locate the selection mismatch before estimating edits. For TN, those same adapter/Core layers plus `VoxelStrateTypes.h` are the likely work area. For VS, expect those layers plus shaft-specific geometry and proof. Exact touched-file counts depend on whether existing records can represent the required parameter semantics.
|
||||
|
||||
### FI cost: traced paths and falsifier
|
||||
|
||||
There are three paths to keep separate:
|
||||
|
||||
- **[REPORT] Current owner path:** L67 reports `compiled-analytic/FloatingIslands`, not native FI. In `ShapeWorldInternal.h`, that means `DensityAt` falls through the native probes to `AnalyticDensityAt`; its general field path uses `EffectiveRockDistance`, the local detail pass, and shared composition. [ShapeWorldInternal.h:3068–3099](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3068) [ShapeWorldInternal.h:5343–5422](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:5343) **I have not proved that the L55/L60 FI timings isolate this exact evaluator.**
|
||||
- **[CODE] Unreal legacy generator path:** `BuildFloatingIslandStack` starts from void, adds `FIslandBlobSource`, roughness, fill and structural posts. `FIslandBlobSource` evaluates nearby hash-placed islands and contour warp. [VoxelGenerator.cpp:2630–2636](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelGenerator.cpp:2630) [VoxelDensityOpStack.cpp:2871–2935](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:2871) [VoxelDensityOpStack.cpp:9504–9533](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelDensityOpStack.cpp:9504)
|
||||
- **[CODE] Native FI path:** when selected, Core samples `FloatingIslandFieldV1`; near a mouth it may add a 27-tap smoothing filter and six neighbor probes. [ShapeWorldInternal.h:6592–6608](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6592) [ShapeWorldInternal.h:6737–6800](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6737)
|
||||
|
||||
**[REPORT]** L55 attributes substantial worker work to grid and skirts: 36.414 worker-seconds of grid work and 2.787 of skirts across its 2,370-tile FI drain. These are worker-seconds for that fixture, not the current owner’s 1.503 s wall time. [L55 report:15–30](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux55/REPORT.md:15) R32’s optimistic calculation says removing all measured FI grid work from the L60 setup still leaves about 1.59 s; again, that is not the current owner path. [R32 report:41–45](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research32/REPORT.md:41)
|
||||
|
||||
So the defensible conclusion is **native FI is not yet demonstrated to be a speed fix**. I would revise that if a paired owner benchmark held camera, tile counts and clip settings fixed, logged `DensityDispatchPathAt`, and attributed time to field sampling, grid classification, meshing/normals and skirts. If native dispatch cut the same owner cold view to about 1 s without changing passage audits, that would disprove the current caution.
|
||||
|
||||
### Passage and landing risk
|
||||
|
||||
**Yes, a TN field migration could break a contract that L66–L68 just made pass.** The shared composer consumes the generator field before applying the boundary bands and passage carves; native graph caves also skip the legacy floor-cap stage. Changing the underlying field can therefore change support near a landing even if the passage plan and composer code stay untouched. [ShapeWorldInternal.h:1892–1920](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1892)
|
||||
|
||||
L68’s three interfaces had 0 blocked, unsupported and grade failures, with wall ripple ≤3.5 cm and floor ripple ≤2.5 cm. [L68 report:11–21](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux68/REPORT.md:11) Keeping the passage configuration fixed is necessary, but it does not prove the changed generator field preserves those results.
|
||||
|
||||
## First-round brief for the implementer
|
||||
|
||||
**Goal:** Add an explicitly opt-in native TN path for the owner’s current TN stratum. Compile its existing generation settings into a versioned native cave plan; keep legacy TN as the default until the owner plan passes all gates below. Do not change the shared passage planner or composer in this round.
|
||||
|
||||
**Invariants:**
|
||||
|
||||
- Account for every non-default owner TN setting in a field-by-field mapping. If the native graph cannot represent one, extend the recipe or reject native opt-in and leave legacy active. Do not silently drop it.
|
||||
- Hold the owner’s seed, strate bounds, resolved passage configuration, passage route and shared composition code fixed.
|
||||
- Keep FI and VS dispatch unchanged.
|
||||
- Do not promote TN if its changed field alters a landing or route contract.
|
||||
|
||||
**Evidence required before enabling it on the owner asset:**
|
||||
|
||||
- `DensityDispatchPathAt` reports `Caves-native` throughout the TN stratum; record the resolved manifest/layout identity and graph plan used.
|
||||
- Repeat the **full L68 passage audit across all three interfaces**, not just TN→VS: zero blocked/unsupported/grade failures and the same wall/floor ripple limits. Compare route and landing geometry with the fixed L68 plan.
|
||||
- Recheck reachability, headroom and grade, plus interval boxes against brute-force samples; false all-air/all-solid controls must still be rejected.
|
||||
- Verify deterministic hashes at W1/W8/W12 on both compilers.
|
||||
- Run five interleaved cold owner whole-view repetitions with stable tile counts; target a median at or below 1.0 s and report moving p50/p95 against the legacy baseline.
|
||||
|
||||
Until those checks pass, native TN should remain an opt-in canary and the current owner recipe should remain on legacy dispatch.
|
||||