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 0000000..f6b502c Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux60/captures/caves-far-lod.jpg differ 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 0000000..e09cb65 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux60/captures/lattice-far-lod.jpg differ 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 0000000..0e73ba2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux60/captures/motifs-far-lod.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Research29/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research29/REPORT.md new file mode 100644 index 0000000..f6aaef1 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research29/REPORT.md @@ -0,0 +1,114 @@ +# VoxelForge R29 — revised self-check + +## Mechanism in the L59 field + +I read the L59 implementation on `wip/l59-open-shaft`. It constructs the shaft and ramp as cavity fields, then applies a support cap. The source gives a specific reason to investigate the support and the touching cavity surfaces; it does not prove which one produced each visible shard. + +The core void is a radial cylinder clipped at the room floor: + +```cpp +const double radial = hypot(point[0]-center[0], point[1]-center[1])-radius; +const double bottom = floor-point[2]; +featureVoid = std::max(radial,bottom); +``` + +`wip/l59-open-shaft:Standalone/Core/src/ShapeWorldInternal.h:7340–7346` + +The open ramp’s radial tube is centered at 6 m with a 1.5 m bore radius: + +```cpp +nearestRadial = abs(shaftRadial-centerlineRadius); +tube = nearestRadial-radius; +``` + +Its vertical corridor runs from each turn’s floor to `floorZ+height`; adjacent turns are combined with a minimum: + +```cpp +return max((floorZ-point[2])*openHelixVerticalScale, + (point[2]-(floorZ+height))*openHelixVerticalScale); +openHelixVerticalSdf = min(corridorSdf(turnA),corridorSdf(turnB)); +``` + +`wip/l59-open-shaft:Standalone/Core/src/ShapeWorldInternal.h:7380–7385, 7410–7417` + +The tube and corridor are intersected with a smooth max. The core and ramp voids are then unioned with a smooth min, followed by one subtraction from the host field: + +```cpp +featureVoid = SmoothMaxMetres(tube,openHelixVerticalSdf,width); +caveVoid = SmoothMinMetres(caveVoid,featureVoid,...); +composed = SmoothMaxMetres(hostDistance,-caveVoid,0.0); +``` + +`wip/l59-open-shaft:Standalone/Core/src/ShapeWorldInternal.h:7522–7524, 7533–7541, 7547–7550` + +The geometry report gives a 4.5 m core radius, 6 m ramp centerline, 3 m ramp width, and 1 m support depth. Thus, at the inner edge, the core’s radial zero is `r−4.5`; the ramp tube’s inner zero is `6−r−1.5 = 4.5−r`. Their zero surfaces coincide at 4.5 m with opposite radial gradients. The code smooths the ramp intersection and cavity union, but the tracked L59 report does not record the actual smooth or union widths. I can’t establish that the live edge used a hard intersection; that depends on those values. + +There is a more concrete issue in the support field. Its vertical terms are: + +```cpp +const double vertical = max((floorZ-point[2])*scale, + (point[2]-(floorZ-supportDepth))*scale); +return max(nearestRadial-radius,vertical); +... +composed = min(composed,latticeFloorSupportMinimum); +``` + +`wip/l59-open-shaft:Standalone/Core/src/ShapeWorldInternal.h:7502–7515, 7551–7555` + +At mid-depth below the floor, both vertical terms are positive. With this field’s sign convention—host rock is `-32 m`, and cavity interior is positive—the cap therefore cannot force the nominal support volume solid. It may leave rock solid where the host already does so, but it cannot restore solid under an overlapping cavity. This is my strongest code-specific suspect, though the source alone cannot prove it caused the photographed shards. + +The nominal 1 m depth is four LOD0 cell intervals, so simple under-sampling is a weaker explanation at the captured LOD. The free edge has no explicit bullnose radius. A hard or too-narrow crease remains possible if the configured widths are zero or small. Relief noise is unlikely on this path: the open-helix branch returns before the legacy edge relief code (`ShapeWorldInternal.h:7187–7191, 7249–7255`). + +The geometry and reported route measurements are in `wip/l59-open-shaft:Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md:5–7`. + +## Construction and dimensions + +Use the 25 cm LOD0 spacing as the baseline. The practical minimum `R=2h` and preferred `R=3h` are sampling targets, not guarantees for every field gradient or grid phase. For VoxelForge’s cubic smooth-min/max, `W≈2√2·R` applies to a local cross-section of perpendicular unit-gradient planes; the ramp’s coincident radial surfaces need slice validation. See `Research16/REPORT.md:20–31`. + +The L59 owner uses 25 cm Core voxels, 32 cells at full resolution, two full-resolution LOD levels, and 16 cells per coarse tile. Applying the mesher’s `extent=32<(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