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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
422dd7761a
commit
036bd530fc
6 files changed
+288
-18
No files matched your search
@@ -50,6 +50,11 @@ CLANG = OUT / "bin" / f"VoxelForgeCoreHeadless-{ROUND_NAME}-clang"
|
||||
GCC = OUT / "bin" / f"VoxelForgeCoreHeadless-{ROUND_NAME}-gcc"
|
||||
RENDERER = OUT / "bin" / f"VoxelForgeOffscreenCapture-{ROUND_NAME}-clang"
|
||||
HASH_RE = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
TILE_HASH_RE = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) "
|
||||
r"payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
CAMERA_SWEEP_SEED = 0x4C3434
|
||||
RECIPE_RE = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
|
||||
@@ -284,6 +289,122 @@ def run_hash_pairs(runner: VerificationRunner,
|
||||
f"current owner hashes match {baseline.name}; hashes={len(old_hashes)}")
|
||||
|
||||
|
||||
class _SplitMix64:
|
||||
"""Small fixed-seed generator whose camera sequence is independent of Python random."""
|
||||
def __init__(self, seed: int):
|
||||
self.state = seed & 0xffffffffffffffff
|
||||
|
||||
def unit(self) -> 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.
|
||||
|
||||
Reference in new issue
Block a user