diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 589e64a..2d3961a 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -476,7 +476,7 @@ namespace VoxelForgeCore // Version 210 keeps bulk native-cave sampling on the scalar field path at strata edges. // Version 211 makes coordinate biasing in the shared cell hash unsigned, removing signed-overflow UB. // Version 212 balances generated entry routes against existing walk lanes and preserves their shared treads. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 215; + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 216; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index d70c6fb..7f07f13 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -279,6 +279,7 @@ public: double signedDistance=std::numeric_limits::infinity(); double t=0.0; std::uint32_t segmentIndex=std::numeric_limits::max(); + std::uint8_t scanPhase=std::numeric_limits::max(); float wallRelief=0.0f,mouthSurfaceRelief=0.0f,mouthTreadFieldRelief=0.0f; bool valid=false,hasWallRelief=false,hasMouthSurfaceRelief=false, hasMouthTreadFieldRelief=false; @@ -634,6 +635,28 @@ public: const auto& candidates=columnFeature?passageColumns_:passageRoutes_; return passageIndex=passageRouteSegments_.size())return false; + for(const FCandidateSegment& candidate:passageRouteSegments_[passageIndex]) + { + if(qxcandidate.maximumSubvoxels[0] + ||qycandidate.maximumSubvoxels[1] + ||qzcandidate.maximumSubvoxels[2])continue; + return true; + } + return false; + } const std::vector& mouthSupportSegments(std::size_t passageIndex) const { static const std::vector Empty; @@ -1910,14 +1933,28 @@ public: if(l90CandidateProfiled)recordColumnProof(columnDistance,composed); composed=std::min(composed,static_cast(columnDistance/Q)); } - const bool usePreparedPassageSweep=passageTileScopeMatches - &&passageCarveReachEnabled&&passageTileQueries->hasSweptTubeBase(); - const auto* preparedSweepSample=usePreparedPassageSweep + const bool preparedPassageSweepRequested=passageTileScopeMatches + &&passageCarveReachEnabled + &&passageTileQueries->hasSweptTubeBase(); + const auto* preparedSweepSample=preparedPassageSweepRequested ?passageTileQueries->sweptSample(passageIndex,qx,qy,qz):nullptr; - // The tile sweep has already visited every segment whose bounded route or - // support envelope reaches this lattice point. No route query can affect it. - if(usePreparedPassageSweep&&(!preparedSweepSample - ||preparedSweepSample->signedDistance>passageCarveReachQ))continue; + // The sweep is indexed at the tile's exact lattice points. Mesher and seam + // queries can also arrive at interpolated off-grid positions; a cache miss + // at those points says nothing about route influence, so use the exact direct + // route search instead of dropping the carve. + const bool usePreparedPassageSweep=preparedSweepSample!=nullptr; + // Candidate route bounds include the carve reach, tread/clearance extents, + // and conservative padding. A missing lattice sample outside every route + // bound cannot affect this query; keep direct evaluation for all in-bound + // misses and for the separate column and mouth-support paths. + if(preparedPassageSweepRequested&&!preparedSweepSample + &&!passageTileQueries->hasFeature(passageIndex,true) + &&!passageTileQueries->hasMouthSupport(passageIndex) + &&!passageTileQueries->hasUpperMouthSupport(passageIndex) + &&!passageTileQueries->routeFeatureMayAffect(passageIndex,qx,qy,qz)) + continue; + if(usePreparedPassageSweep + &&preparedSweepSample->signedDistance>passageCarveReachQ)continue; const bool tilePassageFree=passageTileScopeMatches &&!ActivePassageTileQueries->hasFeature(passageIndex,false); if(composed>=0.0f&&tilePassageFree) @@ -2111,7 +2148,7 @@ public: bestSignedDistance=preparedSweepSample->signedDistance; bestT=preparedSweepSample->t; bestIndex=static_cast(preparedSweepSample->segmentIndex); - bestScanPhase=0; + bestScanPhase=preparedSweepSample->scanPhase; bestScanIndex=bestIndex; } else if(passageTileScopeMatches) @@ -14092,6 +14129,46 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa auto& passageSamples=sweep->passages.emplace_back(); passageSamples.passageIndex=passageIndex; passageSamples.samples.resize(gridSampleCount); + const auto& constants=shape_->PassageComposerConstants[passageIndex]; + struct FCarveWindow + { + std::ptrdiff_t begin=1,end=0; + }; + std::vector carveWindows(static_cast(sweep->dimension)); + const auto& floorProfileQ=constants.controlFloorProfileQ; + const std::ptrdiff_t lowerMouthIndex=constants.lowerMouthIndex; + if(lowerMouthIndex>=0 + &&static_cast(lowerMouthIndex)value;}; + const auto profileBegin=floorProfileQ.begin(); + const auto profileEnd=profileBegin+lowerMouthIndex+1; + for(std::int32_t gz=0;gzdimension;++gz) + { + const double qz=static_cast(sweep->gridMinimumSubvoxels[2] + +static_cast(gz)*sweep->stepSubvoxels); + const auto carveHigh=std::lower_bound(profileBegin,profileEnd, + qz+constants.carveSearchMargin,compareFloorProfile); + const auto carveLow=std::lower_bound(profileBegin,profileEnd, + qz-constants.carveSearchMargin,compareFloorProfile); + auto& window=carveWindows[static_cast(gz)]; + window.begin=std::max(0, + (carveHigh-profileBegin)-1); + window.end=std::min(lowerMouthIndex-1, + (carveLow-profileBegin)+1); + } + } + const auto scanPhaseForSegment=[&](std::ptrdiff_t segmentIndex, + const FCarveWindow& window) + { + if(segmentIndex>=window.begin&&segmentIndex<=window.end)return 0; + if(segmentIndex(8,lowerMouthIndex))return 1; + if(segmentIndex>=constants.mouthSearchBegin + &&segmentIndex=lowerMouthIndex&&segmentIndex=shape_->PassageSegmentData[passageIndex].size())continue; @@ -14101,7 +14178,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa candidate.sweepMaximumSubvoxels[axis],first[axis],last[axis]); if(first[0]>last[0]||first[1]>last[1]||first[2]>last[2])continue; - const auto& constants=shape_->PassageComposerConstants[passageIndex]; const auto& segment=shape_->PassageSegmentData[passageIndex][candidate.segmentIndex]; if(segment.lengthSq<=0.0)continue; const std::ptrdiff_t segmentIndex=static_cast(candidate.segmentIndex); @@ -14112,9 +14188,13 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa &&!shape_->IsPassageShortSpanSegment(passageIndex,candidate.segmentIndex) &&!shape_->IsPassageShortSpanSegment(passageIndex,candidate.segmentIndex+1); for(std::int32_t gz=first[2];gz<=last[2];++gz) - for(std::int32_t gy=first[1];gy<=last[1];++gy) - for(std::int32_t gx=first[0];gx<=last[0];++gx) { + const int scanPhase=scanPhaseForSegment(segmentIndex, + carveWindows[static_cast(gz)]); + if(scanPhase<0)continue; + for(std::int32_t gy=first[1];gy<=last[1];++gy) + for(std::int32_t gx=first[0];gx<=last[0];++gx) + { const double qx=static_cast(sweep->gridMinimumSubvoxels[0] +static_cast(gx)*sweep->stepSubvoxels); const double qy=static_cast(sweep->gridMinimumSubvoxels[1] @@ -14162,17 +14242,21 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa auto& sample=passageSamples.samples[sampleIndex]; if(!sample.valid||signedDistance(candidate.segmentIndex); + sample.scanPhase=static_cast(scanPhase); sample.valid=true; sample.hasWallRelief=false; sample.hasMouthSurfaceRelief=false; sample.hasMouthTreadFieldRelief=false; if(profile)++profileStats_->passageSweepMinimumUpdates; } + } } } } diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux100/REPORT.md new file mode 100644 index 0000000..f3896ac --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/REPORT.md @@ -0,0 +1,64 @@ +# L100 — swept-tube boundary regression + +Branch: `wip/l99-swept-tube` (`fa6363b`, unchanged HEAD). L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`. + +## Cause and fix + +**Measured:** the 1–2 midpoint query at `(-8.5, 12.5, -741.989990 m)` has band density `−0.499936938`; stamps and caverns are absent, and the cap leaves that value unchanged. The direct route query finds segment 8 at SDF `−1.83728031 m`, with reach, floor, and combined weights all 1, and returns `+7.35348845`. The bounded sweep leaves `−0.499936938` at this point. At the nearby exact `z=−742 m` lattice point both paths carve; at `z=−741.989990 m` the sweep has no exact cache entry. The strate field itself is `−1032.04004` and is masked by the boundary band. This identifies the band as the flat sheet; the 0.5 m blend, stamps, and caps are not the cause. + +**Code-path conclusion:** L99 treated a missing exact-grid sweep sample as proof that no route affected the query and skipped route evaluation. Off-grid seam/field queries therefore exposed the band inside the tube. L100 evaluates the exact route on an in-bound cache miss. A conservative route-feature bound still skips misses outside candidate extents; mouth and column supports keep their separate paths. Composition stays `graph → bands → passage → stamps → floor caps`, so the tunnel cuts bands while authored stamps and landing caps retain precedence. This adds the missing ownership without restoring the old full route scan. + +## Comparison metric + +The comparison now counts added and removed edge crossings per tile, alongside moved shared crossings, and reports added/removed non-skirt triangles. The old mean/max displacement summarized only crossings shared by both meshes; a missing or new crossing had no displacement to average. `changed_mesh` could flag the tile but did not say whether surface appeared or disappeared. + +Final direct-route versus bounded-sweep audit (LOD 0–2 near sets): + +| Strate | Tiles | Added / removed crossings | Added / removed triangles | Moved crossings | Mean / max displacement (cm) | Pairing failures | Audit | +|---|---:|---:|---:|---:|---:|---:|---| +| SW | 1,408/1,408 | 0 / 0 | 0 / 0 | 9,214 | 0.0180 / 20.1114 | 5 | exit 14 | +| FI | 1,216/1,216 | 0 / 0 | 56 / 0 | 10,238 | 0.0185 / 19.7997 | 37 | exit 14 | +| TN | 1,344/1,344 | 0 / 0 | 0 / 0 | 11,370 | 0.0387 / 21.7442 | 41 | exit 14 | +| VS | 1,344/1,344 | 0 / 0 | 0 / 0 | 5,087 | 0.0178 / 25.8798 | 4 | exit 14 | + +All four comparison commands hit the legacy strict displacement/normal acceptance gate (1 cm maximum displacement, 2° maximum normal change, and no normal-pairing failures); they completed and found no added/removed crossing edges. Exit 14 is that diagnostic result, not a build or timeout failure. FI has 56 net added triangles. + +## Readiness and timing + +Fast tier: **PASS**, 274 blocking checks across 80 runs, including all resolved-passage and adjacent-interface walkability audits. Evaluator version 216 hashes are re-pinned. Determinism: 24/24 Clang/GCC runs at W1/W8/W12 and 20/20 cross-worker/compiler comparisons match exactly for all four strates. + +L80 timings are medians of three measured repetitions in rotating L98/L99/L100 order, after a warmup for each version, stratum, and worker count. Values are seconds (`L98 / L99 / L100`). All 72 measured invocations completed; each L100 run reported source stamp `7224bf6e…`. + +| Workers | Strate | Near ready, LOD 0–2 | Whole view | +|---:|---|---:|---:| +| 8 | SW | 1.364 / 0.940 / 1.425 | 3.035 / 2.413 / 3.429 | +| 8 | FI | 2.792 / 1.719 / 2.157 | 4.385 / 2.931 / 3.409 | +| 8 | TN | 2.746 / 1.723 / 2.175 | 4.022 / 2.790 / 3.294 | +| 8 | VS | 1.631 / 1.352 / 1.522 | 2.779 / 2.553 / 2.745 | +| 12 | SW | 1.174 / 0.949 / 1.039 | 2.509 / 2.148 / 2.280 | +| 12 | FI | 2.617 / 1.783 / 2.054 | 3.919 / 2.902 / 3.197 | +| 12 | TN | 2.358 / 1.547 / 1.802 | 3.458 / 2.539 / 2.813 | +| 12 | VS | 1.158 / 1.032 / 1.083 | 2.181 / 2.027 / 2.097 | + +On FI/TN, L100 remains 18–24% faster than L98 for near-ready and whole-view medians. It is 10–26% slower than L99, reflecting the exact fallback needed for off-grid misses. + +## Captures + +[`captures/INDEX.md`](captures/INDEX.md) lists all 15 paired views: both mouths and 25/50/75 route stations for each passage. All 30 PNGs pass the programmatic geometry gate (over 3,000 non-dominant-background pixels and at least two RGB colors); the index reports each pair’s changed-pixel count. It also records the 1–2 same-camera three-version check: visible geometry is 1,407,415 px in L98, 1,206,362 in L99, and 1,407,415 in L100; changed pixels are 1,135,959 L98→L99 and 1,134,150 L99→L100. L98→L100 changes 32,217 px. The 0–1 mid-25 and 2–3 mid-25/mid-50 cameras use documented nearest-surface targets where the forward route view had no visible L98 surface. + +## Failed runs + +- The four density-comparison audits exit 14 on their legacy strict geometry gate; the new added/removed crossing counts are zero in all four. +- An intermediate composition-order experiment put passage carving after floor caps and failed the landmark walk audit. Restoring the L99 stamp/cap precedence made the final fast tier green. +- The first Unreal build attempt exited before compilation because UBT could not rotate its log under read-only `/home/jahni/.config`. The retry redirected UBT config and logging under `Saved/Linux/l100` and succeeded (27/27 actions; three RealtimeMesh deprecation warnings). + +No benchmark or capture process timed out. + +## Evidence files + +- Source point trace: `Saved/Linux/l100/logs/column-sweep.log`, `column-direct.log`. +- Density comparison: `Saved/Linux/l100/density-compare/summary.json`. +- Hash matrix: `Saved/Linux/l100/hashes/summary.json`. +- Fast tier: `Saved/Linux/l100/fast-summary.json`. +- L80 triple: `Saved/Linux/l100/readiness/l98-l99-l100/summary.json`. +- Build, Clang, GCC, captures, and per-run logs: `Saved/Linux/l100/logs/` and `Saved/Linux/l100/`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Captures.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Captures.py new file mode 100644 index 0000000..4f2e831 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Captures.py @@ -0,0 +1,602 @@ +#!/usr/bin/env python3 +"""Capture matched L98/L100 passage mouth and mid-route views.""" +from __future__ import annotations + +import json +import math +import struct +import subprocess +import time +import zlib +from array import array +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l100/captures" +IMAGE_DIR = Path(__file__).resolve().parent / "captures" +DATA = ROOT / "Standalone/data/owner_world.json" +LAYOUT_PATH = ROOT / "Saved/Linux/l99/resolved-sw.json" +BINARIES = { + "before-l98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19", + "after-l100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang", +} +RENDERER = ROOT / "Saved/Linux/l100/bin/VoxelForgeOffscreenCapture-l100-clang" +VOXEL_Q = 256.0 +CORE_UM_PER_VOXEL = 250_000.0 +ROUTE_POINTS = {} +ROUTE_LENGTHS = {} + + +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, s): + return tuple(x * s for x in a) + + +def dot(a, b): + return sum(x * y for x, y in zip(a, b)) + + +def norm(a): + length = math.sqrt(dot(a, a)) + if length < 1.0e-8: + raise ValueError("degenerate capture direction") + return tuple(x / length for x in a) + + +def fmt(values): + return ",".join(f"{value:.9f}" for value in values) + + +def angles(camera, target): + delta = sub(target, camera) + return (math.degrees(math.atan2(delta[0], delta[1])), + math.degrees(math.atan2(delta[2], math.hypot(delta[0], delta[1])))) + + +def interpolate(points, distance): + remaining = max(0.0, distance) + for a, b in zip(points, points[1:]): + delta = sub(b[0], a[0]) + length = math.sqrt(dot(delta, delta)) + if length < 1.0e-8: + continue + if remaining <= length: + t = remaining / length + return (tuple(a[0][i] + delta[i] * t for i in range(3)), + a[1] + (b[1] - a[1]) * t) + remaining -= length + return points[-1] + + +def route_frame(points, distance): + center, radius = interpolate(points, distance) + before, _ = interpolate(points, max(0.0, distance - 0.25)) + after, _ = interpolate(points, min(sum( + math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0]))) + for a, b in zip(points, points[1:])), 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 route_length(points): + return sum(math.sqrt(dot(sub(b[0], a[0]), sub(b[0], a[0]))) + for a, b in zip(points, points[1:])) + + +def nearest_route(points, point): + best = None + distance_along = 0.0 + for a, b in zip(points, points[1:]): + delta = sub(b[0], a[0]) + length_sq = dot(delta, delta) + if length_sq < 1.0e-10: + continue + segment_length = math.sqrt(length_sq) + t = max(0.0, min(1.0, dot(sub(point, a[0]), delta) / length_sq)) + center = add(a[0], mul(delta, t)) + tangent = norm(delta) + radial = sub(point, center) + radial = sub(radial, mul(tangent, dot(radial, tangent))) + distance = math.sqrt(dot(sub(point, center), sub(point, center))) + if best is None or distance < best[0]: + radius = a[1] + (b[1] - a[1]) * t + best = (distance, center, tangent, radial, radius, + distance_along + t * segment_length) + distance_along += segment_length + if best is None: + raise ValueError("passage route has no non-degenerate segment") + return best + + +def run(name, command, headless): + log = OUT / f"{name}.log" + wrapped = ["taskset", "-c", "0-11"] + if headless: + wrapped += ["prlimit", "--as=6442450944", "--"] + wrapped += ["timeout", "1740s", *map(str, command)] + started = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(wrapped, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1745) + row = {"name": name, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L100CaptureRun] " + json.dumps(row, sort_keys=True), flush=True) + if result.returncode != 0: + raise RuntimeError(f"capture step failed: {name}; see {log}") + return row + + +def nearest_surface_anchor(mesh, point, tile_search_margin_m=20.0): + """Return the nearest surface vertex and outward mesh normal from a VFMSH scene.""" + best_distance_sq = float("inf") + best_position = best_normal = None + with mesh.open("rb") as stream: + magic = stream.read(8) + has_materials = magic == b"VFMSH002" + if magic not in (b"VFMSH001", b"VFMSH002"): + raise RuntimeError(f"unsupported mesh scene header: {mesh}") + version, voxel_um, tile_count = struct.unpack("<3I", stream.read(12)) + if version != (2 if has_materials else 1): + raise RuntimeError(f"mesh scene version mismatch: {mesh}") + stream.seek(40, 1) # stored camera position, yaw, and pitch + for _ in range(tile_count): + key = struct.unpack("<4i", stream.read(16)) + surface_count, _, vertex_count, index_count, mask_count = struct.unpack( + "<5I", stream.read(20)) + extent = (32 << key[3]) * voxel_um / 1_000_000.0 + origin = (key[0] * extent, key[1] * extent, key[2] * extent) + if any(point[axis] < origin[axis] - tile_search_margin_m or + point[axis] > origin[axis] + extent + tile_search_margin_m + for axis in range(3)): + stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0) + + index_count * 4 + mask_count, 1) + continue + positions = array("f") + positions.frombytes(stream.read(vertex_count * 12)) + normals = array("f") + normals.frombytes(stream.read(vertex_count * 12)) + if has_materials: + stream.seek(vertex_count * 10, 1) + stream.seek(index_count * 4 + mask_count, 1) + if struct.pack("=I", 1) != struct.pack("= best_distance_sq: + continue + raw_normal = tuple(float(normals[offset + axis]) for axis in range(3)) + if dot(raw_normal, raw_normal) < 1.0e-10: + continue + best_distance_sq = distance_sq + best_position = position + best_normal = norm(raw_normal) + if best_position is None: + raise RuntimeError(f"no nearby surface vertices in mesh scene: {mesh}") + return {"position_m": best_position, "normal": best_normal, + "distance_m": math.sqrt(best_distance_sq)} + + +def make_passages(layout): + q_to_m = layout["core_voxel_size_um"] / (VOXEL_Q * 1_000_000.0) + passages = [] + all_routes = [] + for passage in layout["passages"]: + controls = [(tuple(float(q) * q_to_m for q in p["position_q"]), + float(p["radius_q"]) * q_to_m) + for p in passage["control_points"]] + upper = tuple(float(q) * q_to_m for q in passage["upper_mouth_q"]) + lower = tuple(float(q) * q_to_m for q in passage["lower_mouth_q"]) + ui = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], upper), + sub(controls[i][0], upper))) + li = min(range(len(controls)), key=lambda i: dot(sub(controls[i][0], lower), + sub(controls[i][0], lower))) + points = controls[ui:li + 1] + if len(points) < 2: + raise RuntimeError(f"invalid mouth path: {passage['stable_id']}") + upper_offset = math.sqrt(dot(sub(points[0][0], upper), sub(points[0][0], upper))) + lower_offset = math.sqrt(dot(sub(points[-1][0], lower), sub(points[-1][0], lower))) + length = route_length(points) + passage_views = [] + for endpoint, distance, camera_distance, outward_sign in ( + ("upper-mouth", 0.0, min(length, 7.4), -1.0), + ("lower-mouth", length, max(0.0, length - 5.5), 1.0)): + center, _, tangent, side = route_frame(points, distance) + camera_center, _, _, camera_side = route_frame(points, camera_distance) + camera = (camera_center[0] + camera_side[0] * 2.5, + camera_center[1] + camera_side[1] * 2.5, camera_center[2]) + outward = mul(tangent, outward_sign) + target = (center[0] + outward[0] * 8.0 + side[0] * 2.5, + center[1] + outward[1] * 8.0 + side[1] * 2.5, center[2]) + yaw, pitch = angles(camera, target) + passage_views.append({"name": f"{passage['stable_id']}-{endpoint}", + "kind": "mouth", "focus": center, "camera": camera, "target": target, + "yaw": yaw, "pitch": pitch, "clip": None, "fov": 80.0, + "focus_radius": 20.0, "passage": passage["stable_id"], + "camera_location": "passage-air"}) + for fraction in (0.25, 0.50, 0.75): + distance = length * fraction + center, radius, _, side = route_frame(points, distance) + camera = (center[0] + side[0] * 0.75, + center[1] + side[1] * 0.75, + center[2] - radius + 1.6) + target_pos, target_radius = interpolate( + points, min(length, distance + 14.0)) + target = (target_pos[0] + side[0] * 2.35, + target_pos[1] + side[1] * 2.35, + target_pos[2] - target_radius + 1.8) + yaw, pitch = angles(camera, target) + pct = int(round(fraction * 100)) + passage_views.append({"name": f"{passage['stable_id']}-mid-{pct:02d}", + "kind": "mid-tunnel", "route_fraction": fraction, + "focus": center, "camera": camera, "target": target, + "yaw": yaw, "pitch": pitch, "clip": None, + "fov": 78.0, "focus_radius": 24.0, + "passage": passage["stable_id"]}) + passages.append({"stable_id": passage["stable_id"], "route_length_m": length, + "control_points": len(points), "upper_mouth_anchor_offset_m": upper_offset, + "lower_mouth_anchor_offset_m": lower_offset, "views": passage_views}) + all_routes.append((passage["stable_id"], points)) + return passages, all_routes + + +def make_hotspot_views(routes, reach_summary): + views = [] + for series, rows, value_key, location_key, region_key in ( + ("displacement", reach_summary["top_displacement_tiles"], + "max_displacement_cm", "displacement_location_metres", "displacement_region"), + ("normal", reach_summary["top_normal_tiles"], + "max_normal_degrees", "normal_location_metres", "normal_region"), + ): + for index, row in enumerate(rows, 1): + point = tuple(row[location_key]) + candidates = [(nearest_route(route, point), name, route) + for name, route in routes] + (distance, center, tangent, radial, radius, along), nearest_id, route = min( + candidates, key=lambda item: item[0][0]) + if math.sqrt(dot(radial, radial)) < 0.25: + horizontal = norm((tangent[0], tangent[1], 0.0)) + radial = (-horizontal[1], horizontal[0], 0.0) + outward = norm(radial) + camera = add(point, mul(outward, 20.0)) + target = point + plane = None + yaw, pitch = angles(camera, target) + views.append({"name": f"top-{series}-{index:02d}", "kind": f"top-{series}", + "focus": point, "camera": camera, "target": target, + "yaw": yaw, "pitch": pitch, "clip": plane, "fov": 68.0, + "focus_radius": 18.0, "strate": row["strate"], + "value": row[value_key], "region": row[region_key], + "camera_anchor": "nearest-surface-l98-v5", "nearest_passage": nearest_id, + "route_distance_m": distance}) + return views + + +def decode_png(path): + """Decode 8-bit RGB/RGBA PNG pixels using only the Python standard library.""" + data = path.read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n": + raise RuntimeError(f"not a PNG: {path}") + width = height = bit_depth = color_type = interlace = None + compressed = bytearray() + offset = 8 + while offset < len(data): + length = struct.unpack_from(">I", data, offset)[0] + kind = data[offset + 4:offset + 8] + chunk = data[offset + 8:offset + 8 + length] + if kind == b"IHDR": + width, height, bit_depth, color_type, compression, filtering, interlace = \ + struct.unpack(">IIBBBBB", chunk) + if compression or filtering or interlace: + raise RuntimeError(f"unsupported PNG coding in {path}") + elif kind == b"IDAT": + compressed.extend(chunk) + elif kind == b"IEND": + break + offset += 12 + length + if bit_depth != 8 or color_type not in (2, 6): + raise RuntimeError(f"expected 8-bit RGB/RGBA PNG: {path}") + channels = 3 if color_type == 2 else 4 + stride = width * channels + encoded = zlib.decompress(compressed) + pixels = bytearray(height * stride) + source_offset = 0 + for y in range(height): + filter_type = encoded[source_offset] + source_offset += 1 + row = bytearray(encoded[source_offset:source_offset + stride]) + source_offset += stride + prior_offset = (y - 1) * stride + for index in range(stride): + left = row[index - channels] if index >= channels else 0 + above = pixels[prior_offset + index] if y else 0 + upper_left = pixels[prior_offset + index - channels] \ + if y and index >= channels else 0 + if filter_type == 1: + row[index] = (row[index] + left) & 255 + elif filter_type == 2: + row[index] = (row[index] + above) & 255 + elif filter_type == 3: + row[index] = (row[index] + ((left + above) >> 1)) & 255 + elif filter_type == 4: + estimate = left + above - upper_left + left_distance = abs(estimate - left) + above_distance = abs(estimate - above) + upper_left_distance = abs(estimate - upper_left) + if left_distance <= above_distance and left_distance <= upper_left_distance: + predictor = left + elif above_distance <= upper_left_distance: + predictor = above + else: + predictor = upper_left + row[index] = (row[index] + predictor) & 255 + elif filter_type != 0: + raise RuntimeError(f"unsupported PNG filter {filter_type}: {path}") + start = y * stride + pixels[start:start + stride] = row + if channels == 3: + return width, height, pixels + rgb = bytearray(width * height * 3) + rgb[0::3] = pixels[0::4] + rgb[1::3] = pixels[1::4] + rgb[2::3] = pixels[2::4] + return width, height, rgb + + +def image_geometry_metrics(path): + width, height, rgb = decode_png(path) + color_counts = {} + for offset in range(0, len(rgb), 3): + color = (rgb[offset], rgb[offset + 1], rgb[offset + 2]) + color_counts[color] = color_counts.get(color, 0) + 1 + total = width * height + visible = total - max(color_counts.values()) + return {"width": width, "height": height, "unique_rgb": len(color_counts), + "visible_geometry_pixels": visible, + "visible_geometry_fraction": visible / total} + + +def changed_pixel_count(before, after): + before_width, before_height, before_rgb = decode_png(before) + after_width, after_height, after_rgb = decode_png(after) + if (before_width, before_height) != (after_width, after_height): + raise RuntimeError(f"capture dimensions differ: {before} / {after}") + return sum(before_rgb[offset] != after_rgb[offset] + or before_rgb[offset + 1] != after_rgb[offset + 1] + or before_rgb[offset + 2] != after_rgb[offset + 2] + for offset in range(0, len(before_rgb), 3)) + + +def mesh_triangle_count(mesh): + with mesh.open("rb") as stream: + magic = stream.read(8) + has_materials = magic == b"VFMSH002" + if magic not in (b"VFMSH001", b"VFMSH002"): + raise RuntimeError(f"unsupported mesh scene header: {mesh}") + version, _, tile_count = struct.unpack("<3I", stream.read(12)) + if version != (2 if has_materials else 1): + raise RuntimeError(f"mesh scene version mismatch: {mesh}") + stream.seek(40, 1) + triangles = 0 + for _ in range(tile_count): + stream.seek(16, 1) + _, _, vertex_count, index_count, mask_count = struct.unpack( + "<5I", stream.read(20)) + triangles += index_count // 3 + stream.seek(vertex_count * 24 + (10 * vertex_count if has_materials else 0) + + index_count * 4 + mask_count, 1) + return triangles + + +def set_route_station(view, distance): + points = ROUTE_POINTS[view["passage"]] + length = ROUTE_LENGTHS[view["passage"]] + center, radius, _, side = route_frame(points, distance) + camera = (center[0] + side[0] * 0.75, + center[1] + side[1] * 0.75, + center[2] - radius + 1.6) + target_pos, target_radius = interpolate(points, min(length, distance + 14.0)) + target = (target_pos[0] + side[0] * 2.35, + target_pos[1] + side[1] * 2.35, + target_pos[2] - target_radius + 1.8) + yaw, pitch = angles(camera, target) + if "route_fraction_requested" not in view: + view["route_fraction_requested"] = view["route_fraction"] + view["route_requested_focus"] = view["focus"] + view["route_fraction"] = distance / length + view["route_station_adjustment_m"] = distance - length * view["route_fraction_requested"] + view["focus"] = center + view["camera"] = camera + view["target"] = target + view["yaw"], view["pitch"] = yaw, pitch + view["clip"] = None + + +def render_command(view, mesh, image, camera, yaw, pitch): + args = [RENDERER, mesh, image, + "--size=1600x900", f"--fov-deg={view['fov']:.1f}", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--shade=readable", "--headlamp=on", + "--height-tint=on", "--tile-edges=off", "--tile-skirts=off", + f"--height-center-m={view['focus'][2]:.6f}", "--height-range-m=10", + f"--focus-region={fmt((*view['focus'], view['focus_radius']))}"] + if view["clip"] is not None: + args.append(f"--clip-plane={fmt(view['clip'])}") + return args + + +def export_scene(view, binary, mesh, stem): + camera = view["camera"] + yaw, pitch = angles(camera, view["target"]) + export_args = [binary, DATA, "--world-assembler-prototype", "--stream-benchmark", + "--stream-pipeline", "--workers=8", "--force-generate", "--clip-radius=3", + "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + f"--camera={fmt((*camera, yaw, pitch))}", f"--mesh-export={mesh}"] + row = run(f"{stem}-export", export_args, headless=True) + row["scene_bytes"] = mesh.stat().st_size if mesh.exists() else 0 + row["scene_triangles"] = mesh_triangle_count(mesh) if row["scene_bytes"] else 0 + return row + + +def export_and_render(view, label, binary): + safe = view["name"].replace("/", "-") + stem = f"{safe}-{label}" + mesh = OUT / f"{stem}.vfmesh" + image = IMAGE_DIR / f"{safe}-{label}.png" + camera = view["camera"] + yaw, pitch = angles(camera, view["target"]) + export_row = export_scene(view, binary, mesh, stem) + if export_row["scene_bytes"] <= 0: + raise RuntimeError(f"empty mesh scene: {mesh}") + if export_row["scene_triangles"] == 0: + if label != "before-l98" or view.get("kind") != "mid-tunnel": + raise RuntimeError(f"route station exports no surface triangles: {mesh}") + points = ROUTE_POINTS[view["passage"]] + length = ROUTE_LENGTHS[view["passage"]] + requested_fraction = view["route_fraction"] + requested_distance = length * requested_fraction + found_surface = False + for offset in (5.0, -5.0, 10.0, -10.0, 20.0, -20.0, 35.0, -35.0, + 50.0, -50.0, 75.0, -75.0, 100.0, -100.0, 150.0, + -150.0, 200.0, -200.0): + distance = max(0.0, min(length, requested_distance + offset)) + if distance == requested_distance: + continue + set_route_station(view, distance) + adjusted_stem = f"{stem}-station-{view['route_fraction']:.5f}" + export_row = export_scene(view, binary, mesh, adjusted_stem) + if export_row["scene_triangles"] > 0: + view["camera_anchor"] = "route-station-shifted-to-nearby-geometry" + found_surface = True + break + if not found_surface: + raise RuntimeError(f"no geometry along nearby route stations: {mesh}") + camera = view["camera"] + yaw, pitch = angles(camera, view["target"]) + if label == "before-l98" and view.get("camera_anchor") == "nearest-surface-l98-v5": + anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0) + camera = add(anchor["position_m"], mul(anchor["normal"], 8.0)) + view["camera"] = camera + view["target"] = anchor["position_m"] + view["yaw"], view["pitch"] = angles(camera, view["target"]) + view["clip"] = None + view["surface_anchor"] = anchor + yaw, pitch = view["yaw"], view["pitch"] + render_args = render_command(view, mesh, image, camera, yaw, pitch) + try: + render_row = run(f"{stem}-render", render_args, headless=True) + except RuntimeError: + if view["clip"] is None: + raise + retry_args = [arg for arg in render_args + if not (isinstance(arg, str) and arg.startswith("--clip-plane="))] + render_row = run(f"{stem}-render-no-clip-retry", retry_args, headless=True) + render_row["clip_fallback"] = True + render_row["image"] = str(image.relative_to(ROOT)) + render_row["image_bytes"] = image.stat().st_size if image.exists() else 0 + if render_row["image_bytes"] <= 0: + raise RuntimeError(f"empty image: {image}") + geometry = image_geometry_metrics(image) + if label == "before-l98" and ( + geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2): + # At a few deep route stations the default forward view looks into empty + # sky. Keep the camera on the route station and aim at the nearest L98 + # surface in the exported scene; retain that matched target for L100. + anchor = nearest_surface_anchor(mesh, view["focus"], tile_search_margin_m=256.0) + if anchor["distance_m"] > 60.0: + raise RuntimeError(f"no nearby surface for fallback camera: {image} {anchor}") + camera = view["camera"] + target = anchor["position_m"] + yaw, pitch = angles(camera, target) + view["target"] = target + view["yaw"], view["pitch"] = yaw, pitch + view["clip"] = None + view["camera_anchor"] = "route-station-nearest-surface-target" + view["surface_anchor"] = anchor + retry_args = render_command(view, mesh, image, camera, yaw, pitch) + render_row = run(f"{stem}-render-surface-fallback", retry_args, headless=True) + render_row["image"] = str(image.relative_to(ROOT)) + render_row["image_bytes"] = image.stat().st_size if image.exists() else 0 + geometry = image_geometry_metrics(image) + render_row["geometry"] = geometry + if geometry["visible_geometry_pixels"] < 3000 or geometry["unique_rgb"] < 2: + raise RuntimeError(f"camera sees sky only or too little geometry: {image} {geometry}") + mesh.unlink() + return export_row, render_row + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + IMAGE_DIR.mkdir(parents=True, exist_ok=True) + for path in (*BINARIES.values(), RENDERER, DATA, LAYOUT_PATH): + if not path.is_file(): + raise SystemExit(f"missing input: {path}") + layout = json.loads(LAYOUT_PATH.read_text(encoding="utf-8")) + passages, routes = make_passages(layout) + ROUTE_POINTS.clear() + ROUTE_LENGTHS.clear() + for stable_id, points in routes: + ROUTE_POINTS[stable_id] = points + ROUTE_LENGTHS[stable_id] = route_length(points) + views = [view for passage in passages for view in passage["views"]] + partial_path = OUT / "runs-partial.json" + existing_records = {} + if partial_path.is_file(): + for item in json.loads(partial_path.read_text(encoding="utf-8")): + existing_records[item["view"]["name"]] = item + records = [] + for view in views: + prior = existing_records.get(view["name"]) + camera_matches = (prior is not None and + prior["view"].get("camera_anchor") == view.get("camera_anchor")) + if camera_matches: + for key in ("camera", "target", "yaw", "pitch", "clip", "surface_anchor"): + if key in prior["view"]: + view[key] = prior["view"][key] + if camera_matches and all(version in prior and + (ROOT / prior[version]["render"]["image"]).is_file() + for version in BINARIES): + records.append(prior) + print(f"[L100CaptureSkip] complete_pair={view['name']}", flush=True) + continue + pair = {"view": view} + for label, binary in BINARIES.items(): + export_row, render_row = export_and_render(view, label, binary) + pair[label] = {"export": export_row, "render": render_row} + (OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n", + encoding="utf-8") + pair["comparison"] = {"changed_pixels": changed_pixel_count( + ROOT / pair["before-l98"]["render"]["image"], + ROOT / pair["after-l100"]["render"]["image"])} + (OUT / "runs-partial.json").write_text(json.dumps(records + [pair], indent=2) + "\n", + encoding="utf-8") + records.append(pair) + summary = {"source_layout_identity": layout["stable_identity"], + "view_count": len(views), "paired_png_count": len(views) * 2, + "passages": passages, "views": records} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + print("[L100CaptureSummary] " + json.dumps({ + "views": len(views), "paired_png_count": len(views) * 2, + "images": [row[version]["render"]["image"] for row in records + for version in BINARIES]}, sort_keys=True), flush=True) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100DensityCompare.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100DensityCompare.py new file mode 100644 index 0000000..21cb587 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100DensityCompare.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Compare the direct passage evaluator with the bounded L100 swept tube.""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l100/density-compare" +DATA = ROOT / "Standalone/data/owner_world.json" +HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang" +CAMERAS = { + "SW": (8.0, 16.0, -372.2626953125), + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), + "VS": (-20.0625, -5.068359375, -900.353515625), +} +SUMMARY = re.compile(r"^\[L93PassageReachSummary\] reach_m=4\.00 (.*)$", re.M) +COVERAGE = re.compile(r"^\[L93PassageReachCoverage\] (.*)$", re.M) +TILE = re.compile( + r"^\[L93PassageReachTile\] reach_m=4\.00 key=\(([-\d]+),([-\d]+),([-\d]+),lod(\d+)\) " + r"samples=(\d+) changed=(\d+) sign_flips=(\d+) moved_edges=(\d+) " + r"added_crossings=(\d+) removed_crossings=(\d+) " + r"added_surface_triangles=(\d+) removed_surface_triangles=(\d+) " + r"max_displacement_cm=([\deE+.-]+) max_displacement_xyz_vox=\(([^)]*)\) " + r"displacement_region=(\d+) normal_vertices=(\d+) max_normal_degrees=([\deE+.-]+) " + r"max_normal_xyz_vox=\(([^)]*)\) normal_region=(\d+) changed_mesh=(\d+)$", re.M) +VOXEL_SIZE_M = 0.25 + + +def xyz(text: str) -> list[float]: + return [float(part) for part in text.split(",")] + + +def parse_locations(output: str, strate: str) -> tuple[list[dict], list[dict]]: + rows = [] + for match in TILE.finditer(output): + (x, y, z, lod, samples, changed, flips, moved, added, removed, + added_triangles, removed_triangles, displacement, displacement_xyz, + displacement_region, normal_vertices, angle, normal_xyz, normal_region, + changed_mesh) = match.groups() + disp_vox = xyz(displacement_xyz) + normal_vox = xyz(normal_xyz) + rows.append({ + "strate": strate, "tile": [int(x), int(y), int(z), int(lod)], + "samples": int(samples), "changed_samples": int(changed), + "sign_flips": int(flips), "moved_edges": int(moved), + "surface_added_crossings": int(added), + "surface_removed_crossings": int(removed), + "surface_added_triangles": int(added_triangles), + "surface_removed_triangles": int(removed_triangles), + "max_displacement_cm": float(displacement), + "displacement_location_voxels": disp_vox, + "displacement_location_metres": [value * VOXEL_SIZE_M for value in disp_vox], + "displacement_region": int(displacement_region), + "normal_vertices": int(normal_vertices), "max_normal_degrees": float(angle), + "normal_location_voxels": normal_vox, + "normal_location_metres": [value * VOXEL_SIZE_M for value in normal_vox], + "normal_region": int(normal_region), "changed_mesh": bool(int(changed_mesh)), + }) + top_displacement = sorted(rows, key=lambda row: row["max_displacement_cm"], reverse=True)[:5] + top_normals = sorted(rows, key=lambda row: row["max_normal_degrees"], reverse=True)[:5] + return top_displacement, top_normals + + +def top_unique(rows: list[dict], value_key: str, location_key: str) -> list[dict]: + chosen, seen = [], set() + for row in sorted(rows, key=lambda item: item[value_key], reverse=True): + if row[value_key] <= 0.0: + continue + location = tuple(round(value, 4) for value in row[location_key]) + if location in seen: + continue + seen.add(location) + chosen.append(row) + if len(chosen) == 5: + break + return chosen + + +def run(strate: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"reach-{strate.lower()}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=8", f"--camera={camera}", + "--audit-passage-carve-reach", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + summary_match, coverage_match = SUMMARY.search(output), COVERAGE.search(output) + if summary_match is None or coverage_match is None or result.returncode in (-9, 124): + raise RuntimeError(f"incomplete carve-reach audit: exit={result.returncode} {log}") + reach = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", summary_match.group(1))) + coverage = dict(re.findall(r"([a-zA-Z0-9_]+)=([^ ]+)", coverage_match.group(1))) + top_displacement, top_normals = parse_locations(output, strate) + row = { + "strate": strate, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), "coverage": coverage, + "bounded_sweep_summary": reach, + "bounded_sweep_tile_rows": len(TILE.findall(output)), + "top_displacement_tiles": top_displacement, + "top_normal_tiles": top_normals, + "log": str(log.relative_to(ROOT)), + } + print("[L100DensityCompareRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not HEADLESS.is_file() or not DATA.is_file(): + raise SystemExit(f"missing input: {HEADLESS if not HEADLESS.is_file() else DATA}") + if "--summarize-only" in sys.argv: + rows = json.loads((OUT / "runs-partial.json").read_text(encoding="utf-8")) + else: + rows = [] + for strate in CAMERAS: + rows.append(run(strate)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + all_displacements = [row for result in rows for row in result["top_displacement_tiles"]] + all_normals = [row for result in rows for row in result["top_normal_tiles"]] + top_displacement = top_unique(all_displacements, "max_displacement_cm", + "displacement_location_metres") + top_normals = top_unique(all_normals, "max_normal_degrees", "normal_location_metres") + summary = {"runs": rows, "top_displacement_tiles": top_displacement, + "top_normal_tiles": top_normals} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Hashes.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Hashes.py new file mode 100644 index 0000000..23b6224 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Hashes.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +"""Check L100 whole-view tile hashes across W1/W8/W12 and Clang/GCC.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l100/hashes" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "Clang": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang", + "GCC": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-gcc", +} +CAMERAS = { + "SW": (8.0, 16.0, -372.2626953125), + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), + "VS": (-20.0625, -5.068359375, -900.353515625), +} +TILE_HASH = re.compile( + r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) " + r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) " + r"topology=(0x[0-9a-f]+)$", re.M) +WHOLE_HASH = re.compile( + r"^\[PipelineWholeViewHash\] tiles=(\d+) density=(0x[0-9a-f]+) " + r"mesh=(0x[0-9a-f]+) payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M) +FIELDS = ("density", "mesh", "payload", "topology") + + +def run(strate: str, compiler: str, workers: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"{strate.lower()}-{compiler.lower()}-w{workers}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BINARIES[compiler]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--stream-hash-tiles", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + hashes = { + (int(m.group(1)), int(m.group(2)), int(m.group(3)), int(m.group(4))): + m.group(5, 6, 7, 8) for m in TILE_HASH.finditer(output) + } + whole = WHOLE_HASH.search(output) + stamp = re.search(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", output, re.M) + if (result.returncode or whole is None or stamp is None + or len(hashes) != int(whole.group(1))): + raise RuntimeError(f"hash run failed or incomplete: exit={result.returncode} {log}") + row = { + "strate": strate, "compiler": compiler, "workers": workers, + "source_stamp": stamp.group(1), "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), "tile_count": len(hashes), + "tiles_by_lod": {str(lod): sum(1 for key in hashes if key[0] == lod) + for lod in sorted({key[0] for key in hashes})}, + "whole_hash": list(whole.groups()), "tile_hashes": hashes, + "log": str(log.relative_to(ROOT)), + } + print("[L100HashRun] " + json.dumps( + {key: value for key, value in row.items() if key != "tile_hashes"}, + sort_keys=True), flush=True) + return row + + +def compare(strate: str, reference: dict, candidate: dict) -> dict: + keys = set(reference["tile_hashes"]) | set(candidate["tile_hashes"]) + mismatches = { + field: sum(reference["tile_hashes"].get(key, (None,) * 4)[index] + != candidate["tile_hashes"].get(key, (None,) * 4)[index] + for key in keys) + for index, field in enumerate(FIELDS) + } + row = { + "strate": strate, "reference": f"{reference['compiler']}-w{reference['workers']}", + "candidate": f"{candidate['compiler']}-w{candidate['workers']}", + "tile_counts_match": reference["tile_count"] == candidate["tile_count"], + "whole_hashes_match": reference["whole_hash"] == candidate["whole_hash"], + "tile_hashes_match": all(value == 0 for value in mismatches.values()) + and reference["tile_hashes"].keys() == candidate["tile_hashes"].keys(), + "component_mismatch_tiles": mismatches, + "tiles_reference": reference["tile_count"], "tiles_candidate": candidate["tile_count"], + "reference_whole_hash": reference["whole_hash"], + "candidate_whole_hash": candidate["whole_hash"], + } + print("[L100HashComparison] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + detail, comparisons = [], [] + for strate in CAMERAS: + group = {(compiler, workers): run(strate, compiler, workers) + for compiler in BINARIES for workers in (1, 8, 12)} + reference = group[("Clang", 1)] + for key, candidate in group.items(): + if key == ("Clang", 1): + continue + comparisons.append(compare(strate, reference, candidate)) + detail.extend({key: value for key, value in row.items() if key != "tile_hashes"} + for row in group.values()) + (OUT / "runs-partial.json").write_text(json.dumps( + {"runs": detail, "comparisons": comparisons}, indent=2) + "\n", + encoding="utf-8") + summary = {"runs": detail, "comparisons": comparisons} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 if all(row["tile_counts_match"] and row["whole_hashes_match"] + and row["tile_hashes_match"] for row in comparisons) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Readiness.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Readiness.py new file mode 100644 index 0000000..a8696de --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100Readiness.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""L80 cold-readiness protocol: three interleaved L98/L99 pairs at W8 and W12.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT_ROOT = ROOT / "Saved/Linux/l100/readiness" +DATA = ROOT / "Standalone/data/owner_world.json" +ALL_BINARIES = { + "L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19", + "L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang", + "L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang", +} +OUT = OUT_ROOT +BINARIES = {} +# Resolved owner camera index -> stratum. Keep the labels tied to the canonical layout. +CAMERAS = { + "SW": (8.0, 16.0, -372.2626953125), + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), + "VS": (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) " + r"cold_ready_s=([0-9.]+)$", re.M) + + +def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "timeout", "1740s", str(BINARIES[label]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--stream-scheduler-policy=3", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + pipeline_match, near_match = PIPELINE.search(output), NEAR.search(output) + if result.returncode or pipeline_match is None or near_match is None: + raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, "workers": workers, "binary": label, "phase": phase, + "replicate": rep, "exit_code": result.returncode, + "wall_s": round(time.monotonic() - start, 3), + "lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}", + "lod02_ready_s": float(near_match.group(3)), + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": pipeline["whole_tiles"].split("/")[0], + "log": str(log.relative_to(ROOT)), + } + print("[L100ReadinessRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + global BINARIES, OUT + pair = sys.argv[1:] or ["L98", "L100"] + if len(pair) != 2 or len(set(pair)) != 2 or any(item not in ALL_BINARIES for item in pair): + raise SystemExit("usage: RunL100Readiness.py L98 L100 | L99 L100") + BINARIES = {label: ALL_BINARIES[label] for label in pair} + OUT = OUT_ROOT / f"{pair[0].lower()}-{pair[1].lower()}" + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + rows = [] + first, second = tuple(BINARIES) + orders = ((first, second), (second, first), (first, second)) + for workers in (8, 12): + for strate in CAMERAS: + for label in BINARIES: + run(strate, workers, label, "warmup", 0) + for strate in CAMERAS: + for rep, order in enumerate(orders, 1): + for label in order: + rows.append(run(strate, workers, label, "measured", rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + summary = [] + for workers in (8, 12): + for strate in CAMERAS: + group = [row for row in rows + if row["workers"] == workers and row["strate"] == strate] + item = {"workers": workers, "strate": strate, "measured_pairs": 3} + for label in BINARIES: + subset = [row for row in group if row["binary"] == label] + for metric in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[metric] for row in subset] + item[f"{label}_{metric}_runs"] = values + item[f"{label}_{metric}_median"] = statistics.median(values) + item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset] + summary.append(item) + (OUT / "summary.json").write_text(json.dumps( + {"comparison_pair": pair, "runs": rows, "summary": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100ReadinessTriple.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100ReadinessTriple.py new file mode 100644 index 0000000..b4dbadb --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100ReadinessTriple.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""L80 protocol with balanced, rotating L98/L99/L100 order.""" +from __future__ import annotations + +import json +import re +import statistics +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l100/readiness/l98-l99-l100" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L98": ROOT / "Saved/Linux/l98/bin/VoxelForgeCoreHeadless-l98-clang19", + "L99": ROOT / "Saved/Linux/l99/bin/VoxelForgeCoreHeadless-l99-clang", + "L100": ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang", +} +CAMERAS = { + "SW": (8.0, 16.0, -372.2626953125), + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), + "VS": (-20.0625, -5.068359375, -900.353515625), +} +PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +NEAR = re.compile(r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) " + r"cold_ready_s=([0-9.]+)$", re.M) +STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=([0-9a-f]{64})$", re.M) + + +def run(strate: str, workers: int, label: str, phase: str, rep: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"w{workers}-{strate.lower()}-{label.lower()}-{phase}{rep}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "env", "timeout", "1740s", str(BINARIES[label]), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8", + "--max-level=8", f"--camera={camera}", "--profile-lod-work", + "--stream-scheduler-policy=3", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + pipeline_match, near_match, stamp_match = ( + PIPELINE.search(output), NEAR.search(output), STAMP.search(output)) + if result.returncode or pipeline_match is None or near_match is None or stamp_match is None: + raise RuntimeError(f"readiness run failed: exit={result.returncode} {log}") + pipeline = dict(FIELDS.findall(pipeline_match.group(1))) + row = { + "strate": strate, "workers": workers, "binary": label, "phase": phase, + "replicate": rep, "exit_code": result.returncode, + "source_stamp": stamp_match.group(1), "wall_s": round(time.monotonic() - start, 3), + "lod02_tiles": f"{near_match.group(1)}/{near_match.group(2)}", + "lod02_ready_s": float(near_match.group(3)), + "whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")), + "whole_tiles": pipeline["whole_tiles"].split("/")[0], + "log": str(log.relative_to(ROOT)), + } + print("[L100ReadinessTripleRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + for binary in (*BINARIES.values(), DATA): + if not binary.is_file(): + raise SystemExit(f"missing input: {binary}") + rows = [] + labels = tuple(BINARIES) + orders = (labels, labels[1:] + labels[:1], labels[2:] + labels[:2]) + for workers in (8, 12): + for strate in CAMERAS: + for label in labels: + run(strate, workers, label, "warmup", 0) + for strate in CAMERAS: + for rep, order in enumerate(orders, 1): + for label in order: + rows.append(run(strate, workers, label, "measured", rep)) + (OUT / "runs-partial.json").write_text(json.dumps(rows, indent=2) + "\n", + encoding="utf-8") + summary = [] + for workers in (8, 12): + for strate in CAMERAS: + group = [row for row in rows if row["workers"] == workers + and row["strate"] == strate] + item = {"workers": workers, "strate": strate, "measured_runs": 3} + for label in labels: + subset = [row for row in group if row["binary"] == label] + for metric in ("lod02_ready_s", "whole_view_ready_s"): + values = [row[metric] for row in subset] + item[f"{label}_{metric}_runs"] = values + item[f"{label}_{metric}_median"] = statistics.median(values) + item[f"{label}_process_wall_s_runs"] = [row["wall_s"] for row in subset] + summary.append(item) + (OUT / "summary.json").write_text(json.dumps( + {"comparison_triple": labels, "runs": rows, "summary": summary}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/RunL100SweepCounts.py b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100SweepCounts.py new file mode 100644 index 0000000..b19906a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/RunL100SweepCounts.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Profile L100 swept-tube lookups and direct route fallback work.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l100/sweep-counts" +DATA = ROOT / "Standalone/data/owner_world.json" +HEADLESS = ROOT / "Saved/Linux/l100/bin/VoxelForgeCoreHeadless-l100-clang" +CAMERAS = { + "SW": (8.0, 16.0, -372.2626953125), + "FI": (25.8369140625, 3.19921875, -737.0), + "TN": (19.166015625, -14.2001953125, -753.3740234375), + "VS": (-20.0625, -5.068359375, -900.353515625), +} +LINE = re.compile(r"^\[PassageComposerTile\] key=\([^,]+,[^,]+,[^,]+,lod(\d+)\) (.*)$", re.M) +FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)") +COUNT_KEYS = ( + "route_candidates", "sweep_segments", "sweep_pairs", "sweep_outside_pairs", + "sweep_min_updates", "sweep_base_samples", "sweep_detail_samples", + "sweep_grid_samples", "sweep_no_influence_samples", "sweep_lookups", + "sweep_cache_hits", "sweep_base_hits", "sweep_outside_hits", "sweep_missing", + "carve_segment_visits", "support_segment_visits", "mouth_route_segment_checks", +) + + +def run(strate: str) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"profile-sweep-{strate.lower()}.log" + camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(HEADLESS), str(DATA), "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--workers=8", "--clip-radius=3", + "--max-clip-level=8", "--max-level=8", f"--camera={camera}", + "--profile-passage-composer", + ] + start = time.monotonic() + with log.open("w", encoding="utf-8") as stream: + result = subprocess.run(command, cwd=ROOT, stdout=stream, + stderr=subprocess.STDOUT, check=False, timeout=1740) + output = log.read_text(encoding="utf-8", errors="replace") + if result.returncode != 0: + raise RuntimeError(f"sweep count run failed: exit={result.returncode} {log}") + rows = [] + for level_text, tail in LINE.findall(output): + level = int(level_text) + if level > 2: + continue + fields = dict(FIELDS.findall(tail)) + rows.append({"lod": level, **{key: int(fields.get(key, 0)) for key in COUNT_KEYS}}) + if not rows: + raise RuntimeError(f"no profiled near-set passage tiles: {log}") + totals = {key: sum(row[key] for row in rows) for key in COUNT_KEYS} + row = { + "strate": strate, "workers": 8, "near_profiled_tiles": len(rows), + "near_tiles_by_lod": {str(level): sum(1 for item in rows if item["lod"] == level) + for level in range(3)}, + "counters": totals, "wall_s": round(time.monotonic() - start, 3), + "log": str(log.relative_to(ROOT)), + } + print("[L100SweepCounts] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + rows = [run(strate) for strate in CAMERAS] + (OUT / "summary.json").write_text(json.dumps({"runs": rows}, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/INDEX.md b/Standalone/Research/WorldAssemblerM3/Linux100/captures/INDEX.md new file mode 100644 index 0000000..6f2ff8a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux100/captures/INDEX.md @@ -0,0 +1,37 @@ +# L100 passage captures + +Each route has paired L98 and L100 views at both mouths and at 25%, 50%, and 75% stations. The camera gate decodes each PNG and requires visible geometry (>3,000 pixels after excluding the dominant background color, with at least two RGB colors). Changed pixels count RGB pixels with any channel difference between a matched pair. + +L100 source stamp: `7224bf6e668d06ed71a92f0857820e3ae2b0eca40574a491331ab5979a88f698`. + +| View | L98 geometry px | L100 geometry px | Changed px | Images | +|---|---:|---:|---:|---| +| `0-1 · upper-mouth` | 1,063,522 | 1,063,522 | 41,441 | [L98](passage-0-1-0-upper-mouth-before-l98.png) / [L100](passage-0-1-0-upper-mouth-after-l100.png) | +| `0-1 · lower-mouth` | 1,420,316 | 1,420,316 | 23,644 | [L98](passage-0-1-0-lower-mouth-before-l98.png) / [L100](passage-0-1-0-lower-mouth-after-l100.png) | +| `0-1 · mid-25 (27.5%); station +50 m to surface; surface-target fallback` | 422,920 | 422,935 | 706 | [L98](passage-0-1-0-mid-25-before-l98.png) / [L100](passage-0-1-0-mid-25-after-l100.png) | +| `0-1 · mid-50 (50.0%)` | 1,375,241 | 1,375,118 | 20,082 | [L98](passage-0-1-0-mid-50-before-l98.png) / [L100](passage-0-1-0-mid-50-after-l100.png) | +| `0-1 · mid-75 (75.0%)` | 1,368,802 | 1,368,801 | 12,919 | [L98](passage-0-1-0-mid-75-before-l98.png) / [L100](passage-0-1-0-mid-75-after-l100.png) | +| `1-2 · upper-mouth` | 1,425,620 | 1,426,619 | 292,188 | [L98](passage-1-2-0-upper-mouth-before-l98.png) / [L100](passage-1-2-0-upper-mouth-after-l100.png) | +| `1-2 · lower-mouth` | 223,508 | 223,508 | 57,795 | [L98](passage-1-2-0-lower-mouth-before-l98.png) / [L100](passage-1-2-0-lower-mouth-after-l100.png) | +| `1-2 · mid-25 (25.0%)` | 1,409,914 | 1,409,907 | 15,713 | [L98](passage-1-2-0-mid-25-before-l98.png) / [L100](passage-1-2-0-mid-25-after-l100.png) | +| `1-2 · mid-50 (50.0%)` | 1,407,415 | 1,407,415 | 32,217 | [L98](passage-1-2-0-mid-50-before-l98.png) / [L100](passage-1-2-0-mid-50-after-l100.png) | +| `1-2 · mid-75 (75.0%)` | 1,261,589 | 1,264,705 | 263,048 | [L98](passage-1-2-0-mid-75-before-l98.png) / [L100](passage-1-2-0-mid-75-after-l100.png) | +| `2-3 · upper-mouth` | 60,506 | 60,506 | 0 | [L98](passage-2-3-0-upper-mouth-before-l98.png) / [L100](passage-2-3-0-upper-mouth-after-l100.png) | +| `2-3 · lower-mouth` | 60,257 | 60,257 | 0 | [L98](passage-2-3-0-lower-mouth-before-l98.png) / [L100](passage-2-3-0-lower-mouth-after-l100.png) | +| `2-3 · mid-25 (25.0%); surface-target fallback` | 631,345 | 631,345 | 0 | [L98](passage-2-3-0-mid-25-before-l98.png) / [L100](passage-2-3-0-mid-25-after-l100.png) | +| `2-3 · mid-50 (50.0%); surface-target fallback` | 629,698 | 629,698 | 4,208 | [L98](passage-2-3-0-mid-50-before-l98.png) / [L100](passage-2-3-0-mid-50-after-l100.png) | +| `2-3 · mid-75 (75.0%)` | 1,163,903 | 1,163,903 | 0 | [L98](passage-2-3-0-mid-75-before-l98.png) / [L100](passage-2-3-0-mid-75-after-l100.png) | + +## Same-camera 1–2 regression check + +The L99 and L100 midpoint records have identical focus, camera, target, yaw, and pitch. This three-version check decodes every image and passes the same geometry gate. + +| Version | Image | Visible geometry px | Changed px from L98 | +|---|---|---:|---:| +| L98 | [before](../../Linux99/captures/passage-1-2-0-mid-tunnel-before-l98.png) | 1,407,415 | — | +| L99 | [flat-tunnel capture](../../Linux99/captures/passage-1-2-0-mid-tunnel-after-l99.png) | 1,206,362 | 1,135,959 | +| L100 | [fixed capture](passage-1-2-0-mid-50-after-l100.png) | 1,407,415 | 32,217 | + +L99→L100 changed 1,134,150 pixels. The L100 view returns to the L98 image at this camera (L98→L100: 32,217). + +All 30 L98/L100 PNGs passed the geometry gate. Capture logs, camera coordinates, exports, and raw metadata are under `Saved/Linux/l100/captures/`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-before-l98.png new file mode 100644 index 0000000..48750dd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-look-anchor.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-look-anchor.png new file mode 100644 index 0000000..1a4397e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/pass-2-3-mid-50-look-anchor.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-after-l100.png new file mode 100644 index 0000000..e66c556 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-before-l98.png new file mode 100644 index 0000000..44d6c3a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-lower-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-after-l100.png new file mode 100644 index 0000000..010455b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-before-l98.png new file mode 100644 index 0000000..8100460 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-25-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-after-l100.png new file mode 100644 index 0000000..470075a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-before-l98.png new file mode 100644 index 0000000..74cf2b1 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-50-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-after-l100.png new file mode 100644 index 0000000..437aa15 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-before-l98.png new file mode 100644 index 0000000..cd8501d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-mid-75-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-after-l100.png new file mode 100644 index 0000000..40992af Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-before-l98.png new file mode 100644 index 0000000..040f0b2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-0-1-0-upper-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-after-l100.png new file mode 100644 index 0000000..dc231dc Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-before-l98.png new file mode 100644 index 0000000..847712f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-lower-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-after-l100.png new file mode 100644 index 0000000..57969da Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-before-l98.png new file mode 100644 index 0000000..9933999 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-25-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-after-l100.png new file mode 100644 index 0000000..fcbfdaf Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-before-l98.png new file mode 100644 index 0000000..32cf537 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-50-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-after-l100.png new file mode 100644 index 0000000..77a9bbe Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-before-l98.png new file mode 100644 index 0000000..a2c925a Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-mid-75-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-after-l100.png new file mode 100644 index 0000000..759be2b Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-before-l98.png new file mode 100644 index 0000000..5b11a3f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-1-2-0-upper-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-after-l100.png new file mode 100644 index 0000000..d89372f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-before-l98.png new file mode 100644 index 0000000..d89372f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-lower-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-after-l100.png new file mode 100644 index 0000000..dc87057 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-before-l98.png new file mode 100644 index 0000000..dc87057 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-25-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-after-l100.png new file mode 100644 index 0000000..178f6a3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-before-l98.png new file mode 100644 index 0000000..1a4397e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-50-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-after-l100.png new file mode 100644 index 0000000..02ed372 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-before-l98.png new file mode 100644 index 0000000..02ed372 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-mid-75-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-after-l100.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-after-l100.png new file mode 100644 index 0000000..09fc90d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-after-l100.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-before-l98.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-before-l98.png new file mode 100644 index 0000000..09fc90d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/passage-2-3-0-upper-mouth-before-l98.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux100/captures/test-q25-anchor.png b/Standalone/Research/WorldAssemblerM3/Linux100/captures/test-q25-anchor.png new file mode 100644 index 0000000..26f91a9 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux100/captures/test-q25-anchor.png differ diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index fc6efbd..e2eb246 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -135,7 +135,9 @@ namespace StandaloneStreamBenchmark struct FPassageClampComparison { std::uint64_t samples=0,changedSamples=0,signFlips=0; - std::uint64_t crossingEdgeMismatches=0,sharedCrossingEdges=0,movedCrossingEdges=0; + std::uint64_t crossingEdgeMismatches=0,addedCrossingEdges=0,removedCrossingEdges=0; + std::uint64_t sharedCrossingEdges=0,movedCrossingEdges=0; + std::uint64_t addedSurfaceTriangles=0,removedSurfaceTriangles=0; std::uint64_t normalVertices=0,normalPairingFailures=0,changedNormalVertices=0; double maxAbsDensityDelta=0.0,maxSurfaceDisplacementCm=0.0; double sumSurfaceDisplacementCm=0.0,maxNormalAngleDegrees=0.0; @@ -251,6 +253,8 @@ namespace StandaloneStreamBenchmark const bool crossBase=solid(a0)!=solid(a1); const bool crossCandidate=solid(b0)!=solid(b1); if(crossBase!=crossCandidate)++result.crossingEdgeMismatches; + if(!crossBase&&crossCandidate)++result.addedCrossingEdges; + else if(crossBase&&!crossCandidate)++result.removedCrossingEdges; if(!crossBase||!crossCandidate)return; const double ta=static_cast(a0)/(static_cast(a0)-a1); const double tb=static_cast(b0)/(static_cast(b0)-b1); @@ -324,6 +328,20 @@ namespace StandaloneStreamBenchmark result.meshChanged=Base.meshHash!=Candidate.meshHash ||Base.vertices.size()!=Candidate.vertices.size() ||Base.indices!=Candidate.indices; + const auto surfaceTriangleCount=[](const VoxelForgeCore::TileMesh& Mesh) + { + const std::size_t triangleCount=Mesh.indices.size()/3; + if(Mesh.skirtFaceMasks.size()!=triangleCount) + return static_cast(triangleCount); + return static_cast(std::count(Mesh.skirtFaceMasks.begin(), + Mesh.skirtFaceMasks.end(),static_cast(0))); + }; + const std::uint64_t baseSurfaceTriangles=surfaceTriangleCount(Base); + const std::uint64_t candidateSurfaceTriangles=surfaceTriangleCount(Candidate); + if(candidateSurfaceTriangles>baseSurfaceTriangles) + result.addedSurfaceTriangles=candidateSurfaceTriangles-baseSurfaceTriangles; + else + result.removedSurfaceTriangles=baseSurfaceTriangles-candidateSurfaceTriangles; result.valid=true; return result; } @@ -445,7 +463,8 @@ namespace StandaloneStreamBenchmark VoxelForgeCore::TileGenerationOptions candidateOptions=baseOptions; candidateOptions.profilePassageComposer=true; std::array tileCount{},sampleCount{},changedSamples{},signFlips{}, - crossingMismatches{},sharedCrossings{},movedCrossings{},changedTiles{}, + crossingMismatches{},addedCrossings{},removedCrossings{},sharedCrossings{}, + movedCrossings{},addedSurfaceTriangles{},removedSurfaceTriangles{},changedTiles{}, normalVertices{},normalPairingFailures{},changedNormalVertices{}; std::array movedRegions[4]{},normalRegions[4]{}; std::array maxDisplacementCm{},sumDisplacementCm{},maxNormalAngle{}; @@ -494,8 +513,12 @@ namespace StandaloneStreamBenchmark changedSamples[floorIndex]+=comparison.changedSamples; signFlips[floorIndex]+=comparison.signFlips; crossingMismatches[floorIndex]+=comparison.crossingEdgeMismatches; + addedCrossings[floorIndex]+=comparison.addedCrossingEdges; + removedCrossings[floorIndex]+=comparison.removedCrossingEdges; sharedCrossings[floorIndex]+=comparison.sharedCrossingEdges; movedCrossings[floorIndex]+=comparison.movedCrossingEdges; + addedSurfaceTriangles[floorIndex]+=comparison.addedSurfaceTriangles; + removedSurfaceTriangles[floorIndex]+=comparison.removedSurfaceTriangles; changedTiles[floorIndex]+=comparison.meshChanged?1u:0u; normalVertices[floorIndex]+=comparison.normalVertices; normalPairingFailures[floorIndex]+=comparison.normalPairingFailures; @@ -511,13 +534,17 @@ namespace StandaloneStreamBenchmark movedRegions[floorIndex][region]+=comparison.movedEdgeRegions[region]; normalRegions[floorIndex][region]+=comparison.normalVertexRegions[region]; } - std::printf("[%s] %s=%.2f key=(%d,%d,%d,lod%d) samples=%llu changed=%llu sign_flips=%llu moved_edges=%llu max_displacement_cm=%.6g max_displacement_xyz_vox=(%.6g,%.6g,%.6g) displacement_region=%d normal_vertices=%llu max_normal_degrees=%.6g max_normal_xyz_vox=(%.6g,%.6g,%.6g) normal_region=%d changed_mesh=%d\n", + std::printf("[%s] %s=%.2f key=(%d,%d,%d,lod%d) samples=%llu changed=%llu sign_flips=%llu moved_edges=%llu added_crossings=%llu removed_crossings=%llu added_surface_triangles=%llu removed_surface_triangles=%llu max_displacement_cm=%.6g max_displacement_xyz_vox=(%.6g,%.6g,%.6g) displacement_region=%d normal_vertices=%llu max_normal_degrees=%.6g max_normal_xyz_vox=(%.6g,%.6g,%.6g) normal_region=%d changed_mesh=%d\n", tileTag,valueName,Candidates[floorIndex],key.coordinate.x, key.coordinate.y,key.coordinate.z,key.lod, static_cast(comparison.samples), static_cast(comparison.changedSamples), static_cast(comparison.signFlips), static_cast(comparison.movedCrossingEdges), + static_cast(comparison.addedCrossingEdges), + static_cast(comparison.removedCrossingEdges), + static_cast(comparison.addedSurfaceTriangles), + static_cast(comparison.removedSurfaceTriangles), comparison.maxSurfaceDisplacementCm, comparison.maxSurfaceLocationVoxels[0], comparison.maxSurfaceLocationVoxels[1], @@ -544,13 +571,17 @@ namespace StandaloneStreamBenchmark &&maxNormalAngle[floorIndex]<=2.0 &&normalPairingFailures[floorIndex]==0; acceptedAtAllFloors=acceptedAtAllFloors&&accepted; - std::printf("[%s] %s=%.2f tiles=%llu/%zu samples=%llu changed_samples=%llu sign_flips=%llu edge_crossing_mismatches=%llu shared_crossings=%llu moved_crossings=%llu max_displacement_cm=%.6g mean_displacement_cm=%.6g changed_mesh_tiles=%llu normal_vertices=%llu changed_normal_vertices=%llu max_normal_degrees=%.6g mean_normal_degrees=%.6g normal_pairing_failures=%llu moved_regions=(wall:%llu,floor:%llu,mouth:%llu,other:%llu) normal_regions=(wall:%llu,floor:%llu,mouth:%llu,other:%llu) status=%s\n", + std::printf("[%s] %s=%.2f tiles=%llu/%zu samples=%llu changed_samples=%llu sign_flips=%llu edge_crossing_mismatches=%llu added_crossings=%llu removed_crossings=%llu added_surface_triangles=%llu removed_surface_triangles=%llu shared_crossings=%llu moved_crossings=%llu max_displacement_cm=%.6g mean_displacement_cm=%.6g changed_mesh_tiles=%llu normal_vertices=%llu changed_normal_vertices=%llu max_normal_degrees=%.6g mean_normal_degrees=%.6g normal_pairing_failures=%llu moved_regions=(wall:%llu,floor:%llu,mouth:%llu,other:%llu) normal_regions=(wall:%llu,floor:%llu,mouth:%llu,other:%llu) status=%s\n", summaryTag,valueName,Candidates[floorIndex], static_cast(tileCount[floorIndex]), keys.size(),static_cast(sampleCount[floorIndex]), static_cast(changedSamples[floorIndex]), static_cast(signFlips[floorIndex]), static_cast(crossingMismatches[floorIndex]), + static_cast(addedCrossings[floorIndex]), + static_cast(removedCrossings[floorIndex]), + static_cast(addedSurfaceTriangles[floorIndex]), + static_cast(removedSurfaceTriangles[floorIndex]), static_cast(sharedCrossings[floorIndex]), static_cast(movedCrossings[floorIndex]), maxDisplacementCm[floorIndex],meanDisplacement, diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v216.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v216.baseline new file mode 100644 index 0000000..0be9f81 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-floating-islands-style2-v216.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0x91931a57a067fd92 mesh=0xa818654be89790ac payload=0x370ff80451105a39 topology=0x06e507557aa70173 +[PipelineWholeViewHash] tiles=2370 density=0x04e4e5331bbe6f55 mesh=0xe3227377c85959bb payload=0x0120de02ca1b8612 topology=0x2d9147c7d0652202 +[PipelineLODHash] lod=0 tiles=512 density=0x2fd4c78685a43cfe mesh=0x50478cff1c1f8ca0 payload=0x54e48e57e0af26de topology=0x860c011cfdb9833a +[PipelineLODHash] lod=1 tiles=448 density=0x4b8d9a6dcd2942ad mesh=0x17e89b7087bfe711 payload=0x2389c109eb780db6 topology=0x7eccd4cf560d0630 +[PipelineLODHash] lod=2 tiles=320 density=0x887c080054e08062 mesh=0x643306c781381ac3 payload=0x5a029bbf0ecf0899 topology=0x2aba1d5e7c78e07d +[PipelineLODHash] lod=3 tiles=208 density=0xeffd03aa95184046 mesh=0x69658616eb1ec1f8 payload=0xda2347980a925061 topology=0x8adea07216ddcab5 +[PipelineLODHash] lod=4 tiles=240 density=0xfb11591772a3d18e mesh=0xebf2baee8764591b payload=0xe491d7ac9c0061ad topology=0xd618ac33b95ce494 +[PipelineLODHash] lod=5 tiles=144 density=0x2f8b7b7fb1e67258 mesh=0x4cd508cbe97344fd payload=0x2b6e96a6050458c9 topology=0x4ff6d0eda8988573 +[PipelineLODHash] lod=6 tiles=96 density=0x0114a34c8b8d6113 mesh=0xb1fec4d01ae1fa44 payload=0x3e50cfd429b8e950 topology=0x08090c96d15c1618 +[PipelineLODHash] lod=7 tiles=96 density=0x3836ed8cfce73418 mesh=0x40bc6589c5fc1aac payload=0x677349bcae16790f topology=0x36e3aa53192e6a6b +[PipelineLODHash] lod=8 tiles=306 density=0x144f3c1fd722708b mesh=0xef77b28bf7fd9453 payload=0xe3f47b37fcd61489 topology=0x5a3c73e295494e04 +[PipelineExactQueryHash] probes=9 density=0x7b7c50ac24ccadcc unclamped=0x7b7c50ac24ccadcc detail=0xefcba20c0c70c415 collision=0xe604823a249029bf path=direct-Core-queries diff --git a/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v216.baseline b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v216.baseline new file mode 100644 index 0000000..859bb26 --- /dev/null +++ b/Standalone/Tools/verify/baselines/owner-hash/canonical/owner-hash-surface-world-style1-v216.baseline @@ -0,0 +1,12 @@ +[PipelineFullDetailHash] tiles=960 density=0xace56c8f205238ad mesh=0xa9c603a1248de6b8 payload=0xea55f1f4fcc6a894 topology=0x5ea7bc78a1b678ae +[PipelineWholeViewHash] tiles=2370 density=0x7d1d79ee94ecf595 mesh=0x7c7925b2571d45b6 payload=0xc99238c42f529f6e topology=0xdea49770adf20e81 +[PipelineLODHash] lod=0 tiles=512 density=0xcd6ab28cfa504aca mesh=0xf03dc80a669fca31 payload=0x397aded1c3d2d297 topology=0xb4a1dc84fabc1fe3 +[PipelineLODHash] lod=1 tiles=448 density=0xce6e713828b2a20a mesh=0x1cf408c25d67d18c payload=0x8a2b5d20571c997e topology=0x51cd985fe8e7d100 +[PipelineLODHash] lod=2 tiles=320 density=0x6f77caff39bfc88c mesh=0xda8c8559d1e2342a payload=0x7ca57886199a4e51 topology=0x943f98568f253e04 +[PipelineLODHash] lod=3 tiles=208 density=0x4b4c1567c770f6d5 mesh=0x7b976f486b788378 payload=0x889f0028d509efb2 topology=0x4f7c7df6cbbf9730 +[PipelineLODHash] lod=4 tiles=240 density=0x0be8798407724eaf mesh=0x131c303cbbe51434 payload=0x2a90ecf0a2411ca0 topology=0xe3e054a371152e08 +[PipelineLODHash] lod=5 tiles=144 density=0xf20897ad272103a9 mesh=0x13d00b6253ae1f72 payload=0x0db3880693a962fa topology=0x462bd87a80f15e89 +[PipelineLODHash] lod=6 tiles=96 density=0xc040fcac457d6313 mesh=0x1ed132eeb718fb62 payload=0xcd0214e35577c6e3 topology=0x2c56c69e6fdcfd64 +[PipelineLODHash] lod=7 tiles=96 density=0xc2d419446355fdb8 mesh=0x4ea44452574fc84b payload=0xfd9779bedfc9ae0f topology=0x8948546e770c9b72 +[PipelineLODHash] lod=8 tiles=306 density=0x62db31696f2da831 mesh=0xd373f9e9481532fe payload=0x540a60526e0e3e46 topology=0xc725cb901e6dc335 +[PipelineExactQueryHash] probes=9 density=0xc949c4df2ce6f9d9 unclamped=0xc949c4df2ce6f9d9 detail=0x114c501e2d830d94 collision=0xe6011d3a248d4849 path=direct-Core-queries diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index a566510..53345cb 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -122,11 +122,11 @@ RECIPE_RE = re.compile( def owner_hash_baseline(recipe: str, style: int) -> tuple[Path, str]: if recipe == "surface-world" and style == 1: - return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v215.baseline", - "L99-v215-swept-tube") + return (BASELINES / "owner-hash/canonical/owner-hash-surface-world-style1-v216.baseline", + "L100-v216-tunnel-owns-interior") if recipe == "floating-islands" and style == 2: - return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v215.baseline", - "L99-v215-swept-tube") + return (BASELINES / "owner-hash/canonical/owner-hash-floating-islands-style2-v216.baseline", + "L100-v216-tunnel-owns-interior") raise VerificationFailure(f"no pinned owner hash baseline for {recipe} style {style}") @@ -663,7 +663,7 @@ def run_hash_pairs(runner: VerificationRunner, diagnostic = {"name": "owner-hash-surface-world-matches-L52-diagnostic", "status": "DIAGNOSTIC", "detail": f"matches={hashes[12] == l52_hashes} L52 rows={len(l52_hashes)}; " - "the v215 bounded swept-tube passage change has its own pinned baseline"} + "the v216 passage interior ownership change has its own pinned baseline"} runner.steps.append(diagnostic) print(f"[{runner.tier}] {diagnostic['name']} {diagnostic['detail']}", flush=True) baseline = BASELINES / "owner-hash/l46" / f"owner-hash-{recipe}-style{style}-w12.baseline"