fix: signed-overflow UB in VoxelHash made cave placement compiler-dependent; all generators LOD0-8; speed matrix (L60)
VoxelHash::Cell/Pair, legacy Cellular3D and the shared Core cellular hash added 0x7FFFFFFF to a signed int32 before casting to uint32; overflow is undefined, so Clang and GCC placed cave passage 1 differently (first differing sample at LOD1 tile (0,64,-512)). The bias is now done in unsigned arithmetic, in both the standalone copy and the Unreal plugin header (Source/VoxelForge/Public/VoxelCaveMorphology.h). Native caves, motifs and lattice now stream LOD0-8 with skirts and cross-level transitions. New verify.py --speed-matrix runs every generator on one asserted-identical view (5 interleaved runs): motifs 1.33, profiles 1.30-1.32, caves 1.41, natural SW 1.67, lattice 1.70, FI 4.93 s whole view. fast/medium/full PASS, Clang/GCC peer hashes equal. Research R29 (open-edge ledge) and R30 (compiler-divergence suspects). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
fbae82e024
commit
c0c204ba50
15 files changed
+938
-65
No files matched your search
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render cave, motif, and lattice features from meshes streamed at LOD4-8."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
CAPTURE54 = ROOT / "Standalone/Research/WorldAssemblerM3/Linux54/CaptureNativeCavesL54.py"
|
||||
spec = importlib.util.spec_from_file_location("capture_native_caves_l54", CAPTURE54)
|
||||
capture54 = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(capture54)
|
||||
run_step = capture54.run_step
|
||||
camera_angles = capture54.camera_angles
|
||||
fmt = capture54.fmt
|
||||
QUALITY = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
Q = 1_000_000.0
|
||||
|
||||
|
||||
def sha256(path: Path) -> 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)
|
||||
@@ -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 `<bit>` 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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -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<<LOD` and cell-count rule gives these normal steps (`VoxelForgeCore.cpp:2058, 2121–2122`; `owner_world.json:1`):
|
||||
|
||||
| LOD | Step `h` | Current 1 m depth in steps | Minimum `R=2h` | `W≈2√2R` | Full two-sided bullnose thickness `T≥2R` |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 0.25 m | 4 | 0.5 m | 1.41 m | 1 m |
|
||||
| 1 | 0.5 m | 2 | 1 m | 2.83 m | 2 m |
|
||||
| 2 | 2 m | 0.5 | 4 m | 11.31 m | 8 m |
|
||||
| 3 | 4 m | 0.25 | 8 m | 22.63 m | 16 m |
|
||||
| 4 | 8 m | 0.125 | 16 m | 45.25 m | 32 m |
|
||||
| 5 | 16 m | 0.0625 | 32 m | 90.51 m | 64 m |
|
||||
| 6 | 32 m | 0.03125 | 64 m | 181.02 m | 128 m |
|
||||
| 7 | 64 m | 0.015625 | 128 m | 362.04 m | 256 m |
|
||||
| 8 | 128 m | 0.0078125 | 256 m | 724.08 m | 512 m |
|
||||
|
||||
A basic two-interval slab needs `T≥2h`; roundovers on both top and underside require `T≥2R=4h`. The table shows the latter. The current 1 m support is four intervals at LOD0, two at LOD1, and only half a cell at LOD2.
|
||||
|
||||
**Near-LOD proposal:** give the open edge an explicit 0.5 m geometric bullnose (`R=2h` at LOD0), with `W≈1.41 m`. Set support depth to **1.5 m**: that is six LOD0 intervals, enough for two 0.5 m roundovers and a 0.5 m web. Build the support as negative solid in its intended volume; for the vertical band, the inside terms should be `(z-floorZ)` and `(floorZ-supportDepth-z)` before taking their maximum.
|
||||
|
||||
Keep the bullnose below the walk-floor plane and into the open-core side, leaving the floor edge at `r=4.5 m`. The 6 m route centerline then remains 1.5 m from that edge, and the 3 m radial tread remains `r=4.5–7.5 m`. A smooth-max width is a field-space parameter, so `W<3 m` alone does not prove that the 3 m walking surface survives; the zero contour must be checked in slices. If the finished floor edge moves into the tread, this construction fails the stated route margin.
|
||||
|
||||
At LOD1, the normal 0.5 m step makes a 0.5 m radius only one interval. I would refine ramp-intersecting LOD1 tiles to the 0.25 m Core step, keeping the same bullnose and support geometry. The current refinement in `VoxelForgeCore.cpp:2124–2149` applies to a different feature type; it does not show that resolved shaft-ramp edges already receive this treatment.
|
||||
|
||||
With the top floor unchanged, the reported maximum grade stays 10.028°, below 15°. Thickening only downward leaves the measured 2.20 m headroom unchanged. At 5 m pitch, a 1.5 m support leaves **3.5 m** from the lower floor to the next tread’s underside, so it does not intrude into the 2.20 m clearance.
|
||||
|
||||
## Far LOD and seams
|
||||
|
||||
The LOD numbers in the actual L59 fixture are coarser than “0.5–2 m at LOD2–8”: LOD1 is 0.5 m, LOD2 is 2 m, and LOD3–8 run from 4 m to 128 m.
|
||||
|
||||
At LOD2, a compliant bullnose would need `R=4 m`, `W≈11.31 m`, and `T≥8 m`. That exceeds the 3 m tread width and 5 m pitch. **Do not thicken the ledge.** My concrete proxy is:
|
||||
|
||||
- LOD0–1: the rounded helix ledge, with ramp tiles sampled at 0.25 m.
|
||||
- LOD2–3: remove the helical ledge and support; retain a smooth cylindrical core subtraction. Its 9 m diameter spans 4.5 LOD2 steps and 2.25 LOD3 steps.
|
||||
- LOD4–8: fade the local core opening into the surrounding host field. At LOD4 its 9 m diameter is only about 1.1 steps.
|
||||
|
||||
A field switch alone does **not** establish closed seams. The regular LOD1-to-LOD2 sampling ratio is 4:1; local LOD1 refinement makes it 8:1 at this feature. Both sides need a shared LOD morph evaluated at the same world positions, plus transition cells or a verified skirt stitch for that ratio. The owner requests skirts, but the branch has no L59 captures above LOD0 to demonstrate that this ledge-to-cylinder or cylinder-to-host transition stays closed. The far-LOD proposal is therefore a specific representation plan, not a proven seam result.
|
||||
|
||||
## Audit and slice plan
|
||||
|
||||
The L59 audit excluded the shaft-core room (`AuditLatticeSurfaceL59.py:82–91`) and set a 0.25 m corridor margin (`:229–248`). Its bounds and corridor test expand by that margin (`:159–199`). Shard triangles protruding farther into the core fall outside that ROI.
|
||||
|
||||
Extend an edge-focused ROI along the full helix, including the free edge and an apron on both the core and tread sides. At each LOD, use an edge band of at least `max(1 m, 4h, W_edge+W_union)` from the nominal edge; include the full floor, support underside, and headroom in the vertical bounds. Add edge-specific shard, normal-discontinuity, and open-boundary checks instead of relying only on the corridor’s broad surface score.
|
||||
|
||||
Keep the original L59 LOD0 mesh as a negative control: its free edge must fail the expanded edge checks; the repaired edge must pass; retain a plain-native pass control. Slice the analytic field radially through the edge at several azimuths and turn phases, then unwrap the full four-turn edge. Check the 0.5 m bullnose and 1.5 m support at sub-voxel resolution, including half-cell grid offsets. Recheck the 883 route stations and the reported capsule, support, and headroom samples, with gates of grade ≤15°, headroom ≥2.2 m, and route-centerline distance to the finished edge ≥1.5 m. Inspect LOD1/2 and LOD3/4 transition boundaries separately.
|
||||
|
||||
## Assumptions and skipped evidence
|
||||
|
||||
The tracked L59 report contains the visual diagnosis, but the branch’s `Linux59` directory does not include the resolved-layout JSON or exported `.vfmesh`. The capture script explicitly exports `--max-level=0` (`CaptureNativeLatticeL59.py:78–85`). I therefore do not know the recorded smooth and union widths, and I did not inspect triangle normals or re-view the JPEG pixels. Those gaps prevent me from proving which field term made the observed shards. I did not run scripts, tests, builds, or binaries, and made no source edits.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Research29 — L59 open shaft rim
|
||||
|
||||
## Mechanism
|
||||
|
||||
The L59 dimensions make the ramp’s inner boundary exactly coincide with the core boundary: `6.0 m` centerline radius minus `1.5 m` passage radius equals the `4.5 m` core radius. The ramp has a `1.0 m` support depth and a `5.0 m` pitch. ([Linux59 report](https://example.invalid) records these dimensions; `Standalone/Research/WorldAssemblerM3/Linux59/REPORT.md:5`.)
|
||||
|
||||
The core void is a hard intersection:
|
||||
|
||||
```cpp
|
||||
radial = hypot(dx, dy) - radius;
|
||||
bottom = floor - point[2];
|
||||
featureVoid = max(radial, bottom);
|
||||
```
|
||||
|
||||
`wip/l59-open-shaft:Standalone/Core/src/ShapeWorldInternal.h:7340–47`
|
||||
|
||||
The ramp void is an annular tube intersected with the floor/ceiling interval. The interval uses a hard `max`, then the tube and interval use cubic smooth-max:
|
||||
|
||||
```cpp
|
||||
corridorSdf = max((floorZ-point[2])*scale,
|
||||
(point[2]-(floorZ+height))*scale);
|
||||
featureVoid = SmoothMaxMetres(tube, openHelixVerticalSdf, width);
|
||||
```
|
||||
|
||||
`ShapeWorldInternal.h:7410–17, 7522–25`. Core and ramp voids are then combined by smooth-min, falling back from `unionWidth` to `smoothWidth`, before subtraction from constant host rock: `ShapeWorldInternal.h:7532–50`.
|
||||
|
||||
The leading suspect is the **coincident, oppositely directed zero surfaces** at the core/ramp contact. With zero or insufficient union width, the touching voids form a sharp or pinched seam. Marching Cubes samples that off-grid feature with its ordinary cell topology; the resulting aliasing is consistent with the torn shards. Kobbelt et al. describe severe alias artifacts at sharp features in regular-grid surface extraction and explain that grid refinement alone does not resolve the normal error ([paper](https://people.eecs.berkeley.edu/~jrs/meshpapers/KBSS.pdf)). The ramp floor/tube intersection is smooth-max blended, but its actual width and the feature-union width are absent from the tracked L59 report.
|
||||
|
||||
There is also a likely support-field defect. The open-helix support uses:
|
||||
|
||||
```cpp
|
||||
vertical = max((floorZ-point[2])*scale,
|
||||
(point[2]-(floorZ-supportDepth))*scale);
|
||||
supportSdf = max(nearestRadial-radius, vertical);
|
||||
composed = min(composed, latticeFloorSupportMinimum);
|
||||
```
|
||||
|
||||
`ShapeWorldInternal.h:7497–15, 7551–55`
|
||||
|
||||
With VoxelForge’s negative-solid, positive-air convention, this vertical expression is nonnegative everywhere; it is positive inside the intended below-floor slab. The final `min` therefore cannot add negative solid there. A negative-inside support slab would reverse both vertical terms. This is a high-confidence sign concern from the equations, but I cannot isolate its visual effect without field samples. The hard support cap can also distort field values after the smooth feature union.
|
||||
|
||||
The reported `1 m` support is four LOD0 cell steps at `0.25 m`, so support thickness alone is unlikely to explain the LOD0 capture. It is only two steps at `0.5 m` and falls below one cell at a `2 m` step. Relief noise is unlikely to be the cause: the open-shaft path bypasses the legacy cap path, where the noise evaluation lives (`ShapeWorldInternal.h:7187–91, 7249–55`).
|
||||
|
||||
## Construction and numbers
|
||||
|
||||
Build one continuous core-plus-ramp void, smooth-union its components, then subtract that void from the host once. Encode the ledge floor in the void definition and remove the post-union support cap; preserve the support as negative solid in the composed field. Verify that smoothing keeps the intended `4.5 m` core opening.
|
||||
|
||||
For the current cubic smooth-max, Research16 estimates a local fillet radius `R ≈ W/(2√2)` for perpendicular unit-gradient fields (`Research16/REPORT.md:29–31`). Applied as a sizing guide:
|
||||
|
||||
| Mesh step `h` | Minimum bullnose `R=2h` | Approx. width `W=2√2R` | Slab thickness `T≥2R`* |
|
||||
|---:|---:|---:|---:|
|
||||
| 0.25 m | 0.50 m | 1.41 m | 1 m |
|
||||
| 0.50 m | 1.00 m | 2.83 m | 2 m |
|
||||
| 2.00 m | 4.00 m | 11.31 m | 8 m |
|
||||
|
||||
\*For a bullnose that rounds both the top and underside. A preferred `R=3h` increases the required width and thickness by 50%. These are sampling targets, not guarantees; measure the composed field because its gradients and intersection angles vary.
|
||||
|
||||
At LOD0, the existing `1 m` support only meets the minimum two-sided bullnose thickness, with no margin. A `0.75 m` radius is a safer preferred LOD0 target and calls for at least `1.5 m` of slab.
|
||||
|
||||
## Far LODs
|
||||
|
||||
The L59 capture command forces `--max-level=0` (`CaptureNativeLatticeL59.py:78–85`), so it provides no evidence for coarser meshes. With the owner defaults (`0.25 m` Core voxels, `full_res_clip_levels=2`, `coarse_tile_cells=16`) and the mesher’s extent/cell calculation (`VoxelForgeCore.cpp:2121–22`), nominal steps are `0.25 m` at LOD0, `0.5 m` at LOD1, then `2, 4, 8, 16, 32, 64, 128 m` at LOD2–8. The ramp is a resolved cavern edge; the separate fine-resolution override checks type-4 world-assembly shelf features (`VoxelForgeCore.cpp:2124–49`).
|
||||
|
||||
A fixed `1 m` ledge cannot survive those far steps. Keep the detailed ledge in near LODs, refining ramp tiles where needed. At far LODs, fade it into a broad, rounded wall bump and then out of the silhouette. Do not keep thickening the walkable slab: at LOD2 the two-sided bullnose sizing already asks for an `8 m` slab and an `11.3 m` smooth width against a `3 m` passage.
|
||||
|
||||
## Audit and slice plan
|
||||
|
||||
The L59 audit uses a `0.25 m` corridor margin and excludes the shaft-core room from its room ROI (`AuditLatticeSurfaceL59.py:82–91, 159–205, 229–48`). That leaves only a one-voxel allowance for shards protruding into the core. Expand the ROI and its bounds to cover at least a `1 m` band on both sides of each free edge, plus the full `1 m` underside and headroom. Add edge-specific coverage and roughness measurements so the free rim is scored even when its triangles cannot serve as smooth reference terrain.
|
||||
|
||||
Run the raw L59 mesh as a negative control with `expect=fail`; require the edge defect to be detected. Require the corrected candidate and plain-native control to pass. Include all four turns, both free edges, and the support underside.
|
||||
|
||||
Proposed slices: sample radial/vertical sections through the core and ledge at quarter-turn intervals and multiple azimuths, at `0.0625 m` increments. Record the core, ramp, support, union, and final density values. Then compare current and corrected LOD0 meshes across half-cell grid phases; inspect LOD1 and LOD2 transitions, followed by the far-LOD bump/fade through LOD8. The existing audit phase option shifts its analytical reference; it does not remesh a shifted grid.
|
||||
|
||||
## Uncertainty
|
||||
|
||||
The tracked L59 report does not give the resolved `smoothWidth` or `unionWidth`, and the branch has no raw mesh/audit JSON here to localize the shard triangles. The tangent-seam diagnosis is therefore likely, not proven. The support sign reversal is directly visible in the equations and deserves field-level confirmation.
|
||||
|
||||
Read-only inspection only; no files were changed and no tests or binaries were run.
|
||||
@@ -0,0 +1,29 @@
|
||||
## Ranked suspects
|
||||
|
||||
| Rank | Suspect and fit | Cheapest confirming experiment |
|
||||
|---|---|---|
|
||||
| **1 — cave segment query arithmetic** — [`ShapeWorldInternal.h:7124–7199`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:7124). The cave field chooses a nearest segment using computed distances, a `1e-12 m` tie tolerance, and BVH pruning. Small differences in `hypot` or intermediate arithmetic could change the selected segment, its floor/grade data, or whether a node is visited. That fits stable, spatially repeatable failures on a small set of cave tiles. | For one bad tile, find the first differing density lattice coordinate. At that point log the BVH’s visited/pruned nodes and selected segment, then compare with a full segment scan in index order. |
|
||||
| **2 — cave detection in batched sampling** — [`ShapeWorldInternal.h:3157–3224, 10177–10220`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3157). The new X8 and Z-run paths test both manager and resolved cave bounds before falling back to scalar density. A boundary classification or fallback difference could affect coarse samples at particular Z values. The L60 report says broader bulk-versus-scalar diagnostics were tried, so a targeted trace is more useful than repeating that toggle. | At the first differing coordinate, record its manager/resolved stratum, batch route, and scalar result bits. |
|
||||
| **3 — coarse meshing and skirts** — [`VoxelMarchingCubesMesher.cpp:1239–1305, 2070–2172`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1239). A density difference near zero can change cell classification or interpolation. Skirts also make extra continuous-position `DensityAt` calls, with depth scaled by the mesh step. This is a downstream explanation if density hashes match. | Print the density, mesh, payload, and topology hash comparisons separately. If density matches but mesh output differs, regenerate one bad tile with skirts disabled and compare the surface mesh. |
|
||||
|
||||
## LOD1–2 path and other checked constructs
|
||||
|
||||
The L60 log reports the same resolved-layout fingerprint, `3c14454ca74615d0`, for the cave runs across GCC and Clang. That points away from different resolved cave inputs, though a fingerprint is not a byte-for-byte comparison. Each compiler is internally stable across worker counts, and the failures recur on the same six LOD1 and eight LOD2 keys. The report says earlier diagnostics covered segment ordering/ties and bulk-versus-scalar sampling. ([Linux60 report](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux60/REPORT.md:11), [run log](/home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l60/full.log:569))
|
||||
|
||||
`generateTile` uses `extent = CHUNK_SIZE << level`; in the normal coarse path, LOD1 and LOD2 use steps 2 and 4. Type-4 shelf features can force step 1 on overlapping tiles, while hybrid sharp-feature handling is LOD0-only. The tile still goes through the same marching-cubes mesher. LOD1’s step-2 proof path differs from LOD2’s step-4 path, so proof behavior alone does not explain both levels. Skirts run at these LODs; connector shells are gated to LOD6+, and far stratum shells to LOD7+.
|
||||
|
||||
Other legal sources of compiler variation checked:
|
||||
|
||||
- `DensityGrid` is allocated with `SetNumUninitialized`; a broken proof-skip invariant could expose unread values. The hash code reads individual mesh fields and density float bits, not struct padding. It does not hash raw object bytes. ([mesher allocation](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:898), [hashing](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:157))
|
||||
- C++ does not specify the order in which function arguments are evaluated. I found no output-affecting call in these paths with side effects across arguments. ([C++ call rules](https://eel.is/c++draft/expr.call))
|
||||
- The cave evaluator uses `sqrt` and `hypot`; I found no `pow`, `exp`, `sin`, or `atan2` in its runtime field path. GCC can use MPFR to fold constant-argument math builtins, but the matching layout fingerprint makes recipe-planning math a weaker suspect here. ([GCC notes](https://gcc.gnu.org/gcc-4.3/changes.html))
|
||||
- Integer lattice coordinates convert exactly to `float` within the scene’s range; the cave evaluator then uses doubles. Long-double bounds calculations are present in cave queries. On this Linux target, GCC and Clang likely share the platform ABI and math libraries, but `BuildLinux.sh` does not pin or report Clang’s standard-library choice.
|
||||
- The changed segment `std::sort` comparator breaks equal centers by segment index, giving a total order for valid segments. So differing `std::sort` handling of equivalent keys is a weak explanation there. A separate stratum sort compares only bottom Z, so duplicate bottoms could leave equivalent elements in implementation-dependent order. ([sort rules](https://eel.is/c++draft/alg.sort), [stable sort rules](https://eel.is/c++draft/algorithm.stable))
|
||||
- Signed overflow and invalid shifts would be undefined behavior, but the LOD1–8 extent shifts are in range. No concrete overflow in the affected path stands out.
|
||||
- The peer comparator reports only an aggregate failure: it checks density, mesh, payload, and topology hashes, not material hash. Splitting those results is the cheapest first discriminator. ([`SameTileOutput`](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:798))
|
||||
|
||||
## MSVC notes
|
||||
|
||||
The segment math, library math variation, argument-order latitude, equivalent-key sort behavior, and any undefined uninitialized/overflow behavior can also affect Windows builds. MSVC’s `long double` has the same precision and representation as `double`, unlike the extended-precision Linux ABI, so the long-double query calculations are a particular portability risk. ([MSVC type documentation](https://learn.microsoft.com/en-us/cpp/cpp/data-type-ranges?view=msvc-170))
|
||||
|
||||
`CoreNamespacePreamble.h` disables FP contraction for Clang, GCC, and MSVC; the Windows result still depends on the project’s effective `/fp` mode and runtime math implementation. ([preamble](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/CoreNamespacePreamble.h:61), [MSVC FP modes](https://learn.microsoft.com/en-us/cpp/build/reference/fp-specify-floating-point-behavior?view=msvc-170))
|
||||
Reference in new issue
Block a user