From 036bd530fcd79ffd88af27e619eed4f4e5c4c9f7 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 23 Oct 2026 23:00:21 +0200 Subject: [PATCH] fix(core): worker-count-dependent tiles near passage support columns (L44); random-camera determinism sweep L27 added passage support columns, but World::isTileShapeFree only bounded the route and bridge, so tiles touched by a column could be approved for early native-only generation; depending on scheduling they used that incomplete field or the compiled one (six LOD0 tiles differed between W1 and W12 at an SW camera near the arrival mouth). The shape-free proof now includes a conservative envelope for every possible column. The compiled world is unchanged (the old W1 variant), so no version bump. verify tiers gain a seeded (SplitMix64 0x4c3434) random-camera W1/W8/W12 sweep across both recipes and several clip settings; it fails before the fix and passes after. fast/medium/full PASS; cost within noise. Research R11: suspect review. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- Standalone/Core/src/VoxelForgeCore.cpp | 102 +++++++++++--- .../WorldAssemblerM3/Linux44/REPORT.md | 18 +++ .../WorldAssemblerM3/Research11/REPORT.md | 27 ++++ Standalone/Source/StreamBenchmarkHeadless.cpp | 5 +- Standalone/Source/StreamBenchmarkPipeline.h | 28 +++- Standalone/Tools/verify/verify.py | 126 ++++++++++++++++++ 6 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux44/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Research11/REPORT.md diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index a7d1e31..acc15c9 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -1207,27 +1207,97 @@ namespace VoxelForgeCore const double tileMaximum[3]={static_cast(maximum.X), static_cast(maximum.Y),static_cast(maximum.Z)}; for(const auto& passage:layout.passages) - for(std::size_t segment=1;segment(std::max( - a.radiusSubvoxels,b.radiusSubvoxels))/Q; - const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); - const double low[3]={ - std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, - std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, - std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius - -bridgeFootDepthVoxels}; - const double high[3]={ - std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, - std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, - std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; + for(std::size_t segment=1;segment(std::max( + a.radiusSubvoxels,b.radiusSubvoxels))/Q; + const double horizontalExtent=std::max(radius,bridgeHalfWidthVoxels); + const double low[3]={ + std::min(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q-horizontalExtent, + std::min(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q-horizontalExtent, + std::min(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q-radius + -bridgeFootDepthVoxels}; + const double high[3]={ + std::max(a.positionSubvoxels[0],b.positionSubvoxels[0])/Q+horizontalExtent, + std::max(a.positionSubvoxels[1],b.positionSubvoxels[1])/Q+horizontalExtent, + std::max(a.positionSubvoxels[2],b.positionSubvoxels[2])/Q+radius}; + bool overlap=true; + for(int axis=0;axis<3;++axis) + overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis]; + if(overlap)return true; + } + + // L27 can add a long support column around an unsupported passage span. Its + // radius can exceed the route and bridge bounds above by the column wall and + // flare allowance. The precompile shape-free proof runs before that crossing + // plan exists, so conservatively bound every possible column from the resolved + // route itself. The center is either the passage anchor or an average of route + // samples; in the latter case it lies inside the route's XY bounds. + if(passage.controlPoints.size()>=2) + { + double routeMinimum[3]={std::numeric_limits::infinity(), + std::numeric_limits::infinity(), + std::numeric_limits::infinity()}; + double routeMaximum[3]={-std::numeric_limits::infinity(), + -std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}; + double maximumBoreRadius=0.0; + for(const auto& point:passage.controlPoints) + { + for(int axis=0;axis<3;++axis) + { + const double value=static_cast(point.positionSubvoxels[axis])/Q; + routeMinimum[axis]=std::min(routeMinimum[axis],value); + routeMaximum[axis]=std::max(routeMaximum[axis],value); + } + maximumBoreRadius=std::max(maximumBoreRadius, + static_cast(point.radiusSubvoxels)/Q); + } + + double centerMinimumX=routeMinimum[0],centerMaximumX=routeMaximum[0]; + double centerMinimumY=routeMinimum[1],centerMaximumY=routeMaximum[1]; + double maximumRouteOffset=std::hypot( + routeMaximum[0]-routeMinimum[0],routeMaximum[1]-routeMinimum[1]); + const auto upper=std::find_if(layout.strates.begin(),layout.strates.end(), + [&passage](const auto& stratum) + {return stratum.strateIndex==passage.upperStrateIndex;}); + if(upper!=layout.strates.end() + &&upper->passageCapabilities.usePassageAnchorForColumns) + { + const auto anchor=std::find_if(upper->anchors.begin(),upper->anchors.end(), + [](const auto& value) + {return value.kind=="passage_anchor"&&value.values.size()>=2;}); + if(anchor!=upper->anchors.end()) + { + const double centerX=static_cast(anchor->values[0])/Q; + const double centerY=static_cast(anchor->values[1])/Q; + centerMinimumX=centerMaximumX=centerX; + centerMinimumY=centerMaximumY=centerY; + maximumRouteOffset=0.0; + for(const auto& point:passage.controlPoints) + maximumRouteOffset=std::max(maximumRouteOffset,std::hypot( + static_cast(point.positionSubvoxels[0])/Q-centerX, + static_cast(point.positionSubvoxels[1])/Q-centerY)); + } + } + // The generated maximum column radius is the route offset, bore radius, + // and 13.25 m of wall/irregularity/flare allowance. + const double columnRadius=maximumRouteOffset+maximumBoreRadius + +13.25*voxelsPerMetre; + const double columnMinimum[3]={centerMinimumX-columnRadius, + centerMinimumY-columnRadius,routeMinimum[2]-6.0*voxelsPerMetre}; + const double columnMaximum[3]={centerMaximumX+columnRadius, + centerMaximumY+columnRadius,routeMaximum[2]+5.0*voxelsPerMetre}; bool overlap=true; for(int axis=0;axis<3;++axis) - overlap&=high[axis]>=tileMinimum[axis]&&low[axis]<=tileMaximum[axis]; + overlap=overlap&&columnMaximum[axis]>=tileMinimum[axis] + &&columnMinimum[axis]<=tileMaximum[axis]; if(overlap)return true; } + } return false; } diff --git a/Standalone/Research/WorldAssemblerM3/Linux44/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux44/REPORT.md new file mode 100644 index 0000000..c7ffa16 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux44/REPORT.md @@ -0,0 +1,18 @@ +# Linux L44 — worker-count determinism + +- **Bisect:** L26 (`37a2230`) is stable; L27 (`07b3fe2`) is the first bad commit. L28 retains the same mismatch. +- **Difference:** at LOD0, six tiles differ: x `-3..-1`, y `2..3`, z `-64`. Density, mesh, payload, and topology hashes all differ. The exact-camera W1 density hash was `0x68f495ba62aace05`; W12 was `0x18d6f6cb3498deff`. +- **Cause:** L27 added large passage support columns, but `World::isTileShapeFree` only bounded the route and bridge. It incorrectly approved tiles touched by the columns for early native-only generation. Depending on scheduling, a tile used that incomplete field or the compiled field. The direct exact-query hashes remained identical. +- **Fix:** extend the shared shape-free proof with a conservative envelope for every possible support column, including its anchor/route center, bore, wall, flare, and vertical span. Affected tiles wait for the compiled shape; no worker ordering or serialization changed. +- **Output/version:** the compiled world is canonical. At the reported camera it hashes to density `0x68f495ba62aace05`, mesh `0x539885cd589dc985`, payload `0x88a411b8697278ec`, topology `0xd24b13cef6bfdeb4` for all worker counts. This is the old W1 variant; the old W12 variant was the incomplete native-only result. The compiled field did not change, so no version bump is needed. +- **Sweep:** fixed SplitMix64 seed `0x4c3434`; camera 0 jitters around the failing camera, later cameras sample distant SurfaceWorld and FloatingIslands strata. Fast: 3 cameras, both recipes, settings `(clip-radius,max-clip-level,max-level)=(3,0,0)`. Medium/full: 5 cameras and settings `(3,0,0)`, `(2,1,1)`, `(3,2,1)`, W1/W8/W12. Full adds GCC; medium supplies Clang. Before the fix, fast failed on its first camera with the six LOD0 tiles above. After: fast PASS (172.9s), medium PASS (545.9s), full PASS (1635.9s); every per-tile density/mesh/payload/topology comparison matched. +- **Cost:** five full-tier pairs against HEAD (`422dd776`): median whole-view time changed `+0.65%` for FloatingIslands and `+0.40%` for SurfaceWorld, within run noise. +- **Failed runs:** the pre-fix fast failure was expected. The first medium configuration `(4,2,2)` exceeded the 160-million-node shape-build guard at 2,479 tiles; `(3,2,1)` covers max-clip-level 2 at 960 tiles and passed. The first Clang build caught a scope error in the proof loop, corrected before the passing build. + +## Orchestrator review (Claude) + +Accepted. The current tree stamp equals the L44 binaries (`bf2c835c…`). The original repro now gives density +`0x68f495ba62aace05` at W1, W4 and W12. The compiled field is unchanged, so this is not a world-version change. It +also explains a player-visible symptom: depending on thread timing, tiles touched by L27's support columns could be +generated **without** the column. The new seeded random-camera sweep in every tier is the lasting value here; it +fails on the bug and passes on the fix. diff --git a/Standalone/Research/WorldAssemblerM3/Research11/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research11/REPORT.md new file mode 100644 index 0000000..7d72eaa --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research11/REPORT.md @@ -0,0 +1,27 @@ +**Suspects — I couldn’t append to `Saved/Linux/claude/shared-notes.md` because the sandbox is read-only:** L4’s per-worker heightfield cache; L7’s shared column cache; L28’s passage feature lists and their interaction with tile-scoped state. + +| Rank / commit | File and function | Mechanism and fit | Cheapest confirmation | +|---|---|---|---| +| 1 — L4 `7770074` | [WorldAssemblerInternal.h:CachedColumn](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/WorldAssemblerInternal.h:1378); [ThreadLocalStorage.h:FLazy](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/ThreadLocalStorage.h) | LOD0 uses a per-thread direct-mapped heightfield cache. Worker count partitions tile samples across different cache histories. Its key checks recipe, plan and XY, but not the active feature-query scope used by `EvaluateColumn`; a context omission or incomplete candidate list could make a cached value depend on which worker filled it. **Best fit for stable worker-count partitioning.** Cache collisions alone should only evict entries, because hits check the full key. | Temporarily bypass the TLS heightfield cache and compare W1/W12. No standalone switch disables this cache. | +| 2 — L7 `4b1108b` | [SharedColumnCache.h:getOrCompute](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/SharedColumnCache.h:96); [WorldAssemblerInternal.h:CachedColumn](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/WorldAssemblerInternal.h:1604) | The shared cache publishes the first computed value for a recipe/plan/XY key; worker scheduling determines which query fills it first. The same tile-scoped feature-query context is absent from that key. The lock and atomic version checks look designed to prevent torn fills, so this fits only if the factory can vary by context. | Add `--column-cache=thread-local` to both runs. This disables L7, but **leaves the L4 TLS cache active**. | +| 3 — L28 `f23ab7b` | [ShapeWorldInternal.h:FScopedPassageTileQueries](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:156); `BuildPassageFeatureTileList` | Per-tile passage candidates come from BVHs and are exposed through a thread-local scope. This is the strongest spatial fit for a camera near a passage; a missed candidate could omit a carve or mouth feature. The lists are built from immutable shape data and look deterministic per tile, so they need an interaction with scoped state or caching to explain worker dependence by themselves. | Force the passage query to fail open and use the full feature list, then compare W1/W12. There is no CLI flag that disables this culling. | +| L11 `b1cfaf5`, L13 `976d012` | [StreamBenchmarkPipeline.h:ShapeFreeDesired](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:525); `VoxelForgeCore.cpp:generateTile` | Shape-free tiles can use the native-only route while shape compilation runs. Worker scheduling could change which tiles take that route. Fit is lower here: the target is near a passage, and `isTileShapeFree` is intended to exclude passage-overlapping tiles. | `--audit-tile-shape-readiness` compares compiled and native-only output for eligible near-ring tiles. `--diagnostic-slow-shape-compile-ms=250` is a timing sensitivity check. | +| L9 `7650abb`, L12 `32b8670`, L13 `976d012` | `ShapeWorldInternal.h` proof classification; `VoxelForgeCore.cpp:compileTileRegion` | L9’s added proof optimization targets LOD1. The command is LOD0 only; its general LOD0 proof path is tile-local and does not itself explain worker order. L12 pages are built for region/audit routes; this static command has no region crossing. L13 fixed a page/base field-history issue, but that route should also be absent. | `--audit-uniform-block-proofs` audits the Step=2 path, so it does not cover this LOD0 run. Page audits are not a useful first check here. | +| S14 `ec2afe9` | [VoxelMarchingCubesMesher.cpp:GradientMemo](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1487) | The memo is thread-local, but its validity is reset per tile and its indices refer to that tile’s density grid. It should not carry values between tiles or workers. | Unreal has `voxel.CoreMCGradientMemoization=0`; the standalone CLI has no equivalent. Low fit. | +| L18–L19, L21–L37; L40–L42 | `ResolvedLayoutInternal.h`, `ShapeWorldInternal.h`, field-binding code | Strata ceilings, resolved layout, transition bands, passage geometry, and L40 field bindings are resolved or compiled state, not worker-local state. L40 is also ruled out as the sole cause because L37 reproduces. No strong worker-order mechanism found. | No direct off switch. | +| Cross-cutting | `HashTileRows`; unordered containers; floating-point work | Tile rows are sorted before hashing. The unordered containers I found are used for lookup or membership, not output order; no density reduction whose order changes with worker count stood out. | No obvious flag; low fit. | + +### Top three + +1. **L4 per-worker cache:** This is the clearest partitioning mechanism, and the command’s LOD0 path selects direct mapping. The specific risk is that `EvaluateColumn` consults a tile-scoped feature list, while the cached value is keyed only by recipe, plan and XY. That list is meant to include every feature relevant at the point, so this is a hypothesis to verify, not evidence of a known bad hit. Bypass the TLS heightfield cache to test it. + +2. **L7 shared cache:** The shared cache also omits the tile query scope from its key, and first-fill order varies with worker scheduling. Its synchronization appears to protect cache integrity; the concern is whether two valid query contexts can produce different values for the same key. Run W1 and W12 with `--column-cache=thread-local`. If the mismatch disappears, investigate the shared fill path; if it remains, L4 is still active. + +3. **L28 passage lists:** This best matches the location, but the BVH/list path appears deterministic per tile. Force full passage-feature evaluation as a diagnostic. If that removes the worker-count difference, check the bounds and scope feeding the list. + +### Flags to try + +- **First:** `--column-cache=thread-local` on both worker counts. It disables the shared cache only. +- **Readiness check:** `--audit-tile-shape-readiness`; optionally add `--diagnostic-slow-shape-compile-ms=250` to vary overlap with shape compilation. +- **Not an immediate LOD0 check:** `--audit-uniform-block-proofs` covers the Step=2 proof path, not the reported LOD0 path. +- I found no standalone CLI flag to disable the L4 TLS cache or passage BVH culling. \ No newline at end of file diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 49f164d..71c5d2c 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -37,6 +37,7 @@ namespace bool StreamBenchmark=false; bool StreamPipeline=false; bool StreamHash=false; + bool StreamHashTiles=false; bool ProfileColumnOverlap=false; bool UseSharedColumnCache=true; bool ForceGenerate=false; @@ -239,6 +240,7 @@ namespace std::printf("Usage: %s [owner_world.json] --world-assembler-prototype --stream-benchmark --stream-pipeline [options]\n" " --workers=N --clip-radius=N --max-clip-level=N --max-level=N\n" " --camera=X,Y,Z,YAW,PITCH (position in metres) --stream-hash\n" + " --stream-hash-tiles (print per-tile hashes; implies --stream-hash)\n" " --camera-step=X,Y,Z (optional second view step in metres)\n" " --region-crossing (compile entering Core regions at the camera step)\n" " --column-cache=shared|thread-local (default: shared)\n" @@ -292,6 +294,7 @@ namespace else if(arg=="--stream-benchmark")options.StreamBenchmark=true; else if(arg=="--stream-pipeline")options.StreamPipeline=true; else if(arg=="--stream-hash")options.StreamHash=true; + else if(arg=="--stream-hash-tiles")options.StreamHash=true,options.StreamHashTiles=true; else if(arg=="--profile-column-overlap")options.ProfileColumnOverlap=true; else if(arg=="--force-generate")options.ForceGenerate=true; else if(arg=="--mesh-coverage")options.MeshCoverage=true; @@ -795,5 +798,5 @@ int main(int argc,char** argv) options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence, options.AuditRouteParity,options.AuditResolvedLayoutBoundaries, options.AuditResolvedLayoutPassages,options.AuditStrateTransitions, - &resolvedLayout); + &resolvedLayout,options.StreamHashTiles); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 023c157..ffa296d 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -423,7 +423,8 @@ namespace StandaloneStreamBenchmark bool bAuditResolvedLayoutBoundaries=false, bool bAuditResolvedLayoutPassages=false, bool bAuditStrateTransitions=false, - const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr) + const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr, + bool bPrintTileHashes=false) { using Clock=std::chrono::steady_clock; FStandaloneOwnerWorld& World=*OwnerWorld; @@ -3824,6 +3825,31 @@ namespace StandaloneStreamBenchmark &&MeshRows.size()==Desired.Tiles.size() &&PayloadRows.size()==Desired.Tiles.size() &&TopologyRows.size()==Desired.Tiles.size(); + if(bPrintTileHashes) + { + const auto makeHashIndex=[](const std::vector& rows) + { + std::unordered_map index; + index.reserve(rows.size()); + for(const FHashedTile& row:rows)index.emplace(row.Key,row.Hash); + return index; + }; + const auto MeshIndex=makeHashIndex(MeshRows); + const auto PayloadIndex=makeHashIndex(PayloadRows); + const auto TopologyIndex=makeHashIndex(TopologyRows); + const auto findHash=[](const auto& index,const FVoxelTileKey& key) + { + const auto found=index.find(key); + return found==index.end()?uint64{0}:found->second; + }; + for(const FHashedTile& row:DensityRows) + std::printf("[PipelineTileHash] lod=%d x=%d y=%d z=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n", + row.Key.Level,row.Key.Coord.X,row.Key.Coord.Y,row.Key.Coord.Z, + static_cast(row.Hash), + static_cast(findHash(MeshIndex,row.Key)), + static_cast(findHash(PayloadIndex,row.Key)), + static_cast(findHash(TopologyIndex,row.Key))); + } std::vector FullDensity,FullMesh,FullPayload,FullTopology; for(const FHashedTile& Row:DensityRows)if(Row.Key.Level float: + self.state = (self.state + 0x9e3779b97f4a7c15) & 0xffffffffffffffff + value = self.state + value = ((value ^ (value >> 30)) * 0xbf58476d1ce4e5b9) & 0xffffffffffffffff + value = ((value ^ (value >> 27)) * 0x94d049bb133111eb) & 0xffffffffffffffff + value ^= value >> 31 + return (value >> 11) / float(1 << 53) + + def between(self, low: float, high: float) -> float: + return low + (high - low) * self.unit() + + +def seeded_camera_positions(count: int) -> list[tuple[float, float, float, float, float]]: + rng = _SplitMix64(CAMERA_SWEEP_SEED) + positions: list[tuple[float, float, float, float, float]] = [] + for index in range(count): + if index == 0: + # Stay in the known failing 8 m tile neighbourhood while still deriving the + # camera from the fixed seed. The negative Y offset keeps it below the 32 m seam. + x = -8.918 + rng.between(-0.02, 0.02) + y = 31.95 - rng.between(0.005, 0.035) + z = -513.083 + rng.between(-0.02, 0.02) + else: + x = rng.between(-900.0, 900.0) + y = rng.between(-900.0, 900.0) + while math.hypot(x + 46.45, y + 472.88) < 320.0: + x = rng.between(-900.0, 900.0) + y = rng.between(-900.0, 900.0) + if index % 2: + z = -256.0 + rng.between(-12.0, 12.0) # SurfaceWorld strate 0. + else: + z = -1536.0 + rng.between(-120.0, 120.0) # FloatingIslands strate 1. + yaw = rng.between(-180.0, 180.0) + pitch = rng.between(-35.0, 35.0) + positions.append((x, y, z, yaw, pitch)) + return positions + + +def parse_tile_hashes(output: str) -> dict[tuple[int, int, int, int], tuple[str, ...]]: + rows: dict[tuple[int, int, int, int], tuple[str, ...]] = {} + for match in TILE_HASH_RE.finditer(output): + key = tuple(int(match.group(i)) for i in range(1, 5)) + rows[key] = tuple(match.group(i) for i in range(5, 9)) + return rows + + +def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ...], + camera_count: int, + configurations: tuple[tuple[int, int, int], ...]) -> None: + positions = seeded_camera_positions(camera_count) + for binary in binaries: + compiler = compiler_name(binary) + for recipe in ("surface-world", "floating-islands"): + for camera_index, camera in enumerate(positions): + x, y, z, yaw, pitch = camera + camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}" + for clip_radius, max_clip_level, max_level in configurations: + settings = (clip_radius, max_clip_level, max_level) + label = (f"random-camera-{compiler}-{recipe}-s{CAMERA_SWEEP_SEED:06x}" + f"-p{camera_index:02d}-r{clip_radius}-c{max_clip_level}-m{max_level}") + signatures: dict[int, dict[tuple[int, int, int, int], tuple[str, ...]]] = {} + for workers in (1, 8, 12): + name = f"{label}-w{workers}" + flags = ["--stream-hash-tiles", camera_flag, + f"--clip-radius={clip_radius}", + f"--max-clip-level={max_clip_level}", + f"--max-level={max_level}", "--render-distance-chunks=0"] + row, output = runner.run_cpp(name, binary, OWNER, recipe, flags, + expected_style=2, + expected_strate_styles={"0": 2, "1": 1}, + workers=workers, + layout_path=OUT / "layouts" / f"{name}.json") + 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:]] + row["sweep_settings"] = { + "clip_radius": clip_radius, + "max_clip_level": max_clip_level, + "max_level": max_level, + } + current = parse_tile_hashes(output) + signatures[workers] = current + whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M) + expected_rows = int(whole.group(1)) if whole else -1 + runner.require(f"{name}:tile-hash-rows", + bool(current) and len(current) == expected_rows, + f"rows={len(current)} expected={expected_rows} settings={settings}") + camera_text = (f"seed=0x{CAMERA_SWEEP_SEED:06x} camera_m=" + f"({x:.3f},{y:.3f},{z:.3f}) recipe={recipe} settings={settings}") + for left, right in ((1, 8), (1, 12), (8, 12)): + left_rows, right_rows = signatures[left], signatures[right] + keys = set(left_rows) | set(right_rows) + density_diff = [key for key in keys + if left_rows.get(key, (None,))[0] != right_rows.get(key, (None,))[0]] + mesh_diff = [key for key in keys + if left_rows.get(key, (None, None))[1] + != right_rows.get(key, (None, None))[1]] + payload_diff = [key for key in keys + if left_rows.get(key, (None, None, None))[2] + != right_rows.get(key, (None, None, None))[2]] + topology_diff = [key for key in keys + if left_rows.get(key, (None, None, None, None))[3] + != right_rows.get(key, (None, None, None, None))[3]] + detail = (f"{camera_text} W{left}=W{right} " + f"density_tiles={len(density_diff)} mesh_tiles={len(mesh_diff)} " + f"payload_tiles={len(payload_diff)} topology_tiles={len(topology_diff)} " + f"sample_coords={sorted(density_diff or mesh_diff)[:6]}") + runner.require(f"{label}:W{left}-W{right}", + left_rows == right_rows, detail) + + def run_ring(runner: VerificationRunner, *, full: bool) -> None: script = LINUX37 / "RunRingAuditsL37.py" audit_inputs = [script, @@ -604,6 +725,8 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False run_transitions_and_invariants(runner, binaries) run_ring(runner, full=True) run_captures_and_mesh_audits(runner) + run_random_camera_sweep(runner, (CLANG,), 5, + ((3, 0, 0), (2, 1, 1), (3, 2, 1))) def run_gcc_supplement(runner: VerificationRunner) -> None: @@ -635,6 +758,7 @@ def main() -> int: owners = run_owner_passages(runner, (CLANG,)) run_regression_controls(runner) run_hash_pairs(runner, owners) + run_random_camera_sweep(runner, (CLANG,), 3, ((3, 0, 0),)) run_ring(runner, full=False) elif ARGS.tier == "medium": run_medium_suite(runner, both_compilers=False) @@ -643,6 +767,8 @@ def main() -> int: if not runner.reuse_passed_tier(medium_summary, "medium"): 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))) run_30_cell_determinism(runner) run_cost_pairs(runner) # The validated medium summary carries the final canonical capture set.