diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index bb96727..422cdf2 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -968,6 +968,7 @@ namespace VoxelForgeCore inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV5 = 5; inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV6 = 6; + inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV7 = 7; inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1; inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1; enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict }; @@ -982,7 +983,10 @@ namespace VoxelForgeCore fillDais = 4, fillMonolith = 5, fillPlatform = 6, - fillSweptDeckSegment = 7 + fillSweptDeckSegment = 7, + fillBox = 8, + carveBox = 9, + fillSteps = 10 }; struct ResolvedLayoutStampRequestV1 @@ -1005,6 +1009,9 @@ namespace VoxelForgeCore std::int64_t irregularityWavelengthMicrometres = 0; std::int64_t roundingRadiusMicrometres = 0; std::int64_t minimumSpacingMicrometres = 0; + // Vocabulary 7: fillSteps divides the X and Z extents into this many + // identical treads and risers. Other stamp kinds keep this at zero. + std::uint16_t stepCount = 0; // 0 = free placement, 1 = seated on the authored flat profile floor. std::uint8_t attachmentPolicy = 0; std::int64_t floorMicrometres = 0; @@ -1027,6 +1034,7 @@ namespace VoxelForgeCore std::int64_t irregularityWavelengthMicrometres = 0; std::int64_t roundingRadiusMicrometres = 0; std::int64_t minimumSpacingMicrometres = 0; + std::uint16_t stepCount = 0; std::uint8_t attachmentPolicy = 0; std::int64_t floorMicrometres = 0; std::uint64_t irregularitySeed = 0; diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index 813a129..d4fea3d 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -176,7 +176,7 @@ namespace { const auto value=static_cast(kind); return value>=static_cast(ResolvedSdfStampKindV1::carveEllipsoid) - &&value<=static_cast(ResolvedSdfStampKindV1::fillSweptDeckSegment); + &&value<=static_cast(ResolvedSdfStampKindV1::fillSteps); } bool LayoutStampIsMotif(ResolvedSdfStampKindV1 kind) {return kind==ResolvedSdfStampKindV1::fillMonolith @@ -711,6 +711,8 @@ namespace <<",\"minimum_spacing_um\":"<(stamp.attachmentPolicy) <<",\"floor_um\":"< stampIds; for(const auto& stamp:stamps) { @@ -1292,8 +1295,18 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode, if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3 ||(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment &&stampVocabularyVersion128 + ||stamp.roundingRadiusMicrometres!=0)) + ||(stamp.kind!=ResolvedSdfStampKindV1::fillSteps&&stamp.stepCount!=0)) + add("stamp_steps",stamp.stableId+" has an invalid step count or rounding mode"); bool validExtents=true; for(const auto extent:stamp.halfExtentsMicrometres) validExtents=validExtents&&extent>=500000&&extent<=100000000; @@ -1777,6 +1790,7 @@ std::optional ResolvedLayoutSnapshotV3::FromJson( stamp.floorMicrometres=LayoutInt(v,"floor_um"); stamp.irregularitySeed=static_cast(LayoutInt(v,"irregularity_seed")); stamp.yawQuarterTurns=static_cast(LayoutInt(v,"yaw_quarter_turns")); + stamp.stepCount=static_cast(LayoutInt(v,"step_count")); s.stamps.push_back(std::move(stamp)); } for(const auto& v:root.Get("cavern_rooms").AsArray()) @@ -1901,7 +1915,13 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t }); const bool hasSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request) {return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment;}); - candidate.stampVocabularyVersion=hasContinuousSweptDeck + const bool hasVocabulary7Stamp=std::any_of(ordered.begin(),ordered.end(),[](const auto& request) + {return request.kind==ResolvedSdfStampKindV1::fillBox + ||request.kind==ResolvedSdfStampKindV1::carveBox + ||request.kind==ResolvedSdfStampKindV1::fillSteps;}); + candidate.stampVocabularyVersion=hasVocabulary7Stamp + ?ResolvedLayoutStampVocabularyVersionV7 + :hasContinuousSweptDeck ?(hasNonMonotoneSweptDeck?ResolvedLayoutStampVocabularyVersionV6 :ResolvedLayoutStampVocabularyVersionV5) :hasSweptDeck?ResolvedLayoutStampVocabularyVersionV4 @@ -1914,6 +1934,10 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t &&ordered[requestIndex-1].stableId==request.stableId)) {error="stamp requests require non-empty unique stable IDs";return false;} if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3 + ||(request.kind==ResolvedSdfStampKindV1::fillSteps + ?(request.stepCount<2||request.stepCount>128 + ||request.roundingRadiusMicrometres!=0) + :request.stepCount!=0) ||request.placementRadiusMicrometres<0 ||request.placementRadiusMicrometres>100000000 ||(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment @@ -1998,6 +2022,7 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t stamp.smoothWidthMicrometres=request.smoothWidthMicrometres; stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres; stamp.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres; + stamp.stepCount=request.stepCount; stamp.irregularitySeed=baseSeed;stamp.yawQuarterTurns=request.yawQuarterTurns; std::array offset{}; if(request.placementRadiusMicrometres>0) diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 29d4977..768fb94 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -7157,6 +7157,23 @@ private: const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z); return SmoothMinDistance(lower,top,round*2.0f); } + static float StampStepsSdf(float x,float y,float z, + const std::array& halfSize,std::uint16_t stepCount) + { + if(stepCount<2||stepCount>128)return std::numeric_limits::infinity(); + const float treadWidth=(2.0f*halfSize[0])/stepCount; + const float riserHeight=(2.0f*halfSize[2])/stepCount; + float distance=std::numeric_limits::infinity(); + for(std::uint16_t step=0;step extent={treadWidth*0.5f,halfSize[1],stepHeight*0.5f}; + distance=std::min(distance,StampRoundedBoxSdf(x-centerX,y,z-centerZ,extent,0.0f)); + } + return distance; + } static float StampContinuousSweptDeckSdf(const FStampRuntime& stamp, float x,float y,float z) { @@ -7295,6 +7312,13 @@ private: case VoxelForgeCore::ResolvedSdfStampKindV1::fillPlatform: distance=StampRoundedBoxSdf(localX,localY,localZ, stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break; + case VoxelForgeCore::ResolvedSdfStampKindV1::fillBox: + case VoxelForgeCore::ResolvedSdfStampKindV1::carveBox: + distance=StampRoundedBoxSdf(localX,localY,localZ, + stamp.halfExtentsVoxels,stamp.roundingRadiusVoxels);break; + case VoxelForgeCore::ResolvedSdfStampKindV1::fillSteps: + distance=StampStepsSdf(localX,localY,localZ,stamp.halfExtentsVoxels, + stamp.record->stepCount);break; case VoxelForgeCore::ResolvedSdfStampKindV1::fillDais: if(ResolvedLayout&&ResolvedLayout->stampVocabularyVersion >=VoxelForgeCore::ResolvedLayoutStampVocabularyVersionV2) @@ -7471,7 +7495,8 @@ private: ||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return; const float distance=StampSdf(runtime,X,Y,Z); if(record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveEllipsoid - ||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch) + ||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveArch + ||record.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveBox) composed=SmoothMaxDistance(composed,-distance,runtime.smoothWidthVoxels); else composed=SmoothMinDistance(composed,distance,runtime.smoothWidthVoxels); diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/BuildL161Fixtures.py b/Standalone/Research/WorldAssemblerM3/Linux161/BuildL161Fixtures.py new file mode 100644 index 0000000..985d3ed --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux161/BuildL161Fixtures.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +"""Build four standalone liminal geometry fixtures and their capsule-walk specs.""" +from __future__ import annotations + +import argparse +import json +import os +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[4] +OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA", + ROOT / "Standalone/data/owner_world.json")) +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l161")) +FLAT_FIELD = {"version": 1, "kind": "flatField", "minimum_arena_clearance_m": 0.0} + + +def stamp(stamps: list[dict], stable_id: str, kind: str, x: float, y: float, + z: float, half: tuple[float, float, float], *, yaw: int = 0, + steps: int | None = None) -> None: + row = { + "stable_id": stable_id, + "kind": kind, + "anchor_xy_m": [x, y], + "z_offset_m": z, + "half_extents_m": list(half), + "yaw_quarter_turns": yaw, + } + if steps is not None: + row["step_count"] = steps + stamps.append(row) + + +def fixture(name: str, stamps: list[dict], walk_points: list[tuple[float, float, float]], + doors: list[dict], *, center: tuple[float, float], wide_camera: tuple[float, float, float], + eye_camera: tuple[float, float, float], eye_target_offset: tuple[float, float, float], + wide_target_offset: tuple[float, float, float], + eye_clip: int, wide_clip: int) -> tuple[str, dict, dict]: + root = json.loads(OWNER.read_text(encoding="utf-8")) + root["surface_profile_override"] = dict(FLAT_FIELD) + root["liminal_stamp_fixture_override"] = { + "version": 1, + "strate_index": 0, + "stamps": stamps, + } + walk = { + "version": 1, + "path_points_offset_m": [list(point) for point in walk_points], + "door_prefixes": doors, + } + views = { + "center_xy_m": list(center), + "eye_camera_offset_m": list(eye_camera), + "wide_camera_offset_m": list(wide_camera), + "eye_target_offset_m": list(eye_target_offset), + "wide_target_offset_m": list(wide_target_offset), + "eye_clip_radius": eye_clip, + "wide_clip_radius": wide_clip, + } + return name, root, {"walk": walk, "views": views} + + +def pool_rooms() -> tuple[str, dict, dict]: + stamps: list[dict] = [] + cx, cy, cell, cells = 520.0, 440.0, 20.0, 7 + half = cell * cells * 0.5 + wall_height, wall_half_thickness = 9.0, 1.2 + for line in range(cells + 1): + offset = (line - cells * 0.5) * cell + stamp(stamps, f"liminal/pool-rooms-v1/00-wall-x/{line:02d}", "fillBox", + cx + offset, cy, wall_height * 0.5, + (wall_half_thickness, half, wall_height * 0.5)) + stamp(stamps, f"liminal/pool-rooms-v1/00-wall-y/{line:02d}", "fillBox", + cx, cy + offset, wall_height * 0.5, + (half, wall_half_thickness, wall_height * 0.5)) + for row in range(cells): + y = cy + (row - (cells - 1) * 0.5) * cell + for line in range(cells + 1): + x = cx + (line - cells * 0.5) * cell + stamp(stamps, f"liminal/pool-rooms-v1/01-doorway-x/{row:02d}-{line:02d}", + "carveArch", x, y + 7.0, 4.0, (3.6, 1.55, 4.0), yaw=1) + stamp(stamps, f"liminal/pool-rooms-v1/01-doorway-y/{line:02d}-{row:02d}", + "carveArch", cx + (row - (cells - 1) * 0.5) * cell, + cy + (line - cells * 0.5) * cell, 4.0, + (3.6, 1.55, 4.0)) + for row in range(cells): + y = cy + (row - (cells - 1) * 0.5) * cell + for column in range(cells): + x = cx + (column - (cells - 1) * 0.5) * cell + stamp(stamps, f"liminal/pool-rooms-v1/02-roof/{row:02d}-{column:02d}", + "fillBox", x, y, 9.4, (10.0, 10.0, 0.5)) + stamp(stamps, f"liminal/pool-rooms-v1/03-skylight/{row:02d}-{column:02d}", + "carveBox", x, y, 9.4, (5.5, 5.5, 1.0)) + stamp(stamps, f"liminal/pool-rooms-v1/04-basin/{row:02d}-{column:02d}", + "carveBox", x, y, -1.0, (5.5, 5.5, 1.5)) + path_y = cy + 7.0 + walk = [(-half - 2.0 + cx, path_y, 0.88), (half + 2.0 + cx, path_y, 0.88)] + doors = [ + {"prefix": "liminal/pool-rooms-v1/01-doorway-x/", "width_axis": 0, + "minimum_width_m": 1.5}, + {"prefix": "liminal/pool-rooms-v1/01-doorway-y/", "width_axis": 0, + "minimum_width_m": 1.5}, + ] + return fixture("pool-rooms", stamps, walk, doors, center=(cx, cy), + wide_camera=(-60.0, 7.0, 3.0), eye_camera=(-40.0, 7.0, 1.6), + eye_target_offset=(60.0, 7.0, 0.8), + wide_target_offset=(65.0, 7.0, 1.2), eye_clip=4, wide_clip=7) + + +def backrooms(width: float = 4.0, name: str = "backrooms-grid") -> tuple[str, dict, dict]: + stamps: list[dict] = [] + cx, cy, cell, cells = 820.0, 420.0, 18.0, 7 + half, wall_half = cell * cells * 0.5, 1.2 + wall_height = 5.5 + for line in range(cells + 1): + offset = (line - cells * 0.5) * cell + stamp(stamps, f"liminal/backrooms-grid-v1/00-wall-x/{line:02d}", "fillBox", + cx + offset, cy, wall_height * 0.5, + (wall_half, half, wall_height * 0.5)) + stamp(stamps, f"liminal/backrooms-grid-v1/00-wall-y/{line:02d}", "fillBox", + cx, cy + offset, wall_height * 0.5, + (half, wall_half, wall_height * 0.5)) + door_half = width * 0.5 + for row in range(cells): + y = cy + (row - (cells - 1) * 0.5) * cell + for line in range(cells + 1): + x = cx + (line - cells * 0.5) * cell + stamp(stamps, f"liminal/backrooms-grid-v1/01-door-x/{row:02d}-{line:02d}", + "carveBox", x, y, 1.25, (1.6, door_half, 1.25)) + stamp(stamps, f"liminal/backrooms-grid-v1/01-door-y/{line:02d}-{row:02d}", + "carveBox", cx + (row - (cells - 1) * 0.5) * cell, + cy + (line - cells * 0.5) * cell, 1.25, + (door_half, 1.6, 1.25)) + stamp(stamps, "liminal/backrooms-grid-v1/02-ceiling", "fillBox", + cx, cy, 5.9, (half, half, 0.5)) + walk = [(cx - half - 2.0, cy, 0.88), (cx + half + 2.0, cy, 0.88)] + doors = [ + {"prefix": "liminal/backrooms-grid-v1/01-door-x/", "width_axis": 1, + "minimum_width_m": 1.5}, + {"prefix": "liminal/backrooms-grid-v1/01-door-y/", "width_axis": 0, + "minimum_width_m": 1.5}, + ] + return fixture(name, stamps, walk, doors, center=(cx, cy), + wide_camera=(-42.0, -6.0, 1.6), eye_camera=(-54.0, 0.0, 1.6), + eye_target_offset=(102.0, 0.0, 1.6), + wide_target_offset=(80.0, 20.0, 0.0), eye_clip=6, wide_clip=6) + + +def nowhere_stairs() -> tuple[str, dict, dict]: + stamps: list[dict] = [] + cx, cy = 300.0, -760.0 + stamp(stamps, "liminal/staircase-nowhere-v1/00-stair", "fillSteps", + cx, cy, 6.0, (32.0, 4.0, 6.0), steps=64) + stamp(stamps, "liminal/staircase-nowhere-v1/04-landing", "fillBox", + cx + 40.5, cy, 11.5, (9.0, 4.5, 0.5)) + stamp(stamps, "liminal/staircase-nowhere-v1/02-arch-wall", "fillBox", + cx + 38.0, cy, 7.5, (1.2, 6.5, 7.5)) + stamp(stamps, "liminal/staircase-nowhere-v1/03-doorway-arch", "carveArch", + cx + 38.0, cy, 7.0, (5.0, 5.0, 7.0), yaw=1) + # Begin high enough to clear the first riser with the audit capsule, then + # walk the same ascending staircase through the arch and onto the landing. + points = [(cx - 32.5, cy, 0.88 + 12.0 / 64.0 + 0.05)] + tread, rise = 64.0 / 64, 12.0 / 64 + for index in range(64): + points.append((cx - 32.0 + (index + 0.5) * tread, + cy, 0.88 + (index + 1) * rise + 0.05)) + points.extend([(cx + 34.0, cy, 12.93), (cx + 38.0, cy, 12.93), + (cx + 44.0, cy, 12.93)]) + doors = [{"prefix": "liminal/staircase-nowhere-v1/03-doorway-arch", "width_axis": 0, + "minimum_width_m": 1.5}] + return fixture("staircase-to-nowhere", stamps, points, doors, center=(cx, cy), + wide_camera=(-8.0, -32.0, 36.0), eye_camera=(10.0, 0.0, 9.7), + eye_target_offset=(38.0, 0.0, 13.6), + wide_target_offset=(42.0, 0.0, 11.0), eye_clip=4, wide_clip=6) + + +def terraced_hills() -> tuple[str, dict, dict]: + stamps: list[dict] = [] + cx, cy, rows, spacing = -520.0, -500.0, 9, 96.0 + half_run, half_width, half_rise, steps = 48.0, 40.0, 8.0, 80 + for row in range(rows): + y = cy + (row - (rows - 1) * 0.5) * spacing + stamp(stamps, f"liminal/terraced-hills-v1/ridge/{row:02d}/rise-west", + "fillSteps", cx - half_run, y, half_rise, + (half_run, half_width, half_rise), steps=steps) + stamp(stamps, f"liminal/terraced-hills-v1/ridge/{row:02d}/rise-east", + "fillSteps", cx + half_run, y, half_rise, + (half_run, half_width, half_rise), yaw=2, steps=steps) + points = [(cx - 100.0, cy, 0.93)] + tread, rise = (2.0 * half_run) / steps, (2.0 * half_rise) / steps + for index in range(steps): + points.append((cx - 2.0 * half_run + (index + 0.5) * tread, + cy, 0.93 + (index + 1) * rise)) + for index in reversed(range(steps)): + points.append((cx + 2.0 * half_run - (index + 0.5) * tread, + cy, 0.93 + (index + 1) * rise)) + points.append((cx + 100.0, cy, 0.93)) + return fixture("terraced-hills", stamps, points, [], center=(cx, cy), + wide_camera=(-40.0, 0.0, 20.0), eye_camera=(-60.0, 0.0, 7.8), + eye_target_offset=(0.0, 0.0, 14.5), + wide_target_offset=(100.0, 0.0, -10.0), eye_clip=4, wide_clip=3) + + +def write(path: Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures") + args = parser.parse_args() + args.out.mkdir(parents=True, exist_ok=True) + specs = args.out / "walk-specs" + fixtures = [pool_rooms(), backrooms(), nowhere_stairs(), terraced_hills()] + fixtures.append(backrooms(width=1.2, name="backrooms-narrow-control")) + for name, world, metadata in fixtures: + write(args.out / f"liminal-{name}.json", world) + write(specs / f"{name}.json", metadata["walk"]) + write(args.out / "views" / f"{name}.json", metadata["views"]) + print(f"[L161Fixture] name={name} stamps=" + f"{len(world['liminal_stamp_fixture_override']['stamps'])} status=PASS") + + +if __name__ == "__main__": + main() diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/CaptureL161.py b/Standalone/Research/WorldAssemblerM3/Linux161/CaptureL161.py new file mode 100644 index 0000000..9babcaa --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux161/CaptureL161.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python3 +"""Run L161 stamp-fixture determinism, walk audits and two-view captures.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import os +from pathlib import Path +import re +import subprocess +import sys +import time + + +ROOT = Path(__file__).resolve().parents[4] +OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l161")) +CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures" +LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"] +Q_PER_METRE = 1024.0 +FOG_COLOR = "0.78,0.84,0.87" + +sys.path.insert(0, str(ROOT / "Standalone/Tools/verify")) +from runner import current_stamp, first_stamp + + +def fmt(values) -> str: + return ",".join(f"{value:.6f}" for value in values) + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str], + stamped: bool = False, expected_exit: int = 0, + timeout_seconds: int = 1740) -> str: + expected_stamp = current_stamp() if stamped else None + if stamped: + probe = subprocess.run([str(command[0]), "--print-source-stamp"], cwd=ROOT, + capture_output=True, text=True, timeout=15, check=False) + if probe.returncode != 0 or first_stamp(probe.stdout) != expected_stamp: + raise RuntimeError(f"{name} refused a stale executable: {command[0]}") + log.parent.mkdir(parents=True, exist_ok=True) + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run([*LIMIT, "timeout", f"{timeout_seconds}s", + *map(str, command)], cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, timeout=timeout_seconds + 5, check=False) + elapsed = time.monotonic() - started + output = log.read_text(encoding="utf-8", errors="replace") + print(f"[L161Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}", + flush=True) + if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp): + raise RuntimeError(f"{name} source stamp mismatch; see {log}") + if result.returncode != expected_exit: + raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}") + return output + + +def camera_for(view: dict, floor: float, name: str, kind: str) -> tuple[float, float, float]: + center_x, center_y = view["center_xy_m"] + offset = view[f"{kind}_camera_offset_m"] + z = floor + offset[2] + if name == "backrooms-grid" or name == "backrooms-narrow-control": + z = floor + offset[2] + return center_x + offset[0], center_y + offset[1], z + + +def target_for(view: dict, floor: float, name: str, kind: str) -> tuple[float, float, float]: + center_x, center_y = view["center_xy_m"] + offset = view[f"{kind}_target_offset_m"] + return center_x + offset[0], center_y + offset[1], floor + offset[2] + + +def headless_command(binary: Path, fixture: Path, camera, mesh: Path, + manifest: Path, layout: Path, workers: int, clip_radius: int, + *, max_level: int = 0, hash_tiles: bool = False, + audit_path: Path | None = None) -> list[object]: + command: list[object] = [binary, fixture, f"--workers={workers}", "--seed=0", + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--force-generate", f"--clip-radius={clip_radius}", + f"--max-clip-level={max_level}", f"--max-level={max_level}", + "--render-distance-chunks=0", + f"--camera={fmt((*camera, 0.0, 0.0))}", f"--mesh-export={mesh}", + f"--world-assembler-manifest-output={manifest}", + f"--resolved-layout-output={layout}"] + if hash_tiles: + command.append("--stream-hash-tiles") + if audit_path is not None: + command.append(f"--audit-liminal-stamp-walk={audit_path}") + return command + + +def digest(output: str, manifest_path: Path, layout_path: Path) -> tuple[str, str, str]: + tile_hashes = sorted(line for line in output.splitlines() + if line.startswith("[PipelineTileHash] ")) + if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes: + raise RuntimeError("determinism run omitted manifest, resolved layout or tile hashes") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + layout = json.loads(layout_path.read_text(encoding="utf-8")) + manifest_digest = "|".join((manifest["content_hash"], + manifest["recipe"]["content_hash"], manifest["plan"]["content_hash"])) + return manifest_digest, layout["stable_identity"], "\n".join(tile_hashes) + + +def write_walk_audit(spec_path: Path, manifest_path: Path, audit_path: Path) -> None: + spec = json.loads(spec_path.read_text(encoding="utf-8")) + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + surface = manifest["recipe"]["surface_profile"] + floor = surface["h0_q"] / Q_PER_METRE + lines = ["# P records are world metres; doorway width axes use the resolved stamp's local extents."] + for gate in spec["door_prefixes"]: + lines.append(f"D {gate['prefix']} {gate['width_axis']} {gate['minimum_width_m']:.6f}") + for x, y, z_offset in spec["path_points_offset_m"]: + lines.append(f"P {x:.6f} {y:.6f} {floor + z_offset:.6f}") + audit_path.parent.mkdir(parents=True, exist_ok=True) + audit_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def camera_angles(camera, target) -> tuple[float, float]: + dx, dy, dz = (target[index] - camera[index] for index in range(3)) + return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy))) + + +def build_plan(binary: Path, name: str, fixture: Path, view: dict, + out: Path, env: dict[str, str]) -> tuple[Path, Path, dict, dict]: + run_dir = out / "plan-probes" / name + run_dir.mkdir(parents=True, exist_ok=True) + manifest, layout = run_dir / "manifest.json", run_dir / "layout.json" + mesh = run_dir / "probe.vfmesh" + camera = (view["center_xy_m"][0], view["center_xy_m"][1], -500.0) + output = run_step(f"plan-{name}", headless_command(binary, fixture, camera, + mesh, manifest, layout, 12, 1), out / "logs" / f"plan-{name}.log", + env=env, stamped=True) + if "[LiminalStampFixtureResolved] vocabulary=7" not in output: + raise RuntimeError(f"{name} did not resolve with stamp vocabulary 7") + if not manifest.is_file() or not layout.is_file(): + raise RuntimeError(f"{name} plan probe omitted its outputs") + manifest_value = json.loads(manifest.read_text(encoding="utf-8")) + layout_value = json.loads(layout.read_text(encoding="utf-8")) + return manifest, layout, manifest_value, layout_value + + +def run_determinism(name: str, fixture: Path, view: dict, floor: float, + binaries: dict[str, Path], out: Path, env: dict[str, str]) -> None: + camera = camera_for(view, floor, name, "eye") + baseline = None + for compiler in ("clang", "gcc"): + for workers in (1, 8, 12): + tag = f"{name}-{compiler}-w{workers}" + run_dir = out / "determinism" / tag + run_dir.mkdir(parents=True, exist_ok=True) + mesh, manifest, layout = (run_dir / "fixture.vfmesh", + run_dir / "manifest.json", run_dir / "layout.json") + output = run_step(tag, headless_command(binaries[compiler], fixture, + camera, mesh, manifest, layout, workers, 1, + hash_tiles=True), out / "logs" / f"determinism-{tag}.log", + env=env, stamped=True) + result = digest(output, manifest, layout) + if baseline is None: + baseline = result + elif result != baseline: + raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs") + print(f"[L161Determinism] fixture={name} compiler_worker_cells=6 identical=yes status=PASS", + flush=True) + + +def run_walk_audit(name: str, fixture: Path, view: dict, floor: float, + spec: Path, binary: Path, out: Path, env: dict[str, str], + *, expected_exit: int = 0) -> str: + camera = camera_for(view, floor, name, "eye") + run_dir = out / "walk-audits" / name + run_dir.mkdir(parents=True, exist_ok=True) + manifest = run_dir / "manifest.json" + layout = run_dir / "layout.json" + mesh = run_dir / "audit.vfmesh" + audit_path = run_dir / "walk.path" + # Use a fixture-specific manifest, so the path uses the exact authored floor height. + plan = out / "plan-probes" / name / "manifest.json" + if not plan.is_file(): + raise RuntimeError(f"{name} has no resolved manifest for its walk audit") + write_walk_audit(spec, plan, audit_path) + output = run_step(f"walk-{name}", headless_command(binary, fixture, camera, + mesh, manifest, layout, 12, 1, audit_path=audit_path), + out / "logs" / f"walk-{name}.log", env=env, stamped=True, + expected_exit=expected_exit) + expected_status = "FAIL" if expected_exit else "PASS" + if not re.search(rf"\[LiminalStampWalkAudit\].*status={expected_status}\b", output): + raise RuntimeError(f"{name} walk audit did not report expected {expected_status}") + return output + + +def capture(name: str, fixture: Path, view: dict, floor: float, *, kind: str, + binary: Path, renderer: Path, out: Path, artifact_dir: Path, + env: dict[str, str]) -> None: + camera = camera_for(view, floor, name, kind) + target = target_for(view, floor, name, kind) + clip_radius = int(view[f"{kind}_clip_radius"]) + max_level = 1 if name == "terraced-hills" and kind == "wide" else 0 + stem = f"{name}-{kind}" + run_dir = out / "captures" / stem + run_dir.mkdir(parents=True, exist_ok=True) + mesh, manifest, layout = (run_dir / "capture.vfmesh", + run_dir / "manifest.json", run_dir / "layout.json") + png = run_dir / "capture.png" + image = artifact_dir / f"{stem}.jpg" + output = run_step(f"capture-export-{stem}", headless_command(binary, + fixture, camera, mesh, manifest, layout, 12, clip_radius, + max_level=max_level), out / "logs" / f"capture-export-{stem}.log", + env=env, stamped=True) + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{stem} produced an empty mesh") + yaw, pitch = camera_angles(camera, target) + if name == "backrooms-grid": + fog_start, fog_end, fog_color = 90, 500, "0.82,0.83,0.76" + elif name == "pool-rooms": + fog_start, fog_end, fog_color = 90, 480, "0.76,0.84,0.87" + elif name == "staircase-to-nowhere": + fog_start, fog_end, fog_color = 110, 700, "0.78,0.84,0.87" + else: + fog_start, fog_end, fog_color = 160, 1050, "0.78,0.84,0.89" + renderer_command = [renderer, mesh, png, "--size=1600x900", "--fov-deg=72", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=on", "--shade=readable", + "--headlamp=off", "--height-tint=on", + f"--height-center-m={floor + 3.0:.6f}", "--height-range-m=30.0", + "--light-direction=-0.42,-0.58,0.70", "--fog=on", + f"--fog-start-m={fog_start}", f"--fog-end-m={fog_end}", + f"--fog-color={fog_color}"] + run_step(f"capture-render-{stem}", renderer_command, + out / "logs" / f"capture-render-{stem}.log", env=env, stamped=True) + run_step(f"capture-jpeg-{stem}", ["ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image], + out / "logs" / f"capture-jpeg-{stem}.log", env=env) + quality = run_step(f"capture-quality-{stem}", [sys.executable, + ROOT / "Standalone/Tools/verify/capture_quality.py", image], + out / "logs" / f"capture-quality-{stem}.log", env=env) + if "[CaptureQuality]" not in quality or not image.is_file() or image.stat().st_size > 300_000: + raise RuntimeError(f"{stem} failed image quality or size checks") + print(f"[L161Capture] fixture={name} view={kind} camera_m=({fmt(camera)}) " + f"target_m=({fmt(target)}) bytes={image.stat().st_size} " + f"sha256={sha256(image)} status=PASS", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, default=OUT_DEFAULT) + parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT) + parser.add_argument("--headless-clang", type=Path, required=True) + parser.add_argument("--headless-gcc", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + args = parser.parse_args() + out, artifact_dir = args.out.resolve(), args.artifact_dir.resolve() + fixture_dir = out / "fixtures" + for directory in (out, fixture_dir, artifact_dir): + directory.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["VOXELFORGE_VERIFY_OUT"] = str(out) + env["VOXELFORGE_OWNER_DATA"] = str(ROOT / "Standalone/data/owner_world.json") + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out / "xdg-cache") + run_step("build-fixtures", [sys.executable, + Path(__file__).resolve().parent / "BuildL161Fixtures.py", + f"--out={fixture_dir}"], out / "logs" / "build-fixtures.log", env=env) + + binaries = {"clang": args.headless_clang.resolve(), "gcc": args.headless_gcc.resolve()} + fixtures = {name: fixture_dir / f"liminal-{name}.json" for name in + ("pool-rooms", "backrooms-grid", "staircase-to-nowhere", "terraced-hills")} + views = {} + floors = {} + for name, fixture_path in fixtures.items(): + view = json.loads((fixture_dir / "views" / f"{name}.json").read_text(encoding="utf-8")) + manifest_path, _, manifest, layout = build_plan(binaries["clang"], name, + fixture_path, view, out, env) + view["center_xy_m"] = view["center_xy_m"] + floors[name] = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE + if layout["stamp_vocabulary_version"] != 7 or not layout["stamps"]: + raise RuntimeError(f"{name} plan lacks resolved vocabulary-7 stamps") + write_walk_audit(fixture_dir / "walk-specs" / f"{name}.json", + manifest_path, out / "walk-audits" / name / "walk.path") + views[name] = view + print(f"[L161Plan] fixture={name} stamps={len(layout['stamps'])} " + f"floor_m={floors[name]:.3f} layout={layout['stable_identity']} status=PASS") + + for name, fixture_path in fixtures.items(): + run_walk_audit(name, fixture_path, views[name], floors[name], + fixture_dir / "walk-specs" / f"{name}.json", binaries["clang"], out, env) + run_determinism(name, fixture_path, views[name], floors[name], binaries, out, env) + + narrow_name = "backrooms-narrow-control" + narrow_fixture = fixture_dir / f"liminal-{narrow_name}.json" + narrow_view = json.loads((fixture_dir / "views" / f"{narrow_name}.json").read_text(encoding="utf-8")) + narrow_manifest, _, narrow_manifest_value, _ = build_plan(binaries["clang"], + narrow_name, narrow_fixture, narrow_view, out, env) + narrow_floor = narrow_manifest_value["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE + run_walk_audit(narrow_name, narrow_fixture, narrow_view, narrow_floor, + fixture_dir / "walk-specs" / f"{narrow_name}.json", binaries["clang"], + out, env, expected_exit=14) + print("[L161DoorwayNegativeControl] width_m=1.200 minimum_m=1.500 expected=FAIL status=PASS") + + for name, fixture_path in fixtures.items(): + for kind in ("eye", "wide"): + capture(name, fixture_path, views[name], floors[name], kind=kind, + binary=binaries["clang"], renderer=args.renderer.resolve(), out=out, + artifact_dir=artifact_dir, env=env) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L161Capture] status=FAIL error={error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux161/REPORT.md new file mode 100644 index 0000000..582c123 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux161/REPORT.md @@ -0,0 +1,31 @@ +# L161 — More liminal looks + +| Look | Eye height | Wide | +|---|---|---| +| Pool rooms | ![Pool rooms at eye height](captures/pool-rooms-eye.jpg) | ![Pool rooms wide](captures/pool-rooms-wide.jpg) | +| Backrooms grid | ![Backrooms corridor at eye height](captures/backrooms-grid-eye.jpg) | ![Backrooms grid wide](captures/backrooms-grid-wide.jpg) | +| Staircase to nowhere | ![Staircase at eye height](captures/staircase-to-nowhere-eye.jpg) | ![Staircase wide](captures/staircase-to-nowhere-wide.jpg) | +| Terraced hills | ![Terraced hills at eye height](captures/terraced-hills-eye.jpg) | ![Terraced hills wide](captures/terraced-hills-wide.jpg) | + +## Result + +Four standalone SurfaceWorld fixtures resolve from the shared stamp vocabulary. They use repeated rooms and basins, a doorway grid, a freestanding stair and arch, and nine stepped ridges. This round authored no inter-strate passages or owner content. + +## Changes + +- Stamp vocabulary v7 adds `fillBox`, `carveBox`, and `fillSteps`; the existing `carveArch` is reused. The new kinds are opt-in. +- Added a flat-field liminal stamp-fixture override and a capsule walk audit with doorway-width gates. The audit checks support, collision, grade, and authored opening widths. +- Fixtures contain 275 pool-room stamps, 129 backrooms stamps, 4 staircase/arch stamps, and 18 terrace stamps. + +## Verification + +- Clang 19 and GCC 14 builds passed with FP contraction off. Both binaries carry source stamp `8e9e114c1357`. +- Each fixture matched across Clang/GCC at W1, W8, and W12: manifest, resolved-layout identity, and tile hashes were identical in all six cells. +- Positive capsule audits passed. Pool doorways measured 7.2 m, backrooms doorways 4.0 m, and the arch opening 10.0 m. The narrow 1.2 m backrooms control failed the 1.5 m minimum as expected. The terrace walk had zero blocked samples, unsupported samples, or grade failures. +- All eight 1600×900 captures passed image quality checks; files are 25.3–58.4 KB. Capture and audit logs are under `Saved/Linux/l161/logs/`. +- Fast tier passed: 273 checks in 346.6 s. Both owner generators matched their pinned L130/v227 canonical baselines exactly (12/12 rows), and W1/W12 signatures matched. Summary: `Saved/Linux/l161/fast-tier/fast-summary.json`. +- `git diff --check` passed. No Unreal `Source/` files changed; no Unreal build or medium/full verification tier was run. + +## Remaining risk + +The captures show untextured geometry with simple fog. The terraced wide capture uses clip radius 3 and max LOD 1 to stay within the standalone shape-build bound, so the long-range horizon read still needs an art review. diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-eye.jpg new file mode 100644 index 0000000..0201742 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-wide.jpg new file mode 100644 index 0000000..7453fcd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/backrooms-grid-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-eye.jpg new file mode 100644 index 0000000..14cb03d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-wide.jpg new file mode 100644 index 0000000..479c33b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/pool-rooms-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-eye.jpg new file mode 100644 index 0000000..70f3963 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-wide.jpg new file mode 100644 index 0000000..ade83b1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/staircase-to-nowhere-wide.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-eye.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-eye.jpg new file mode 100644 index 0000000..2d81901 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-eye.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-wide.jpg b/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-wide.jpg new file mode 100644 index 0000000..34f2467 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux161/captures/terraced-hills-wide.jpg differ diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 561df41..c5aaf56 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -105,6 +105,7 @@ namespace std::filesystem::path WorldAssemblyManifestOutputPath; std::filesystem::path ResolvedLayoutOutputPath; std::filesystem::path CoreSnapshotInputPath; + std::filesystem::path LiminalStampWalkAuditPath; bool ResolvedLayoutOnly=false; bool AuditResolvedLayoutBoundaries=false; bool AuditResolvedLayoutPassages=false; @@ -1449,6 +1450,7 @@ namespace " --transition-mode=hard|gradient|interleaved --transition-width-chunks=N (fixture override)\n" " --audit-resolved-layout-passages (capsule, support, route-binding and blocked-control audit)\n" " --audit-liminal-road-walk (capsule, support, width and grade audit for the liminal road)\n" + " --audit-liminal-stamp-walk=FILE (capsule, support and doorway-width audit for a stamp fixture)\n" " --audit-heightfield-profile (brute-force the profiled field intervals and route)\n" " --audit-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\n" " --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n" @@ -1497,6 +1499,9 @@ namespace else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true; else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true; else if(arg=="--audit-liminal-road-walk")options.AuditLiminalRoadWalk=true; + else if(arg.starts_with("--audit-liminal-stamp-walk=")) + options.LiminalStampWalkAuditPath=std::filesystem::path(std::string( + arg.substr(std::string_view("--audit-liminal-stamp-walk=").size()))); else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true; else if(arg=="--density-dispatch-report") {options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;} @@ -2035,10 +2040,14 @@ int main(int argc,char** argv) ownerJsonRoot.Get("liminal_road_override").Data); const bool hasLiminalPillarGrid=!std::holds_alternative( ownerJsonRoot.Get("liminal_pillar_grid_override").Data); + const bool hasLiminalStampFixture=!std::holds_alternative( + ownerJsonRoot.Get("liminal_stamp_fixture_override").Data); if((hasMotifFamily&&hasLiminalRoad)||(hasMotifFamily&&hasLiminalPillarGrid) - ||(hasLiminalRoad&&hasLiminalPillarGrid)) + ||(hasLiminalRoad&&hasLiminalPillarGrid) + ||(hasLiminalStampFixture&&(hasMotifFamily||hasLiminalRoad||hasLiminalPillarGrid))) {std::fprintf(stderr,"liminal content overrides must be used in separate fixtures\n");return 2;} - if(hasCoreLiminalField&&(hasMotifFamily||hasLiminalRoad||hasLiminalPillarGrid)) + if(hasCoreLiminalField&&(hasMotifFamily||hasLiminalRoad||hasLiminalPillarGrid + ||hasLiminalStampFixture)) {std::fprintf(stderr,"Core.Liminal.Field owns its variant geometry and cannot be combined with prototype content overrides\n");return 2;} if(hasMotifFamily) { @@ -2138,6 +2147,27 @@ int main(int argc,char** argv) "placement=regular_grid route_keepout=PASS band_clearance=PASS status=PASS\n", resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size()); } + if(hasLiminalStampFixture) + { + std::vector requests; + if(!StandaloneWorldAssemblyPrototype::BuildLiminalStampFixtureRequests(ownerJsonRoot, + *ownerWorld,worldAssembly,resolvedLayout,requests,error) + ||!VoxelForgeCore::ResolveResolvedLayoutStampsV1( + resolvedLayout,seed,requests,error)) + {std::fprintf(stderr,"Liminal stamp fixture resolution failed: %s\n",error.c_str());return 2;} + const std::size_t boxFills=static_cast(std::count_if( + resolvedLayout.stamps.begin(),resolvedLayout.stamps.end(),[](const auto& stamp) + {return stamp.kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillBox;})); + const std::size_t boxCarves=static_cast(std::count_if( + resolvedLayout.stamps.begin(),resolvedLayout.stamps.end(),[](const auto& stamp) + {return stamp.kind==VoxelForgeCore::ResolvedSdfStampKindV1::carveBox;})); + const std::size_t stairs=static_cast(std::count_if( + resolvedLayout.stamps.begin(),resolvedLayout.stamps.end(),[](const auto& stamp) + {return stamp.kind==VoxelForgeCore::ResolvedSdfStampKindV1::fillSteps;})); + std::printf("[LiminalStampFixtureResolved] vocabulary=%u records=%zu box_fills=%zu box_carves=%zu steps=%zu placement=bounded_flat_field status=PASS\n", + resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size(), + boxFills,boxCarves,stairs); + } if(options.LandmarkChamberDemo) { std::vector requests; @@ -2639,5 +2669,6 @@ int main(int argc,char** argv) options.MovingCameraSpeedMetersPerSecond,options.MovingCameraDurationSeconds, options.MovingCameraDwellSeconds,options.ColdWorkerTimeline, options.AuditPassageSourceClamp,options.AuditPassageCarveReach, - options.AuditPassageTileSeams,options.AuditLiminalRoadWalk); + options.AuditPassageTileSeams,options.AuditLiminalRoadWalk, + options.LiminalStampWalkAuditPath); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 166b5d4..d861a64 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -2385,7 +2386,8 @@ namespace StandaloneStreamBenchmark double MovingCameraDurationSeconds=12.0,double MovingCameraDwellSeconds=5.0, bool bColdWorkerTimeline=false,bool bAuditPassageSourceClamp=false, bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false, - bool bAuditLiminalRoadWalk=false) + bool bAuditLiminalRoadWalk=false, + const std::filesystem::path& LiminalStampWalkAuditPath={}) { using Clock=std::chrono::steady_clock; const std::clock_t ColdCpuOrigin=std::clock(); @@ -2555,7 +2557,8 @@ namespace StandaloneStreamBenchmark ||bAuditOreVeinYield||bAuditLandmarkReachability||bAuditStrateTransitions ||bAuditResolvedLayoutPassages||bAuditResolvedLayoutBoundaries ||bAuditTileShapeReadiness||bAuditBasePageEquivalence||bAuditRouteParity - ||bAuditPassageSourceClamp||bAuditPassageCarveReach||bAuditLiminalRoadWalk; + ||bAuditPassageSourceClamp||bAuditPassageCarveReach||bAuditLiminalRoadWalk + ||!LiminalStampWalkAuditPath.empty(); const bool bDeferFarWorld=bIncludeFarWorld&&!bMovingCameraTrace&&!bFarWorldAudit; VoxelForgeCore::ShapeBuildRequest ShapeRequest; ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z}; @@ -2821,7 +2824,8 @@ namespace StandaloneStreamBenchmark Streamer.Stop(); return passed?0:11; } - if(bAuditResolvedLayoutPassages||bAuditLiminalRoadWalk) + if(bAuditResolvedLayoutPassages||bAuditLiminalRoadWalk + ||!LiminalStampWalkAuditPath.empty()) { if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout()) {Streamer.Stop();std::fprintf(stderr,"[ResolvedWalkAudit] missing resolved-layout snapshot\n");return 2;} @@ -3230,6 +3234,82 @@ namespace StandaloneStreamBenchmark } return passed; }; + if(!LiminalStampWalkAuditPath.empty()) + { + struct FDoorwayWidthGate + { + std::string prefix; + std::uint32_t widthAxis=0; + double minimumWidthMetres=0.0; + }; + std::ifstream auditFile(LiminalStampWalkAuditPath); + std::vector> walkPath; + std::vector doorwayGates; + bool inputValid=static_cast(auditFile); + std::string line; + while(inputValid&&std::getline(auditFile,line)) + { + if(line.empty()||line[0]=='#')continue; + std::istringstream fields(line); + char record=0;fields>>record; + if(record=='P') + { + std::array point{}; + fields>>point[0]>>point[1]>>point[2]; + inputValid=inputValid&&static_cast(fields) + &&std::isfinite(point[0])&&std::isfinite(point[1]) + &&std::isfinite(point[2]); + if(inputValid) + walkPath.push_back({point[0]/coreMetres, + point[1]/coreMetres,point[2]/coreMetres}); + } + else if(record=='D') + { + FDoorwayWidthGate gate; + fields>>gate.prefix>>gate.widthAxis>>gate.minimumWidthMetres; + inputValid=inputValid&&static_cast(fields) + &&!gate.prefix.empty()&&gate.widthAxis<2 + &&std::isfinite(gate.minimumWidthMetres) + &&gate.minimumWidthMetres>=1.5; + if(inputValid)doorwayGates.push_back(std::move(gate)); + } + else inputValid=false; + } + std::uint64_t doorwayCount=0; + double narrowestDoorway=std::numeric_limits::infinity(); + bool doorwayWidthsPass=inputValid; + for(const auto& gate:doorwayGates) + { + std::uint64_t found=0; + for(const auto& stamp:ResolvedLayout->stamps) + { + if(!stamp.stableId.starts_with(gate.prefix))continue; + ++found;++doorwayCount; + const double width=2.0*stamp.halfExtentsMicrometres[gate.widthAxis]/1.0e6; + narrowestDoorway=std::min(narrowestDoorway,width); + if(width+1.0e-90; + } + if(doorwayCount==0)narrowestDoorway=0.0; + const FWalkAudit walkAudit=inputValid&&walkPath.size()>=2 + ?auditPlayerPath(Shape,walkPath,0.125,0.125,1.125,3,0.45):FWalkAudit{}; + const bool passed=inputValid&&doorwayWidthsPass&&walkAudit.passed; + std::printf("[LiminalStampWalkAudit] path_points=%zu stations=%llu capsule_samples=%llu " + "blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu " + "doorways=%llu narrowest_doorway_m=%.3f minimum_doorway_m=1.500 " + "status=%s\n",walkPath.size(), + static_cast(walkAudit.centers), + static_cast(walkAudit.capsuleSamples), + static_cast(walkAudit.blocked), + static_cast(walkAudit.supportSamples), + static_cast(walkAudit.unsupported), + static_cast(walkAudit.gradeFailures), + static_cast(doorwayCount),narrowestDoorway, + passed?"PASS":"FAIL"); + Streamer.Stop(); + return passed?0:14; + } if(bAuditLiminalRoadWalk&&!bAuditResolvedLayoutPassages) { const bool passed=auditLiminalRoadWalk(true); diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h index 5c5e262..aa6dc89 100644 --- a/Standalone/Source/WorldAssemblyPrototype.h +++ b/Standalone/Source/WorldAssemblyPrototype.h @@ -822,6 +822,109 @@ namespace StandaloneWorldAssemblyPrototype return true; } + inline bool BuildLiminalStampFixtureRequests(const VFJson::Value& root, + const FStandaloneOwnerWorld& owner, + const VoxelForgeCore::WorldAssemblyManifest& assembly, + const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout, + std::vector& requests, + std::string& error) + { + using namespace VoxelForgeCore; + requests.clear(); + const VFJson::Value& fixture=root.Get("liminal_stamp_fixture_override"); + if(!fixture.IsObject()||!fixture.Get("version").IsNumber() + ||fixture.Get("version").Integer()!=1||!fixture.Get("stamps").IsArray()) + {error="liminal_stamp_fixture_override must be a version-1 object with a stamps array";return false;} + if(assembly.recipe.surfaceProfile.version!=1 + ||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField + ||owner.CoreVoxelSizeMicrons==0) + {error="liminal stamp fixtures require a valid flatField SurfaceWorld recipe";return false;} + const VFJson::Value& strateValue=fixture.Get("strate_index"); + if(!strateValue.IsNumber()) + {error="liminal stamp fixture must select a resolved SurfaceWorld strate";return false;} + const auto strateIndex=static_cast(strateValue.Integer()); + const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(), + [strateIndex](const auto& value){return value.strateIndex==strateIndex + &&value.generatorKind=="SurfaceWorld";}); + if(strate==layout.strates.end()) + {error="liminal stamp fixture strate is not a resolved SurfaceWorld strate";return false;} + const auto toMicrometres=[](double value) + {return static_cast(std::llround(value*1000000.0));}; + const std::int64_t floorMicrometres=static_cast(std::llround( + static_cast(assembly.recipe.surfaceProfile.h0Subvoxels) + *owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel)); + const auto readOptionalMetres=[&](const VFJson::Value& object,const char* key, + double fallback,double& result) + { + const VFJson::Value& value=object.Get(key); + if(std::holds_alternative(value.Data)) + {result=fallback;return true;} + if(!value.IsNumber()||!std::isfinite(value.Number()))return false; + result=value.Number();return true; + }; + for(const auto& entry:fixture.Get("stamps").AsArray()) + { + if(!entry.IsObject()) + {error="liminal stamp fixture contains a malformed stamp record";return false;} + const auto* id=std::get_if(&entry.Get("stable_id").Data); + const auto* kind=std::get_if(&entry.Get("kind").Data); + const VFJson::Value& anchor=entry.Get("anchor_xy_m"); + const VFJson::Value& extents=entry.Get("half_extents_m"); + double zOffset=0.0,rounding=0.0,smooth=0.0; + if(!id||id->empty()||!kind||anchor.AsArray().size()!=2 + ||extents.AsArray().size()!=3 + ||!readOptionalMetres(entry,"z_offset_m",0.0,zOffset) + ||!readOptionalMetres(entry,"rounding_radius_m",0.0,rounding) + ||!readOptionalMetres(entry,"smooth_width_m",0.0,smooth)) + {error="liminal stamp fixture record requires an identity, kind, anchor, extents and finite metre values";return false;} + for(const auto& value:anchor.AsArray()) + if(!value.IsNumber()||!std::isfinite(value.Number())) + {error="liminal stamp anchor_xy_m must contain finite metre values";return false;} + for(const auto& value:extents.AsArray()) + if(!value.IsNumber()||!std::isfinite(value.Number())||value.Number()<=0.0) + {error="liminal stamp half_extents_m must contain positive finite metre values";return false;} + if(std::abs(anchor.AsArray()[0].Number())>5000.0 + ||std::abs(anchor.AsArray()[1].Number())>5000.0 + ||std::abs(zOffset)>1000.0||rounding<0.0||smooth<0.0) + {error="liminal stamp fixture values exceed their bounded ranges";return false;} + ResolvedLayoutStampRequestV1 request; + request.stableId=*id;request.strateIndex=strateIndex; + request.anchorPositionMicrometres={ + toMicrometres(anchor.AsArray()[0].Number()), + toMicrometres(anchor.AsArray()[1].Number()), + floorMicrometres+toMicrometres(zOffset)}; + request.halfExtentsMicrometres={toMicrometres(extents.AsArray()[0].Number()), + toMicrometres(extents.AsArray()[1].Number()), + toMicrometres(extents.AsArray()[2].Number())}; + request.roundingRadiusMicrometres=toMicrometres(rounding); + request.smoothWidthMicrometres=toMicrometres(smooth); + const VFJson::Value& yaw=entry.Get("yaw_quarter_turns"); + if(!std::holds_alternative(yaw.Data)) + { + if(!yaw.IsNumber()||yaw.Integer()<0||yaw.Integer()>3) + {error="liminal stamp yaw_quarter_turns must be an integer from zero through three";return false;} + request.yawQuarterTurns=static_cast(yaw.Integer()); + } + if(*kind=="fillBox")request.kind=ResolvedSdfStampKindV1::fillBox; + else if(*kind=="carveBox")request.kind=ResolvedSdfStampKindV1::carveBox; + else if(*kind=="carveArch")request.kind=ResolvedSdfStampKindV1::carveArch; + else if(*kind=="fillSteps") + { + request.kind=ResolvedSdfStampKindV1::fillSteps; + const VFJson::Value& count=entry.Get("step_count"); + if(!count.IsNumber()||count.Integer()<2||count.Integer()>128) + {error="fillSteps requires step_count from two through 128";return false;} + request.stepCount=static_cast(count.Integer()); + } + else + {error="liminal stamp kind must be fillBox, carveBox, carveArch or fillSteps";return false;} + requests.push_back(std::move(request)); + } + if(requests.empty()) + {error="liminal stamp fixture must resolve at least one stamp";return false;} + return true; + } + inline bool BuildCoreLiminalFieldRequests( const VoxelForgeCore::WorldAssemblyManifest& assembly, const FStandaloneOwnerWorld& owner,