diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index a00ac38..6e6d4c4 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -17982,6 +17982,25 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin OutError = TEXT("Could not create the BuildHost stream-trace pawn."); return false; } + FVector StartupTraceSpawnCm = FVector::ZeroVector; + FString StartupTraceSpawnText; + if (FParse::Value(FCommandLine::Get(), TEXT("voxel.StartupTraceSpawnCm="), + StartupTraceSpawnText, false)) + { + TArray Components; + StartupTraceSpawnText.ParseIntoArray(Components, TEXT(","), true); + if (Components.Num() != 3 + || !LexTryParseString(StartupTraceSpawnCm.X, *Components[0]) + || !LexTryParseString(StartupTraceSpawnCm.Y, *Components[1]) + || !LexTryParseString(StartupTraceSpawnCm.Z, *Components[2]) + || !FMath::IsFinite(StartupTraceSpawnCm.X) + || !FMath::IsFinite(StartupTraceSpawnCm.Y) + || !FMath::IsFinite(StartupTraceSpawnCm.Z)) + { + OutError = TEXT("voxel.StartupTraceSpawnCm must be finite X,Y,Z centimetres."); + return false; + } + } USceneComponent* PawnRoot = NewObject(PlayerPawn, NAME_None, RF_Transient); if (PawnRoot == nullptr) { @@ -17990,7 +18009,7 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin } PlayerPawn->SetRootComponent(PawnRoot); PawnRoot->RegisterComponent(); - PlayerPawn->SetActorLocation(FVector::ZeroVector, false, nullptr, ETeleportType::TeleportPhysics); + PlayerPawn->SetActorLocation(StartupTraceSpawnCm, false, nullptr, ETeleportType::TeleportPhysics); PlayerController->Possess(PlayerPawn); if (TestWorld->GetFirstPlayerController() != PlayerController || PlayerController->GetPawn() != PlayerPawn) @@ -18029,6 +18048,11 @@ bool RunCoreWorldActorIntegrationTest(const FExploreArguments& Arguments, FStrin return false; } Actor->SetActorTickEnabled(true); + VoxelForgeStartupTrace::RecordEvent(TEXT("trace_spawn_location"), FString::Printf( + TEXT("\"location_cm\":[%s,%s,%s]"), + *FString::SanitizeFloat(StartupTraceSpawnCm.X), + *FString::SanitizeFloat(StartupTraceSpawnCm.Y), + *FString::SanitizeFloat(StartupTraceSpawnCm.Z))); auto DesiredTilesLoaded = [](const AVoxelWorld* World) { diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/CaptureOwnerStratesL69.py b/Standalone/Research/WorldAssemblerM3/Linux69/CaptureOwnerStratesL69.py new file mode 100644 index 0000000..3e4322d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux69/CaptureOwnerStratesL69.py @@ -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(" 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(" 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") + + diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux69/REPORT.md new file mode 100644 index 0000000..b7c1d98 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux69/REPORT.md @@ -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. diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/RunEngineSpawnTracesL69.py b/Standalone/Research/WorldAssemblerM3/Linux69/RunEngineSpawnTracesL69.py new file mode 100644 index 0000000..dfaf925 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux69/RunEngineSpawnTracesL69.py @@ -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() diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-arrival.jpg new file mode 100644 index 0000000..b0bfccb Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-departure-mouth.jpg new file mode 100644 index 0000000..4f1315a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-overview.jpg new file mode 100644 index 0000000..c2d5a5f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-overview.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-route.jpg new file mode 100644 index 0000000..0f49914 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate0-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-arrival.jpg new file mode 100644 index 0000000..9b52ce4 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-departure-mouth.jpg new file mode 100644 index 0000000..cd3b956 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-overview.jpg new file mode 100644 index 0000000..1066f09 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-overview.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-route.jpg new file mode 100644 index 0000000..e965b30 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate1-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-arrival.jpg new file mode 100644 index 0000000..e40bf14 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-departure-mouth.jpg new file mode 100644 index 0000000..cfe6f13 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-overview.jpg new file mode 100644 index 0000000..a17756a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-overview.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-route.jpg new file mode 100644 index 0000000..4864e6b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate2-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-arrival.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-arrival.jpg new file mode 100644 index 0000000..d34b338 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-arrival.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-departure-mouth.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-departure-mouth.jpg new file mode 100644 index 0000000..ff40632 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-departure-mouth.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-overview.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-overview.jpg new file mode 100644 index 0000000..53727bc Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-overview.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-route.jpg b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-route.jpg new file mode 100644 index 0000000..e66b1be Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux69/captures/strate3-route.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Research36/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research36/REPORT.md new file mode 100644 index 0000000..13218d9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research36/REPORT.md @@ -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. \ No newline at end of file diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index ec306a5..52b614a 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -794,7 +794,9 @@ void main() glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,Material))); glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST); - glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f); + // A covered empty ray represents open air. Keep it visibly blue so it cannot + // be mistaken for the old near-black result from a scene that lacked tiles. + glDepthFunc(GL_LEQUAL);glClearColor(0.30f,0.48f,0.62f,1.0f); glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glUseProgram(Program); glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast(TanHalfHorizontal)); diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index c62db60..592b9fc 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -82,6 +82,7 @@ namespace bool HasCamera=false; bool HasCameraStep=false; bool AutoSurfaceCamera=false; + bool CaptureSkipWalkability=false; FVector CameraMeters=FVector::ZeroVector; FVector CameraStepMeters=FVector::ZeroVector; float CameraYawDegrees=0.0f; @@ -1410,6 +1411,7 @@ namespace " --region-crossing (compile entering Core regions at the camera step)\n" " --column-cache=shared|thread-local (default: shared)\n" " --mesh-export=FILE (write the exact generated tile meshes for offline rendering)\n" + " --capture-skip-walkability (mesh export only; defer the owner-wide passage gate)\n" " --world-assembler-manifest-output=FILE (write the canonical Core recipe and plan)\n" " --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n" " --seed=N --recipe=surface-world|floating-islands|caves|lattice\n" @@ -1589,6 +1591,7 @@ namespace if(options.MeshExportPath.empty()) {std::fprintf(stderr,"--mesh-export needs a file path\n");return false;} } + else if(arg=="--capture-skip-walkability")options.CaptureSkipWalkability=true; else if(arg.starts_with("--workers=")) {if(!readInt("--workers=",options.WorkerCount))return false;} else if(arg.starts_with("--clip-radius=")) @@ -1626,6 +1629,8 @@ namespace {std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;} if(options.LatticeRecipe&&options.LatticeStrateIndex<0) {std::fprintf(stderr,"--recipe=lattice requires an explicit --lattice-strate=N selection\n");return false;} + if(options.CaptureSkipWalkability&&options.MeshExportPath.empty()) + {std::fprintf(stderr,"--capture-skip-walkability requires --mesh-export=FILE\n");return false;} if((options.AuditNativeLattice||options.LatticeOrphanControl)&&!options.LatticeRecipe) {std::fprintf(stderr,"native lattice audits require --recipe=lattice\n");return false;} if(options.TlsProbeRequested)return true; @@ -1716,12 +1721,31 @@ int main(int argc,char** argv) const auto planStart=Clock::now(); VoxelForgeCore::WorldAssemblyManifest worldAssembly; if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype( - *ownerWorld,ownerJsonRoot,error,options.WorldAssemblyManifestOutputPath.string(),&worldAssembly)) + *ownerWorld,ownerJsonRoot,error,{},&worldAssembly)) {std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;} if(options.FloatingIslandsRecipe &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly( *ownerWorld,ownerJsonRoot,worldAssembly,error)) {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;} + if(!options.WorldAssemblyManifestOutputPath.empty()) + { + const std::string manifestText=worldAssembly.ToJson(); + const auto& outputPath=options.WorldAssemblyManifestOutputPath; + std::error_code directoryError; + if(!outputPath.parent_path().empty()) + std::filesystem::create_directories(outputPath.parent_path(),directoryError); + if(directoryError) + {std::fprintf(stderr,"Could not create the complete world assembly manifest output directory.\n");return 2;} + std::ofstream output(outputPath,std::ios::binary); + if(!output) + {std::fprintf(stderr,"Could not open the complete world assembly manifest output path.\n");return 2;} + output.write(manifestText.data(),static_cast(manifestText.size())); + if(!output) + {std::fprintf(stderr,"Could not write the complete world assembly manifest payload.\n");return 2;} + std::printf("[WorldAssemblerFullManifest] bytes=%zu surface_strates=%zu floating_islands=%zu path=%s status=PASS\n", + manifestText.size(),worldAssembly.plan.strates.size(),worldAssembly.floatingIslands.size(), + outputPath.string().c_str()); + } if (options.AuditHeightfieldProfile && !RunHeightfieldProfileAudit(worldAssembly)) { std::fprintf(stderr,"Profiled SurfaceWorld interval, classification or route audit failed.\n"); @@ -2159,5 +2183,6 @@ int main(int argc,char** argv) options.ExactMidpointDensityMemo,options.ActiveLaneBatching, options.ProofBlockCells,options.HasSignEdgeRepairOverride ?options.ParallelSignEdgeRepair:options.FloatingIslandsRecipe, - options.AuditNativeLattice,options.LatticeOrphanControl); + options.AuditNativeLattice,options.LatticeOrphanControl, + options.CaptureSkipWalkability); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 347cbf8..3fa2aec 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -1872,7 +1872,7 @@ namespace StandaloneStreamBenchmark bool bExactMidpointDensityMemo=false, bool bActiveLaneBatching=false,int32 ProofBlockCells=8, bool bParallelSignEdgeRepair=false,bool bAuditNativeLattice=false, - bool bLatticeOrphanControl=false) + bool bLatticeOrphanControl=false,bool bCaptureSkipWalkability=false) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -2029,6 +2029,9 @@ namespace StandaloneStreamBenchmark ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld; ShapeRequest.roundTunnels=bRoundTunnels; ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls; + ShapeRequest.validatePassageRoutes=!bCaptureSkipWalkability; + if(bCaptureSkipWalkability) + std::printf("[CaptureWalkabilityGate] validation=deferred geometry_input=unchanged mode=mesh-export\n"); FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel, bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape,bProfileColumnOverlap, FarHorizonLOD8InteriorStepVoxels,bUseSharedColumnCache);