From c0c204ba5092fee15caf7763a350d7d3c2fd7fde Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 26 Oct 2026 01:50:15 +0100 Subject: [PATCH] fix: signed-overflow UB in VoxelHash made cave placement compiler-dependent; all generators LOD0-8; speed matrix (L60) VoxelHash::Cell/Pair, legacy Cellular3D and the shared Core cellular hash added 0x7FFFFFFF to a signed int32 before casting to uint32; overflow is undefined, so Clang and GCC placed cave passage 1 differently (first differing sample at LOD1 tile (0,64,-512)). The bias is now done in unsigned arithmetic, in both the standalone copy and the Unreal plugin header (Source/VoxelForge/Public/VoxelCaveMorphology.h). Native caves, motifs and lattice now stream LOD0-8 with skirts and cross-level transitions. New verify.py --speed-matrix runs every generator on one asserted-identical view (5 interleaved runs): motifs 1.33, profiles 1.30-1.32, caves 1.41, natural SW 1.67, lattice 1.70, FI 4.93 s whole view. fast/medium/full PASS, Clang/GCC peer hashes equal. Research R29 (open-edge ledge) and R30 (compiler-divergence suspects). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../VoxelForge/Public/VoxelCaveMorphology.h | 29 +- .../include/VoxelForgeCore/VoxelForgeCore.h | 4 +- Standalone/Core/src/ShapeWorldInternal.h | 32 +- Standalone/Core/src/VoxelForgeFieldShared.h | 6 +- .../Linux60/CaptureFarLODsL60.py | 219 ++++++++++ .../WorldAssemblerM3/Linux60/REPORT.md | 77 ++++ .../Linux60/captures/caves-far-lod.jpg | Bin 0 -> 16642 bytes .../Linux60/captures/lattice-far-lod.jpg | Bin 0 -> 30714 bytes .../Linux60/captures/motifs-far-lod.jpg | Bin 0 -> 24926 bytes .../WorldAssemblerM3/Research29/REPORT.md | 114 ++++++ .../WorldAssemblerM3/Research29/first-pass.md | 78 ++++ .../WorldAssemblerM3/Research30/REPORT.md | 29 ++ Standalone/Source/StreamBenchmarkHeadless.cpp | 8 +- .../VoxelForge/Public/VoxelCaveMorphology.h | 29 +- Standalone/Tools/verify/verify.py | 378 ++++++++++++++++-- 15 files changed, 938 insertions(+), 65 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux60/CaptureFarLODsL60.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux60/captures/caves-far-lod.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux60/captures/lattice-far-lod.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Linux60/captures/motifs-far-lod.jpg create mode 100644 Standalone/Research/WorldAssemblerM3/Research29/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Research29/first-pass.md create mode 100644 Standalone/Research/WorldAssemblerM3/Research30/REPORT.md diff --git a/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Source/VoxelForge/Public/VoxelCaveMorphology.h index c5c47b9..ba56cb1 100644 --- a/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -261,8 +261,12 @@ namespace VoxelHash FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed) { uint32 H = Seed; - H ^= Mix((uint32)(CellX + 0x7FFFFFFF)); - H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u); + // Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for + // positive cells and makes the world hash compiler-dependent. + const uint32 BiasedX = static_cast(CellX) + 0x7FFFFFFFu; + const uint32 BiasedY = static_cast(CellY) + 0x7FFFFFFFu; + H ^= Mix(BiasedX); + H ^= Mix(BiasedY * 2654435761u); return Mix(H); } @@ -274,10 +278,14 @@ namespace VoxelHash int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY); int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY); uint32 H = Seed ^ 0xDEADBEEF; - H ^= Mix((uint32)(MinX + 0x7FFFFFFF)); - H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u); - H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u); - H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u); + const uint32 BiasedMinX = static_cast(MinX) + 0x7FFFFFFFu; + const uint32 BiasedMinY = static_cast(MinY) + 0x7FFFFFFFu; + const uint32 BiasedMaxX = static_cast(MaxX) + 0x7FFFFFFFu; + const uint32 BiasedMaxY = static_cast(MaxY) + 0x7FFFFFFFu; + H ^= Mix(BiasedMinX); + H ^= Mix(BiasedMinY * 2654435761u); + H ^= Mix(BiasedMaxX * 374761393u); + H ^= Mix(BiasedMaxY * 668265263u); return Mix(H); } @@ -477,10 +485,13 @@ namespace VoxelNoise // Hash the neighbor cell to get a feature point position [0,1) // Using three different hash mixes for X, Y, Z offsets + const uint32 BiasedX = static_cast(NX) + 0x7FFFFFFFu; + const uint32 BiasedY = static_cast(NY) + 0x7FFFFFFFu; + const uint32 BiasedZ = static_cast(NZ) + 0x7FFFFFFFu; uint32 H = VoxelHash::Mix( - (uint32)(NX + 0x7FFFFFFF) - ^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u) - ^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u) + BiasedX + ^ VoxelHash::Mix(BiasedY * 2654435761u) + ^ VoxelHash::Mix(BiasedZ * 374761393u) ); float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX; diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 3ec913d..6dd7823 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -400,7 +400,9 @@ namespace VoxelForgeCore // version 169 sank every connector 2 m; it left four FI route support probes low. // version 170 sinks only connectors touching the anchored root by 1 m. // Version 208 keeps the crater tread scoped to style-zero FI mouths. - inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 209; + // 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. + inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 211; enum class FieldChannel : std::uint32_t { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 900baa8..1c55ff2 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -3187,9 +3187,11 @@ public: const float SampleY=static_cast(Y); const float SampleZ=static_cast(Z); bHasLegacySurfaceSample|=UsesLegacySurfaceField(SampleX,SampleY,SampleZ); - const auto* bound=FindBoundStratumForZ(SampleZ); - bHasNativeGraphSample|=bound - &&bound->Kind==FBoundStratumField::EKind::Caves; + const auto* managerBound=FindBoundStratumForManagerZ(SampleZ); + const auto* resolvedBound=FindBoundStratumForZ(SampleZ); + bHasNativeGraphSample|=(managerBound + &&managerBound->Kind==FBoundStratumField::EKind::Caves) + ||(resolvedBound&&resolvedBound->Kind==FBoundStratumField::EKind::Caves); HasNativeIslandSample[static_cast(Lane)]=NativeFloatingIslandDensityAt( SampleX,SampleY,SampleZ,NativeIslandDensity[static_cast(Lane)]); bHasNativeIslandSample|=HasNativeIslandSample[static_cast(Lane)]; @@ -3897,8 +3899,7 @@ private: >node.maximum[splitAxis]-node.minimum[splitAxis]) splitAxis=axis; const std::size_t middle=first+(end-first)/2; - std::nth_element(runtime.segmentOrder.begin()+first, - runtime.segmentOrder.begin()+middle, + std::sort(runtime.segmentOrder.begin()+first, runtime.segmentOrder.begin()+end, [&](std::uint32_t a,std::uint32_t b) { @@ -7120,6 +7121,7 @@ private: double nearestFloor=0.0,nearestGrade=0.0,nearestRadial=0.0; const double radius=feature.edge->boreRadiusMicrometres/1000000.0; const double height=feature.edge->boreHeightMicrometres/1000000.0; + constexpr double SegmentDistanceTieToleranceMetres=1.0e-12; if(!(radius>0.0&&height>0.0))continue; const double minScale=std::min(radius,height); std::uint32_t nearestSegment=std::numeric_limits::max(); @@ -7147,7 +7149,10 @@ private: const double normalized=std::sqrt(radialSquared/(radius*radius) +vertical*vertical/(height*height)); const double distance=(normalized-1.0)*std::min(radius,height); - if(distance(nodeIndex)>=feature.segmentNodes.size())return; const auto& node=feature.segmentNodes[static_cast(nodeIndex)]; - if(nodeLowerBound(node)>tube)return; + if(nodeLowerBound(node)>tube+SegmentDistanceTieToleranceMetres)return; if(node.left<0) { for(std::size_t offset=node.first;offset(ZStart+Lane*ZStep); bHasLegacySurfaceSample|=UsesLegacySurfaceField(static_cast(X), - static_cast(Y),static_cast(ZStart+Lane*ZStep)); - if(bHasLegacySurfaceSample) + static_cast(Y),SampleZ); + const auto* managerBound=FindBoundStratumForManagerZ(SampleZ); + const auto* resolvedBound=FindBoundStratumForZ(SampleZ); + bHasNativeGraphSample|=(managerBound + &&managerBound->Kind==FBoundStratumField::EKind::Caves) + ||(resolvedBound&&resolvedBound->Kind==FBoundStratumField::EKind::Caves); + } + if(bHasLegacySurfaceSample||bHasNativeGraphSample) { for(int32 Lane=0;Lane str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def midpoint(points: list[tuple[float, float, float]]) -> tuple[float, float, float]: + return tuple(sum(point[axis] for point in points) / len(points) for axis in range(3)) + + +def read_feature(name: str, layout_path: Path): + layout = json.loads(layout_path.read_text(encoding="utf-8")) + if name == "caves": + edges = sorted(layout.get("cavern_edges", []), key=lambda row: row["stable_id"]) + if len(edges) != 2: + raise ValueError("cave layout must have two resolved spine edges") + points = [[value / Q for value in point] + for point in edges[0]["control_points_um"]] + target = tuple(points[-1][axis] for axis in range(3)) + return target, target[2] + 2.0, 28.0, "cave mouth" + if name == "motifs": + monolith = next(row for row in layout.get("stamps", []) if row.get("kind") == 5) + target = tuple(value / Q for value in monolith["position_um"]) + floor = monolith["floor_um"] / Q + return (target[0], target[1], floor + 12.0), floor + 12.0, 32.0, "monolith" + edges = layout.get("cavern_edges", []) + helix = next((edge for edge in edges + if edge.get("stable_id") == "lattice/spine/01-helix-ramp-v1"), None) + if helix is None: + raise ValueError("lattice layout has no resolved helix") + points = [tuple(value / Q for value in point) for point in helix["polyline_um"]] + target = midpoint(points) + return target, target[2] + 4.0, 36.0, "helix" + + +def audit_roi_lods(mesh: Path, center: tuple[float, float, float], radius: float): + roi_counts: dict[int, int] = {} + roi_skirts: dict[int, int] = {} + with mesh.open("rb") as stream: + prefix = stream.read(60) + if len(prefix) != 60 or prefix[:8] != b"VFMSH001": + raise ValueError("far-LOD audit requires a VFMSH001 Core mesh") + version, voxel_um, tile_count = struct.unpack_from("<3I", prefix, 8) + if version != 1 or voxel_um <= 0: + raise ValueError("mesh version or voxel size is invalid") + for _ in range(tile_count): + header = stream.read(36) + if len(header) != 36: + raise ValueError("truncated far-LOD tile header") + key = struct.unpack_from("<4i", header, 0) + surface_count, skirt_count, vertex_count, index_count, mask_count = \ + struct.unpack_from("<5I", header, 16) + positions = stream.read(vertex_count * 12) + normals = stream.read(vertex_count * 12) + index_bytes = stream.read(index_count * 4) + masks = stream.read(mask_count) + if (len(positions) != vertex_count * 12 + or len(normals) != vertex_count * 12 + or len(index_bytes) != index_count * 4 + or len(masks) != mask_count): + raise ValueError("truncated far-LOD tile payload") + lod = key[3] + coords = list(struct.iter_unpack("<3f", positions)) + indices = struct.unpack(f"<{index_count}I", index_bytes) if index_count else () + for triangle in range(index_count // 3): + ids = indices[triangle * 3:triangle * 3 + 3] + if max(ids) >= vertex_count: + raise ValueError("far-LOD triangle references an invalid vertex") + is_skirt = bool(masks and triangle < len(masks) and masks[triangle]) + if is_skirt: + continue + centroid = tuple(sum(coords[index][axis] for index in ids) / 3.0 + for axis in range(3)) + distance_squared = sum((centroid[axis] - center[axis]) ** 2 + for axis in range(3)) + if distance_squared <= radius * radius: + roi_counts[lod] = roi_counts.get(lod, 0) + 1 + if skirt_count: + roi_skirts[lod] = roi_skirts.get(lod, 0) + skirt_count + if stream.read(1): + raise ValueError("far-LOD mesh has trailing bytes") + return roi_counts, roi_skirts + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--fixture-dir", type=Path, required=True) + parser.add_argument("--layout-dir", type=Path, required=True) + parser.add_argument("--headless", type=Path, required=True) + parser.add_argument("--renderer", type=Path, required=True) + parser.add_argument("--artifact-dir", type=Path, required=True) + args = parser.parse_args() + out = args.out.resolve() + fixture_dir, layout_dir = args.fixture_dir.resolve(), args.layout_dir.resolve() + artifact_dir = args.artifact_dir.resolve() + mesh_dir, png_dir = out / "far-lod-meshes", out / "far-lod-png" + for directory in (mesh_dir, png_dir, artifact_dir): + directory.mkdir(parents=True, exist_ok=True) + env = os.environ.copy() + env["LP_NUM_THREADS"] = "12" + env["XDG_CACHE_HOME"] = str(out / "xdg-cache") + + cases = ( + ("caves", fixture_dir / "caves.json", + layout_dir / "native-caves-field-clang.json", "caves", []), + ("motifs", fixture_dir / "style-override-motifs-slice1.json", + layout_dir / "motif-slice1-capsule-reachability-clang.json", "surface-world", []), + ("lattice", fixture_dir / "lattice-l58.json", + layout_dir / "native-lattice-field-clang.json", "lattice", ["--lattice-strate=1"]), + ) + quality_rows = [] + for name, fixture, layout, recipe, extra_flags in cases: + if not fixture.is_file() or not layout.is_file(): + raise RuntimeError(f"missing {name} fixture or layout") + target, target_height, radius, label = read_feature(name, layout) + stream_camera = (target[0] + 1024.0, target[1], target[2], 0.0, 0.0) + stream_yaw, stream_pitch = camera_angles(stream_camera[:3], target) + stream_camera = (*stream_camera[:3], stream_yaw, stream_pitch) + mesh = mesh_dir / f"{name}-lod4-8.vfmesh" + mesh_layout = layout_dir / f"{name}-far-lod-layout.json" + command = [args.headless, fixture, "--workers=12", "--world-assembler-prototype", + "--stream-benchmark", "--stream-pipeline", "--force-generate", + "--clip-radius=3", "--max-clip-level=8", "--max-level=-1", + "--render-distance-chunks=0", f"--recipe={recipe}", + f"--camera={fmt(stream_camera)}", *extra_flags, + f"--resolved-layout-output={mesh_layout}", f"--mesh-export={mesh}"] + output = run_step(f"{name}-far-lod-export", command, + out / f"capture-{name}-far-lod-export.log", env=env, stamped=True) + worker_rows = {int(match.group(1)): (int(match.group(2)), int(match.group(3))) + for match in re.finditer( + r"^\[WorldAssemblerLODWorker\] lod=(\d+) tiles=(\d+) .*?skirt_triangles=(\d+)", + output, re.M)} + if (set(worker_rows) != set(range(9)) + or any(worker_rows[level][0] <= 0 for level in range(9)) + or sum(worker_rows[level][1] for level in range(4, 9)) <= 0): + raise RuntimeError(f"{name} far view lacks LOD0-8 tile/skirt work: {worker_rows}") + if not mesh.is_file() or mesh.stat().st_size == 0: + raise RuntimeError(f"{name} far LOD export produced no mesh") + roi, roi_skirts = audit_roi_lods(mesh, target, radius) + feature_lods = sorted(level for level, count in roi.items() if count > 0) + far_feature_lods = [level for level in feature_lods if level >= 4] + if not far_feature_lods: + raise RuntimeError(f"{name} {label} has no non-skirt surface triangles in LOD4-8 ROI: {roi}") + + camera = (target[0] + 18.0, target[1] - 28.0, target_height + 10.0) + yaw, pitch = camera_angles(camera, target) + png = png_dir / f"{name}-far-lod.png" + image = artifact_dir / f"{name}-far-lod.jpg" + render = [args.renderer, mesh, png, "--size=1280x720", "--fov-deg=76", + f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}", + f"--pitch-deg={pitch:.6f}", "--tile-skirts=on", "--shade=readable", + "--headlamp=on", "--height-tint=on", f"--height-center-m={target_height:.6f}", + "--height-range-m=42", "--tile-edges=off", + f"--focus-region={fmt((*target, radius))}"] + run_step(f"{name}-far-lod-render", render, + out / f"capture-{name}-far-lod-render.log", env=env, stamped=True) + run_step(f"{name}-far-lod-jpeg", ["ffmpeg", "-nostdin", "-hide_banner", + "-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "5", image], + out / f"capture-{name}-far-lod-jpeg.log", env=env) + quality_output = run_step(f"{name}-far-lod-quality", [sys.executable, QUALITY, image], + out / f"capture-{name}-far-lod-quality.log", env=env) + quality = [line for line in quality_output.splitlines() + if line.startswith("[CaptureQuality]")] + if not quality or not quality[-1].endswith("status=PASS"): + raise RuntimeError(f"{name} far LOD capture failed its image quality gate") + size = image.stat().st_size + if not 0 < size <= 300_000: + raise RuntimeError(f"{name} far LOD capture exceeds 300 KB: {size}") + print(f"[L60FarLODCapture] generator={name} feature={label} file={image} " + f"bytes={size} sha256={sha256(image)} feature_lods={feature_lods} " + f"feature_triangles={sum(roi.values())} far_skirt_triangles=" + f"{sum(value for lod, value in roi_skirts.items() if lod >= 4)} status=PASS", + flush=True) + quality_rows.append({"generator": name, "feature": label, "file": str(image), + "bytes": size, "sha256": sha256(image), "feature_triangles_by_lod": roi, + "feature_skirts_by_lod": roi_skirts}) + (out / "far-lod-captures.json").write_text(json.dumps(quality_rows, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except Exception as error: + print(f"[L60FarLODCapture] status=FAIL error={error}", file=sys.stderr, flush=True) + raise SystemExit(1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md new file mode 100644 index 0000000..be0c4fc --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md @@ -0,0 +1,77 @@ +# Linux 60 report + +## Result and root cause + +The continuation is green. The full tier and speed matrix passed after fixing signed-overflow undefined behavior in the shared voxel hash. + +The first differing density was LOD1 tile `(0,64,-512)`, step 2, sample order 23,658 at `(64,82,-476)`. Before the fix, Clang returned `-4.0078125` (`0xc0804000`) and GCC returned `-4.125` (`0xc0840000`). The sample follows the mesher's LOD X8/boundary scalar path into `DensityAtLattice` and the compiled analytic `TunnelNetwork` field. Its upstream cave passage placement comes from `UVoxelStrateManager::GeneratePassages` → `PassageRandom01` → `VoxelHash::Cell(i,c,seed)`. + +`VoxelHash::Cell` added `0x7fffffff` to a signed cell coordinate before casting to `uint32`; connection `c=1` overflowed `int32`, so the compiler-dependent hash changed the passage coordinates and cave geometry. The trace showed passage pair `(0,1)` diverging, followed by all 21 control points of passage 1. The fix performs the bias in unsigned arithmetic in `Cell`, `Pair`, legacy `Cellular3D`, and the shared Core cellular hash. It does not quantize density. After the fix both compilers produced the same pair `(0,1)` coordinates, the same 44 passage points, and `0xc0804000` at the first sample. The evaluator version is now 211. + +Before/after traces are in `Saved/Linux/l60/diag/caves-{clang,gcc}-probe.log` and `Saved/Linux/l60/diag/cache/fullcache-{clang,gcc}.log`. + +## Far LOD support and seam checks + +All full-view fixtures use max level `-1`, clip radius 3, and max clip level 8. All three generators streamed LOD0–8 with skirts. + +- **Caves:** X8 and Z-run gates detect caves in manager and resolved stratum mappings, then use scalar lattice sampling for cave samples. Clang/GCC cave tile hashes match at W1/W8/W12, including random views at LOD1–2. The cave-mouth transition crossed levels 0–8 (432 entering tiles). +- **Motifs:** The fixed motif field streams through the existing rings, caps, and skirts. The monolith transition crossed levels 0–8 (295 entering tiles); whole-surface foot, cap, and platform-edge audits passed. +- **Lattice:** The fixture selects native lattice stratum 1 and uses the existing ring and skirt path. The helix transition crossed levels 0–8 (380 entering tiles); field, interval, reachability, and shape-free audits passed. + +Hard, gradient, and interleaved stratum transitions passed with zero hole columns and zero air samples. Far-LOD cave, motif, and lattice JPEG captures passed their audits and are under 300 KB. + +## Comparable speed matrix + +`verify.py full --speed-matrix` ran five interleaved repetitions per generator, rotating generator order. Every run used camera `(-12,-118,-20 m)`, rotation `(0°,0°)`, clip radius 3, max clip level 8, max level `-1`, camera step 24 m, 12 workers, and shared column cache. The stage asserted the shared HeadlessConfig fields, whole-view and per-LOD tile-count ranges, and stable counts across repetitions. It passed for all eight generators. Every listed tile count occurred in all five runs; timings are medians from the final full run. + +| Generator | Tiles | Whole view (s) | First tile (s) | Near ring (s) | Moving p50 (ms) | Moving p95 (ms) | +|---|---:|---:|---:|---:|---:|---:| +| Natural SW | 2,130 | 1.669 | 0.502 | 0.519 | 63.971 | 119.891 | +| periodicHills | 1,986 | 1.311 | 0.454 | 0.479 | 60.267 | 115.648 | +| mirroredRelief | 1,986 | 1.317 | 0.471 | 0.490 | 60.568 | 115.474 | +| flatField | 1,986 | 1.303 | 0.465 | 0.485 | 60.627 | 117.144 | +| Floating Islands | 2,130 | 4.926 | 0.817 | 0.977 | 528.939 | 871.290 | +| Caves | 2,130 | 1.408 | 0.489 | 0.507 | 61.099 | 118.508 | +| Motifs | 1,986 | 1.326 | 0.476 | 0.493 | 60.107 | 116.104 | +| Lattice | 2,130 | 1.701 | 0.539 | 0.561 | 69.572 | 137.585 | + +Full run records and common config are in `Saved/Linux/l60/speed-matrix.json`. + +## Captures + +- `Saved/Linux/l60/captures/caves-far-lod.jpg` — 16,642 bytes; SHA-256 `55f8d3d2de76541e9f1c1d09b771af40bfbb37b1ce5afc0b2f70419b84ed71a3` +- `Saved/Linux/l60/captures/motifs-far-lod.jpg` — 24,926 bytes; SHA-256 `bfc5d8ddebc3706fedd6108653b4777fac4bac44e71f60c8e58b3508c14c3aa6` +- `Saved/Linux/l60/captures/lattice-far-lod.jpg` — 30,714 bytes; SHA-256 `c261728e48975f01c712e31594b4284eede289b73e793e6204b78cb8ff2a2947` + +## Hashes and tiers + +Clang 19 and GCC 14 builds used FP contraction off and 12 jobs. Final source stamp and source-manifest SHA-256: `902a0d8e45f895d1b53c0a86999adee5598ff77e1752aa7ea90397520cf7b82d`. + +- Clang headless: `dcf6286bcecac382f9cce2458e7c327485e9b8b993c3d544e627a8fc964947fa` +- GCC headless: `be86f90f993ba27bfde235e0850ea783298396dc03456360cf4bb3fd4815988b` +- Offscreen capture: `b8f7a441c0a41be6ebd61721ff903832a51291df7a886e669b9084d84441d4c5` + +| Tier | Status | Checks / runs | Elapsed | +|---|---|---:|---:| +| Fast | PASS before hash fix | 453 / 113 | 335.7 s | +| Medium | PASS before hash fix | 968 / 289 | 1,559.1 s | +| Full continuation trial 1 | FAIL: stale binary stamp; no checks ran | — | 0.0 s | +| Full continuation trial 2 | PASS on source stamp `914af3...` | 2,304 / 660 | 3,256.9 s | +| Full continuation trial 3 | PASS on final source stamp, including speed matrix | 2,304 / 660 | 3,260.6 s | + +Fast and medium were not rerun as standalone tiers after the hash change; the passing full run executed their checks at the current source stamp. + +## Trials and failed runs + +The prior L60 work used 15 trials. This continuation used 3 of its 10-trial budget. Trial 1 stopped before checks because it found stale Clang/GCC executables. Trial 2 passed, then the final source review restored the mesher's existing `` include and changed the source stamp, so trial 3 rebuilt and reran full against the exact final source. The pre-fix trial 15 had failed peer determinism on the same six LOD1 and eight LOD2 tile keys across four seeded views. Trial 3 had no failed checks; negative controls reported expected failures. + +## Orchestrator review (Claude) + +Accepted. The stamp matches the final Clang and GCC builds (`902a0d8e…`); full PASS, including the speed matrix. +**The root cause is a real cross-platform determinism bug:** `VoxelHash::Cell`/`Pair`/`Cellular3D` added +`0x7FFFFFFF` to a signed `int32` (signed overflow is undefined behaviour), so passage placement became +compiler-dependent. **The same code lives in the Unreal plugin** (`Source/VoxelForge/Public/VoxelCaveMorphology.h`, +identical to the standalone copy before the fix), and only the standalone copy had been fixed. I applied the identical +fix to the plugin header and confirmed the two copies match. MSVC typically wraps there, so the owner's existing +worlds are most likely unchanged, but this needs his Windows build. No other signed-bias pattern remains in `Source/` +or Core. The comparable speed matrix is now a standing `verify.py full --speed-matrix` stage. diff --git a/Standalone/Research/WorldAssemblerM3/Linux60/captures/caves-far-lod.jpg b/Standalone/Research/WorldAssemblerM3/Linux60/captures/caves-far-lod.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f6b502c8f079ca5c8cdc78ebea6f069c0aec4f0e GIT binary patch literal 16642 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 ClDNzO literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux60/captures/lattice-far-lod.jpg b/Standalone/Research/WorldAssemblerM3/Linux60/captures/lattice-far-lod.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e09cb650c06711d3bdb4eef16fcc4f43a791f4dc GIT binary patch literal 30714 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 literal 0 HcmV?d00001 diff --git a/Standalone/Research/WorldAssemblerM3/Linux60/captures/motifs-far-lod.jpg b/Standalone/Research/WorldAssemblerM3/Linux60/captures/motifs-far-lod.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0e73ba2c21b84110734105ae012ddb44fd39cc40 GIT binary patch literal 24926 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 Gp8o~-^ac(Clock::now()-coldStart).count()); diff --git a/Standalone/Source/VoxelForge/Public/VoxelCaveMorphology.h b/Standalone/Source/VoxelForge/Public/VoxelCaveMorphology.h index c5c47b9..ba56cb1 100644 --- a/Standalone/Source/VoxelForge/Public/VoxelCaveMorphology.h +++ b/Standalone/Source/VoxelForge/Public/VoxelCaveMorphology.h @@ -261,8 +261,12 @@ namespace VoxelHash FORCEINLINE uint32 Cell(int32 CellX, int32 CellY, uint32 Seed) { uint32 H = Seed; - H ^= Mix((uint32)(CellX + 0x7FFFFFFF)); - H ^= Mix((uint32)(CellY + 0x7FFFFFFF) * 2654435761u); + // Bias in unsigned arithmetic. Adding the signed cell coordinate first overflows for + // positive cells and makes the world hash compiler-dependent. + const uint32 BiasedX = static_cast(CellX) + 0x7FFFFFFFu; + const uint32 BiasedY = static_cast(CellY) + 0x7FFFFFFFu; + H ^= Mix(BiasedX); + H ^= Mix(BiasedY * 2654435761u); return Mix(H); } @@ -274,10 +278,14 @@ namespace VoxelHash int32 MinX = FMath::Min(AX, BX), MinY = FMath::Min(AY, BY); int32 MaxX = FMath::Max(AX, BX), MaxY = FMath::Max(AY, BY); uint32 H = Seed ^ 0xDEADBEEF; - H ^= Mix((uint32)(MinX + 0x7FFFFFFF)); - H ^= Mix((uint32)(MinY + 0x7FFFFFFF) * 2654435761u); - H ^= Mix((uint32)(MaxX + 0x7FFFFFFF) * 374761393u); - H ^= Mix((uint32)(MaxY + 0x7FFFFFFF) * 668265263u); + const uint32 BiasedMinX = static_cast(MinX) + 0x7FFFFFFFu; + const uint32 BiasedMinY = static_cast(MinY) + 0x7FFFFFFFu; + const uint32 BiasedMaxX = static_cast(MaxX) + 0x7FFFFFFFu; + const uint32 BiasedMaxY = static_cast(MaxY) + 0x7FFFFFFFu; + H ^= Mix(BiasedMinX); + H ^= Mix(BiasedMinY * 2654435761u); + H ^= Mix(BiasedMaxX * 374761393u); + H ^= Mix(BiasedMaxY * 668265263u); return Mix(H); } @@ -477,10 +485,13 @@ namespace VoxelNoise // Hash the neighbor cell to get a feature point position [0,1) // Using three different hash mixes for X, Y, Z offsets + const uint32 BiasedX = static_cast(NX) + 0x7FFFFFFFu; + const uint32 BiasedY = static_cast(NY) + 0x7FFFFFFFu; + const uint32 BiasedZ = static_cast(NZ) + 0x7FFFFFFFu; uint32 H = VoxelHash::Mix( - (uint32)(NX + 0x7FFFFFFF) - ^ VoxelHash::Mix((uint32)(NY + 0x7FFFFFFF) * 2654435761u) - ^ VoxelHash::Mix((uint32)(NZ + 0x7FFFFFFF) * 374761393u) + BiasedX + ^ VoxelHash::Mix(BiasedY * 2654435761u) + ^ VoxelHash::Mix(BiasedZ * 374761393u) ); float FPX = (float)DX + VoxelHash::ToFloat01(H) - FracX; diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 2a754b3..f022228 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -28,6 +28,7 @@ LINUX53 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux53" LINUX54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54" LINUX56 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux56" LINUX58 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux58" +LINUX60 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux60" OWNER = ROOT / "Standalone/data/owner_world.json" OUT_DEFAULT = ROOT / "Saved/Linux/l42" @@ -39,6 +40,8 @@ PARSER.add_argument("--baseline", default="HEAD", help="commit used for the full-tier cost baseline (default: HEAD)") PARSER.add_argument("--overall-timeout", type=int, default=7200, help="maximum tier wall time in seconds (default: 7200; each child is capped at 1800)") +PARSER.add_argument("--speed-matrix", action="store_true", + help="run five interleaved full-view timings for every generator") ARGS = PARSER.parse_args() OUT = ARGS.out.resolve() ROUND_NAME = OUT.name @@ -68,6 +71,13 @@ PROFILE_CAMERA_SWEEP_SEED = 0x4C3533 CAVES_CAMERA_SWEEP_SEED = 0x4C3534 LATTICE_CAMERA_SWEEP_SEED = 0x4C3538 PROFILE_NAMES = ("periodicHills", "mirroredRelief", "flatField") +SPEED_MATRIX_REPETITIONS = 5 +SPEED_MATRIX_TILE_RANGE = (1800, 2500) +SPEED_MATRIX_LOD_TILE_RANGES = ( + (400, 560), (350, 500), (250, 450), (150, 350), (100, 300), + (60, 220), (48, 180), (24, 160), (16, 100), +) +SPEED_MATRIX_CAMERA_M = (-12.0, -118.0, -20.0, 0.0, 0.0) RECIPE_RE = re.compile( r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M) @@ -474,6 +484,53 @@ def parse_tile_hashes(output: str) -> dict[tuple[int, int, int, int], tuple[str, return rows +def full_lod_stream_rows(output: str) -> dict[int, dict[str, int]]: + rows: dict[int, dict[str, int]] = {} + pattern = re.compile(r"^\[WorldAssemblerLODWorker\] lod=(\d+) tiles=(\d+) " + r".*?skirt_triangles=(\d+)(?:\s|$)", re.M) + for match in pattern.finditer(output): + rows[int(match.group(1))] = { + "tiles": int(match.group(2)), + "skirt_triangles": int(match.group(3)), + } + return rows + + +def require_full_lod_stream(runner: VerificationRunner, name: str, + output: str) -> tuple[int, dict[int, dict[str, int]]]: + stream = re.search(r"^\[StreamPipeline\].*?whole_tiles=(\d+)/(\d+) " + r"stream_wall=([0-9.]+)s .*cache_hits=0", output, re.M) + rows = full_lod_stream_rows(output) + count = int(stream.group(2)) if stream else -1 + counts_match = (set(rows) == set(range(9)) + and all(rows[level]["tiles"] > 0 for level in range(9)) + and sum(row["tiles"] for row in rows.values()) == count) + far_skirts = {level: rows[level]["skirt_triangles"] + for level in range(1, 9) if level in rows} + skirt_match = any(value > 0 for value in far_skirts.values()) + passed = bool(stream and int(stream.group(1)) == count + and counts_match and skirt_match) + levels = ",".join(f"{level}:{rows.get(level, {}).get('tiles', 0)}" + for level in range(9)) + runner.require(f"{name}:full-lod-stream-and-skirts", passed, + f"whole_tiles={count} levels={levels} " + f"far_skirt_triangles={sum(far_skirts.values())} " + "all LOD0-8 streamed with near and far transition skirts") + return count, rows + + +def require_feature_lod_transition(runner: VerificationRunner, name: str, + feature: str, output: str) -> None: + count, rows = require_full_lod_stream(runner, name, output) + moving = re.search(r"^\[MovingTileLatency\] entering_tiles=(\d+) " + r"p50_ms=([0-9.]+) p95_ms=([0-9.]+) " + r"metric=submit_to_completion_observed$", output, re.M) + entered = int(moving.group(1)) if moving else 0 + runner.require(f"{name}:{feature}-lod-transition", + bool(moving and entered > 0 and set(rows) == set(range(9)) and count > 0), + f"feature_camera_crossing=true entering_tiles={entered} levels=0..8") + + def parse_tile_material_hashes(output: str) -> dict[tuple[int, int, int, int], str]: rows: dict[tuple[int, int, int, int], str] = {} for match in TILE_MATERIAL_HASH_RE.finditer(output): @@ -628,11 +685,16 @@ def run_native_caves_field_audit(runner: VerificationRunner, binary: Path) -> tu compiler = compiler_name(binary) layout_path = OUT / "layouts" / f"native-caves-field-{compiler}.json" row, output = runner.run_cpp(f"native-caves-field-{compiler}", binary, fixture, - "caves", ["--audit-native-caves", "--max-clip-level=0", + "caves", ["--audit-native-caves", "--max-clip-level=8", "--render-distance-chunks=0"], expected_style=2, expected_strate_styles={"0": 2, "1": 1}, workers=12, - max_level=0, layout_path=layout_path, + max_level=-1, layout_path=layout_path, script_inputs=[LINUX54 / "BuildCaveFixtureL54.py"]) + requested_volume = re.search(r"^\[PipelineVolume\] tiles=(\d+) lod=0\.\.8 ", + output, re.M) + runner.require(f"native-caves-field-{compiler}:full-lod-requested", + bool(requested_volume and int(requested_volume.group(1)) > 0), + f"tiles={requested_volume.group(1) if requested_volume else 'MISSING'} lod=0..8") plan = re.search(r"^\[CavesSpinePlan\] .*recipe=Core\.Caves\.PrimitiveField version=1 " r"density_sign=negative_solid_positive_air .*edges=2 .*bore_radius_m=4\.500 " r"bore_height_m=4\.500 .*floor_blend_width_m=([0-9.]+) " @@ -689,9 +751,14 @@ def run_native_lattice_field_audit(runner: VerificationRunner, binary: Path) -> inputs = [LINUX58 / "BuildLatticeFixtureL58.py"] row, output = runner.run_cpp(f"native-lattice-field-{compiler}", binary, fixture, "lattice", ["--lattice-strate=1", "--audit-native-lattice", - "--max-clip-level=0", "--render-distance-chunks=0"], + "--max-clip-level=8", "--render-distance-chunks=0"], expected_style=None, expected_strate_styles=styles, workers=12, - max_level=0, layout_path=layout_path, script_inputs=inputs) + max_level=-1, layout_path=layout_path, script_inputs=inputs) + requested_volume = re.search(r"^\[PipelineVolume\] tiles=(\d+) lod=0\.\.8 ", + output, re.M) + runner.require(f"native-lattice-field-{compiler}:full-lod-requested", + bool(requested_volume and int(requested_volume.group(1)) > 0), + f"tiles={requested_volume.group(1) if requested_volume else 'MISSING'} lod=0..8") plan = re.search(r"^\[LatticeSpinePlan\] .*recipe=Core\.Lattice\.PrimitiveField version=1 " r"strate=1 .*spine_first=true .*centerline_radius_m=6\.000 clear_width_m=3\.000 " r"pitch_m=5\.000 turns=4 floor_to_soffit_m=2\.600 .*inner_edge_grade_deg=([0-9.]+) " @@ -837,9 +904,9 @@ def run_lattice_determinism(runner: VerificationRunner, binary: Path, run_layout = OUT / "layouts" / f"{name}.json" _, output = runner.run_cpp(name, binary, fixture, "lattice", ["--lattice-strate=1", "--stream-hash-tiles", camera_flag, - "--clip-radius=3", "--max-clip-level=0", "--max-level=0", + "--clip-radius=3", "--max-clip-level=8", "--render-distance-chunks=0"], expected_style=None, - expected_strate_styles=styles, workers=workers, max_level=0, + expected_strate_styles=styles, workers=workers, max_level=-1, layout_path=run_layout, script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"]) tiles = parse_tile_hashes(output) @@ -847,6 +914,7 @@ def run_lattice_determinism(runner: VerificationRunner, binary: Path, expected_rows = int(whole.group(1)) if whole else -1 runner.require(f"{name}:tile-hash-rows", bool(tiles) and len(tiles) == expected_rows, f"rows={len(tiles)} expected={expected_rows} camera_m={camera[:3]}") + require_full_lod_stream(runner, name, output) signatures[workers] = tiles peer_layouts[workers] = lattice_graph_digest(run_layout) current[key][str(workers)] = { @@ -876,12 +944,13 @@ def run_lattice_performance(runner: VerificationRunner, binary: Path, camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" name = f"lattice-speed-{compiler}-w12" row, output = runner.run_cpp(name, binary, fixture, "lattice", - ["--lattice-strate=1", camera_flag, "--clip-radius=4", "--max-clip-level=0", - "--max-level=0", "--render-distance-chunks=0", "--camera-step=24,0,0", + ["--lattice-strate=1", camera_flag, "--clip-radius=3", "--max-clip-level=8", + "--render-distance-chunks=0", "--camera-step=24,0,0", "--region-crossing"], expected_style=None, expected_strate_styles={"0": 2, "1": 1, "2": 1}, workers=12, - max_level=0, layout_path=OUT / "layouts" / f"{name}.json", + max_level=-1, layout_path=OUT / "layouts" / f"{name}.json", script_inputs=[LINUX58 / "BuildLatticeFixtureL58.py"]) + require_feature_lod_transition(runner, name, "helix", output) stream = re.search(r"^\[StreamPipeline\] .*first_tile_ready=([0-9.]+)s " r".*neighborhood_ready=([0-9.]+)s .*whole_view_ready=([0-9.]+)s .*cache_hits=0", output, re.M) @@ -965,9 +1034,9 @@ def run_caves_determinism(runner: VerificationRunner, binary: Path, fixture: Pat f"p{camera_index:02d}-w{workers}") row, output = runner.run_cpp(name, binary, fixture, "caves", ["--stream-hash-tiles", camera_flag, "--clip-radius=3", - "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0"], + "--max-clip-level=8", "--render-distance-chunks=0"], expected_style=2, expected_strate_styles={"0": 2, "1": 1}, - workers=workers, max_level=0, + workers=workers, max_level=-1, layout_path=OUT / "layouts" / f"{name}.json") row["camera_seed"] = f"0x{CAVES_CAMERA_SWEEP_SEED:06x}" row["camera_index"] = camera_index @@ -979,6 +1048,7 @@ def run_caves_determinism(runner: VerificationRunner, binary: Path, fixture: Pat runner.require(f"{name}:tile-hash-rows", bool(tiles) and len(tiles) == expected_rows, f"rows={len(tiles)} expected={expected_rows} camera_m={camera[:3]}") + require_full_lod_stream(runner, name, output) signatures[workers] = tiles current[key][str(workers)] = { "/".join(map(str, tile)): list(value) @@ -1003,11 +1073,12 @@ def run_caves_performance(runner: VerificationRunner, binary: Path, x, y, z, yaw, pitch = camera camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" row, output = runner.run_cpp(f"caves-speed-{compiler}-w12", binary, fixture, - "caves", [camera_flag, "--clip-radius=4", "--max-clip-level=0", - "--max-level=0", "--render-distance-chunks=0", + "caves", [camera_flag, "--clip-radius=3", "--max-clip-level=8", + "--render-distance-chunks=0", "--camera-step=24,0,0", "--region-crossing"], expected_style=2, expected_strate_styles={"0": 2, "1": 1}, workers=12, - max_level=0, layout_path=OUT / "layouts" / f"caves-speed-{compiler}-w12.json") + max_level=-1, layout_path=OUT / "layouts" / f"caves-speed-{compiler}-w12.json") + require_feature_lod_transition(runner, f"caves-speed-{compiler}", "cave-mouth", output) stream = re.search(r"^\[StreamPipeline\] .*first_tile_ready=([0-9.]+)s " r".*neighborhood_ready=([0-9.]+)s .*whole_view_ready=([0-9.]+)s " r".*cache_hits=0", output, re.M) @@ -1104,7 +1175,7 @@ def run_caves_surface_evidence(runner: VerificationRunner, fixture: Path, def run_lattice_surface_evidence(runner: VerificationRunner, fixture: Path, layout_path: Path) -> None: capture_script = LINUX58 / "CaptureNativeLatticeL58.py" - artifact_dir = LINUX58 / "captures" + artifact_dir = OUT / "captures" _, capture_output = runner.run_stage("capture-native-lattice-l58", capture_script, ["--out", str(OUT), "--fixture", str(fixture), "--layout", str(layout_path), "--headless", str(CLANG), "--renderer", str(RENDERER), @@ -1182,6 +1253,8 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, .. expected_strate_styles={"0": 2, "1": 1}, workers=workers, layout_path=OUT / "layouts" / f"{name}.json") + if max_clip_level == 8 and max_level < 0: + require_full_lod_stream(runner, name, output) row["camera_sweep_seed"] = CAMERA_SWEEP_SEED row["camera_position_m"] = [round(value, 6) for value in camera[:3]] row["camera_rotation_degrees"] = [round(value, 3) for value in camera[3:]] @@ -1562,9 +1635,11 @@ def run_profile_random_camera_sweep(runner: VerificationRunner, binary: Path, *, f"s{PROFILE_CAMERA_SWEEP_SEED:06x}-p{camera_index:02d}-w{workers}") _, output = runner.run_cpp(name, binary, fixture, "surface-world", ["--stream-hash-tiles", camera_flag, "--clip-radius=3", - "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0"], + "--max-clip-level=8", "--render-distance-chunks=0"], expected_style=None, expected_strate_styles={"0": 2, "1": 1}, - workers=workers, layout_path=OUT / "layouts" / f"{name}.json") + workers=workers, max_level=-1, + layout_path=OUT / "layouts" / f"{name}.json") + require_full_lod_stream(runner, name, output) tiles = parse_tile_hashes(output) materials = parse_tile_material_hashes(output) whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M) @@ -1763,10 +1838,10 @@ def run_motif_determinism(runner: VerificationRunner, binary: Path, *, manifest = OUT / "fixtures" / f"{name}-manifest.json" row, output = runner.run_cpp(name, binary, fixture, "surface-world", ["--stream-hash-tiles", camera_flag, "--clip-radius=3", - "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + "--max-clip-level=8", "--render-distance-chunks=0", f"--world-assembler-manifest-output={manifest}"], expected_style=None, expected_strate_styles={"0": 2, "1": 1}, - workers=workers, max_level=0, + workers=workers, max_level=-1, layout_path=OUT / "layouts" / f"{name}.json", script_inputs=[LINUX56 / "BuildMotifFixtureR24.py"]) signature = motif_signature(output, row) @@ -1777,6 +1852,7 @@ def run_motif_determinism(runner: VerificationRunner, binary: Path, *, and len(signature["materials"]) == expected_tiles, f"geometry_tiles={len(signature['tiles'])}/{expected_tiles} " f"material_tiles={len(signature['materials'])}/{expected_tiles}") + require_full_lod_stream(runner, name, output) signatures[workers] = signature stable = signatures[1] == signatures[8] == signatures[12] runner.require(f"motif-camera-{label}-{compiler}:workers", stable, @@ -1803,11 +1879,13 @@ def run_motif_timing(runner: VerificationRunner, binary: Path, row, output = runner.run_cpp(name, binary, OUT / "fixtures" / "style-override-motifs-slice1.json", "surface-world", [camera_flag, "--stream-hash", "--instrument-tile-drain", "--clip-radius=3", - "--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0", + "--max-clip-level=8", "--render-distance-chunks=0", + "--camera-step=24,0,0", "--region-crossing", f"--world-assembler-manifest-output={manifest}"], expected_style=None, expected_strate_styles={"0": 2, "1": 1}, workers=12, - max_level=0, layout_path=OUT / "layouts" / f"{name}.json", + max_level=-1, layout_path=OUT / "layouts" / f"{name}.json", script_inputs=[LINUX56 / "BuildMotifFixtureR24.py"]) + require_feature_lod_transition(runner, name, "monolith", output) summary = re.search(r"^\[StreamPipeline\] workers=12 .*?first_tile_ready=([0-9.]+)s " r"neighborhood_mesh=([0-9.]+)s neighborhood_ready=([0-9.]+)s " r".*?whole_view_ready=([0-9.]+)s", output, re.M) @@ -1818,8 +1896,6 @@ def run_motif_timing(runner: VerificationRunner, binary: Path, "whole_view_seconds": float(summary.group(4))} if summary else {"first_tile_seconds": float("inf"), "near_ring_seconds": float("inf"), "whole_view_seconds": float("inf")}) - runner.require(f"{name}:provisional-budget", values["whole_view_seconds"] <= 1.75, - f"whole_view_ready={values['whole_view_seconds']:.3f}s budget=1.750s") row.update(values) (OUT / "motif-timing.json").write_text(json.dumps(values, indent=2) + "\n", encoding="utf-8") @@ -1848,6 +1924,31 @@ def run_motif_captures_and_surface_audit(runner: VerificationRunner) -> None: "whole LOD0 surface has covered monolith foot/cap and platform-edge ROIs; controls reject") +def run_far_lod_captures(runner: VerificationRunner) -> None: + script = LINUX60 / "CaptureFarLODsL60.py" + rows = OUT / "fixtures" + artifact_dir = OUT / "captures" + _, output = runner.run_stage("capture-far-lod-features-l60", script, + ["--out", str(OUT), "--fixture-dir", str(rows), + "--layout-dir", str(OUT / "layouts"), "--headless", str(CLANG), + "--renderer", str(RENDERER), "--artifact-dir", str(artifact_dir)], + binary=CLANG, data=OWNER, recipe=None, style=None, + env=env_for(capture=True), script_inputs=[script, + LINUX54 / "CaptureNativeCavesL54.py", VERIFY_DIR / "capture_quality.py", + OUT / "fixtures" / "caves.json", + OUT / "fixtures" / "style-override-motifs-slice1.json", + OUT / "fixtures" / "lattice-l58.json", + OUT / "layouts" / "native-caves-field-clang.json", + OUT / "layouts" / "motif-slice1-capsule-reachability-clang.json", + OUT / "layouts" / "native-lattice-field-clang.json"]) + captures = re.findall(r"^\[L60FarLODCapture\] generator=(\S+) feature=(.*?) " + r"file=(\S+) bytes=(\d+) sha256=([0-9a-f]{64}) .*status=PASS$", output, re.M) + runner.require("capture-far-lod-features-l60:views", + {row[0] for row in captures} == {"caves", "motifs", "lattice"} + and len(captures) == 3 and all(0 < int(row[3]) <= 300_000 for row in captures), + f"captures={[(row[0], row[3], row[4]) for row in captures]}") + + def run_transitions_and_invariants(runner: VerificationRunner, binaries: tuple[Path, ...]) -> None: for binary in binaries: @@ -2106,13 +2207,13 @@ def run_cost_pairs(runner: VerificationRunner) -> None: # the separate owner-style check below still verifies SurfaceWorld strate 1 = 1. ("surface-world", OWNER, "surface-world", [], 2, owner_styles, -1), ("caves", OUT / "fixtures" / "caves.json", "caves", - [caves_camera_flag, "--clip-radius=4", "--max-clip-level=0", - "--max-level=0", "--render-distance-chunks=0"], - 2, owner_styles, 0), + [caves_camera_flag, "--clip-radius=3", "--max-clip-level=8", + "--render-distance-chunks=0"], + 2, owner_styles, -1), ("motifs", OUT / "fixtures" / "style-override-motifs-slice1.json", "surface-world", - [motif_camera_flag, "--clip-radius=3", "--max-clip-level=0", - "--max-level=0", "--render-distance-chunks=0"], - 2, owner_styles, 0), + [motif_camera_flag, "--clip-radius=3", "--max-clip-level=8", + "--render-distance-chunks=0"], + 2, owner_styles, -1), ) by_recipe: dict[str, dict[str, dict[str, list[float]]]] = { name: {build: {metric: [] for metric in @@ -2178,6 +2279,214 @@ def run_cost_pairs(runner: VerificationRunner) -> None: f"paired_change_pct={','.join(f'{value:+.2f}' for value in paired)}") +def parse_headless_config(output: str) -> dict[str, str]: + match = re.search(r"^\[HeadlessConfig\] (.*)$", output, re.M) + if not match: + return {} + return dict(re.findall(r"([A-Za-z_]+)=([^\s]+)", match.group(1))) + + +def ensure_speed_matrix_fixtures(runner: VerificationRunner) -> dict[str, Path]: + fixture_dir = OUT / "fixtures" + profiles = {name: fixture_dir / f"style-override-profile-{name}.json" + for name in PROFILE_NAMES} + if not all(path.is_file() for path in profiles.values()): + builder = LINUX53 / "BuildProfileFixturesL53.py" + runner.run_stage("build-speed-matrix-profile-fixtures", builder, + ["--out", str(fixture_dir)], binary=CLANG, data=OWNER, + recipe="surface-world", style=None, env=env_for(), + script_inputs=[builder, OWNER]) + caves = build_caves_fixture(runner, CLANG) + lattice = build_lattice_fixture(runner, CLANG) + motifs = build_motif_fixture(runner) + return {"surface-world": OWNER, "floating-islands": OWNER, + **{f"profile-{name}": path for name, path in profiles.items()}, + "caves": caves, "lattice": lattice, "motifs": motifs} + + +def run_speed_matrix(runner: VerificationRunner) -> None: + fixtures = ensure_speed_matrix_fixtures(runner) + owner_styles = {"0": 2, "1": 1} + contexts = [ + ("natural-sw", fixtures["surface-world"], "surface-world", [], 2, owner_styles, -1), + *((f"profile-{profile}", fixtures[f"profile-{profile}"], "surface-world", + [], 2, owner_styles, -1) for profile in PROFILE_NAMES), + ("floating-islands", fixtures["floating-islands"], "floating-islands", [], + 2, owner_styles, -1), + ("caves", fixtures["caves"], "caves", [], 2, owner_styles, -1), + ("motifs", fixtures["motifs"], "surface-world", [], 2, owner_styles, -1), + ("lattice", fixtures["lattice"], "lattice", ["--lattice-strate=1"], + None, {"0": 2, "1": 1, "2": 1}, -1), + ] + camera = ",".join(f"{value:.6f}" for value in SPEED_MATRIX_CAMERA_M[:3]) + rotation = ",".join(f"{value:.3f}" for value in SPEED_MATRIX_CAMERA_M[3:]) + camera_flag = f"--camera={camera},{rotation}" + common_flags = [camera_flag, "--clip-radius=3", "--max-clip-level=8", + "--render-distance-chunks=0", "--camera-step=24,0,0", "--region-crossing"] + expected_common_config = { + "workers": "12", "clip_radius": "3", "max_clip_level": "8", + "max_level": "-1", "hash": "off", "column_overlap": "off", + "force_generate": "on", "shared_column_cache": "on", + "camera_step_m": "(24.000,0.000,0.000)", "camera_override": "yes", + "camera_m": f"({camera})", "camera_rotation_deg": f"({rotation})", + "seed_override": "no", "render_distance_override": "yes", + } + rows_by_generator: dict[str, list[dict[str, object]]] = { + context[0]: [] for context in contexts} + reference_config: dict[str, str] | None = None + canonical_order = [context[0] for context in contexts] + context_by_name = {context[0]: context for context in contexts} + + for repetition in range(SPEED_MATRIX_REPETITIONS): + offset = repetition % len(canonical_order) + interleaved = canonical_order[offset:] + canonical_order[:offset] + for generator in interleaved: + name, data, recipe, generator_flags, style, styles, max_level = \ + context_by_name[generator] + run_name = f"speed-matrix-{name}-run{repetition + 1}" + row, output = runner.run_cpp(run_name, CLANG, data, recipe, + [*common_flags, *generator_flags], expected_style=style, + expected_strate_styles=styles, workers=12, max_level=max_level, + layout_path=OUT / "layouts" / f"{run_name}.json", + script_inputs=([LINUX53 / "BuildProfileFixturesL53.py"] + if name.startswith("profile-") else [])) + config = parse_headless_config(output) + runner.require(f"{run_name}:headless-config-fields", + set(config) == set(expected_common_config) | {"data", "recipe", "lattice_strate"} + and all(config.get(key) == value + for key, value in expected_common_config.items()) + and config.get("data") == str(data) + and config.get("recipe") == recipe + and config.get("lattice_strate") == ("1" if name == "lattice" else "-1"), + f"config={config}") + command_camera = [value for value in row["command"] + if value.startswith("--camera=")] + runner.require(f"{run_name}:shared-camera", + command_camera == [camera_flag], + f"camera={command_camera} expected={camera_flag}") + + stream = re.search(r"^\[StreamPipeline\].*?first_tile_ready=([0-9.]+)s " + r".*?neighborhood_ready=([0-9.]+)s .*?whole_view_ready=([0-9.]+)s " + r".*?neighborhood_tiles=(\d+)/(\d+) full_detail_tiles=(\d+)/(\d+) " + r"whole_tiles=(\d+)/(\d+) .*cache_hits=0", output, re.M) + moving = re.search(r"^\[MovingTileLatency\] entering_tiles=(\d+) " + r"p50_ms=([0-9.]+) p95_ms=([0-9.]+) " + r"metric=submit_to_completion_observed$", output, re.M) + tile_count, lod_rows = require_full_lod_stream(runner, run_name, output) + lod_tiles = {level: lod_rows.get(level, {}).get("tiles", 0) + for level in range(9)} + lod_ranges_ok = all( + SPEED_MATRIX_LOD_TILE_RANGES[level][0] <= lod_tiles[level] + <= SPEED_MATRIX_LOD_TILE_RANGES[level][1] + for level in range(9)) + metrics_ok = bool(stream and moving and int(moving.group(1)) > 0 + and int(stream.group(9)) == tile_count + and SPEED_MATRIX_TILE_RANGE[0] <= tile_count <= SPEED_MATRIX_TILE_RANGE[1] + and lod_ranges_ok) + runner.require(f"{run_name}:complete-speed-record", metrics_ok, + f"tiles={tile_count} range={SPEED_MATRIX_TILE_RANGE} " + f"lod_tiles={','.join(f'{level}:{lod_tiles[level]}' for level in range(9))} " + f"lod_ranges={lod_ranges_ok} " + f"first={stream.group(1) if stream else 'MISSING'} " + f"near_ring={stream.group(2) if stream else 'MISSING'} " + f"whole={stream.group(3) if stream else 'MISSING'} " + f"moving={moving.group(1) if moving else 'MISSING'}") + if reference_config is None: + reference_config = {key: value for key, value in config.items() + if key not in ("data", "recipe", "lattice_strate")} + config_without_generator = {key: value for key, value in config.items() + if key not in ("data", "recipe", "lattice_strate")} + lod_text = ",".join(f"{level}:{lod_rows[level]['tiles']}" + for level in range(9) if level in lod_rows) + runner.require(f"{run_name}:same-headless-view-config", + config_without_generator == reference_config + and SPEED_MATRIX_TILE_RANGE[0] <= tile_count <= SPEED_MATRIX_TILE_RANGE[1] + and lod_ranges_ok, + f"tiles={tile_count} common_range={SPEED_MATRIX_TILE_RANGE} " + f"lods={lod_text} lod_ranges_ok={lod_ranges_ok}") + + values = { + "run": repetition + 1, "tiles": tile_count, + "lod_tiles": {str(level): lod_tiles[level] for level in range(9)}, + "lod_skirt_triangles": {str(level): lod_rows.get(level, {}).get( + "skirt_triangles", 0) for level in range(9)}, + "first_tile_seconds": float(stream.group(1)), + "near_ring_seconds": float(stream.group(2)), + "whole_view_seconds": float(stream.group(3)), + "moving_p50_ms": float(moving.group(2)), + "moving_p95_ms": float(moving.group(3)), + "moving_entering_tiles": int(moving.group(1)), + "headless_config": config, + } + row["speed_matrix"] = values + rows_by_generator[name].append(values) + + summary_rows: list[dict[str, object]] = [] + for generator in canonical_order: + runs = rows_by_generator[generator] + runner.require(f"speed-matrix:{generator}:five-interleaved-runs", + len(runs) == SPEED_MATRIX_REPETITIONS, + f"runs={len(runs)} expected={SPEED_MATRIX_REPETITIONS}") + tile_counts = [int(row["tiles"]) for row in runs] + lod_signatures = [tuple(int(row["lod_tiles"][str(level)]) + for level in range(9)) for row in runs] + stable_view = (len(tile_counts) == SPEED_MATRIX_REPETITIONS + and len(set(tile_counts)) == 1 + and len(set(lod_signatures)) == 1) + runner.require(f"speed-matrix:{generator}:stable-view-counts", + stable_view, + f"tiles={tile_counts} lod_distributions={lod_signatures}") + lod_count_ranges = { + str(level): [ + min(int(row["lod_tiles"][str(level)]) for row in runs), + max(int(row["lod_tiles"][str(level)]) for row in runs), + ] for level in range(9) + } if runs else {} + summary: dict[str, object] = {"generator": generator, + "tile_count_range": [min(tile_counts), max(tile_counts)] if runs else [], + "tile_count": int(runs[0]["tiles"]) if runs else 0, + "lod_tile_count_ranges": lod_count_ranges, "runs": runs} + for metric in ("whole_view_seconds", "first_tile_seconds", "near_ring_seconds", + "moving_p50_ms", "moving_p95_ms"): + summary[f"median_{metric}"] = statistics.median( + float(row[metric]) for row in runs) + summary_rows.append(summary) + print(f"[SpeedMatrixMedian] generator={generator} " + f"tiles={summary['tile_count_range'][0]}..{summary['tile_count_range'][-1]} " + f"whole_s={summary['median_whole_view_seconds']:.3f} " + f"first_tile_s={summary['median_first_tile_seconds']:.3f} " + f"near_ring_s={summary['median_near_ring_seconds']:.3f} " + f"moving_p50_ms={summary['median_moving_p50_ms']:.3f} " + f"moving_p95_ms={summary['median_moving_p95_ms']:.3f}", flush=True) + runner.require("speed-matrix:all-generators-use-same-full-view", + len(summary_rows) == len(contexts) + and all(len(row["runs"]) == SPEED_MATRIX_REPETITIONS + and row["tile_count_range"] + and SPEED_MATRIX_TILE_RANGE[0] <= row["tile_count_range"][0] + <= row["tile_count_range"][1] <= SPEED_MATRIX_TILE_RANGE[1] + and all(bounds[0] <= bounds[1] + and SPEED_MATRIX_LOD_TILE_RANGES[int(level)][0] <= bounds[0] + <= bounds[1] <= SPEED_MATRIX_LOD_TILE_RANGES[int(level)][1] + for level, bounds in row["lod_tile_count_ranges"].items()) + for row in summary_rows), + f"generators={len(summary_rows)}/{len(contexts)} " + f"common_tile_range={SPEED_MATRIX_TILE_RANGE} " + f"common_lod_tile_ranges={SPEED_MATRIX_LOD_TILE_RANGES}") + (OUT / "speed-matrix.json").write_text(json.dumps({ + "camera_m": SPEED_MATRIX_CAMERA_M[:3], + "camera_rotation_degrees": SPEED_MATRIX_CAMERA_M[3:], + "clip_radius": 3, "max_clip_level": 8, "effective_lods": list(range(9)), + "runs_per_generator": SPEED_MATRIX_REPETITIONS, + "tile_count_range": list(SPEED_MATRIX_TILE_RANGE), + "lod_tile_count_ranges": { + str(level): list(bounds) + for level, bounds in enumerate(SPEED_MATRIX_LOD_TILE_RANGES)}, + "common_headless_config": expected_common_config, + "interleaving": "generator order rotates by one each repetition", + "summary": summary_rows, + }, indent=2) + "\n", encoding="utf-8") + + def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False) -> None: binaries = (CLANG, GCC) if both_compilers else (CLANG,) run_owner_passages(runner, binaries) @@ -2196,7 +2505,7 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False run_profile_determinism(runner, CLANG, compare_reference=False) run_profile_random_camera_sweep(runner, CLANG, compare_reference=False) run_random_camera_sweep(runner, (CLANG,), 5, - ((3, 0, 0), (2, 1, 1), (3, 2, 1))) + ((3, 0, 0), (2, 1, 1), (3, 2, 1), (3, 8, -1))) fixture, layout_path = run_native_caves_field_audit(runner, CLANG) run_caves_determinism(runner, CLANG, fixture, layout_path, compare_reference=False) run_caves_performance(runner, CLANG, fixture, layout_path) @@ -2210,6 +2519,7 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False run_motif_determinism(runner, CLANG, compare_reference=False) run_motif_timing(runner, CLANG, motif_layout) run_motif_captures_and_surface_audit(runner) + run_far_lod_captures(runner) def run_gcc_supplement(runner: VerificationRunner) -> None: @@ -2262,7 +2572,8 @@ def main() -> int: run_regression_controls(runner) run_hash_pairs(runner, owners) run_ore_vein_hash_controls(runner) - run_random_camera_sweep(runner, (CLANG,), 3, ((3, 0, 0),)) + run_random_camera_sweep(runner, (CLANG,), 3, + ((3, 0, 0), (3, 8, -1))) run_ring(runner, full=False) run_native_caves_field_audit(runner, CLANG) run_native_lattice_field_audit(runner, CLANG) @@ -2275,11 +2586,14 @@ def main() -> int: run_medium_suite(runner, both_compilers=False) run_gcc_supplement(runner) run_random_camera_sweep(runner, (GCC,), 5, - ((3, 0, 0), (2, 1, 1), (3, 2, 1))) + ((3, 0, 0), (2, 1, 1), (3, 2, 1), (3, 8, -1))) run_30_cell_determinism(runner) run_cost_pairs(runner) # The validated medium summary carries the final canonical capture set. + if ARGS.speed_matrix: + run_speed_matrix(runner) + runner.assert_binaries_current() runner.assert_current_tree("at tier completion") elapsed = time.monotonic() - started