diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f37b266..8fd0fb1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1856,6 +1856,19 @@ un-inset to match the Unreal adapter. Versions 1 and 2 retain their serialized t SurfaceWorld's field and output hashes remain unchanged. See [`Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux18/REPORT.md). +### 8.38 Closed shared boundary ceilings and LOD sampling audit — World Assembler L19 + +`AppendStratumBoundaryCeilings` fills a candidate cell when the authoritative field is solid just +inside that strate's configured upper boundary. It does not require the raw field to cross back to +air above the boundary: the ceiling itself closes the strate even when the field continues solid +through that plane. This avoids isolated holes where a coarse lattice node has no zero crossing. +`FFarMeshColumnIndex::HasDownwardCellCoverage` still suppresses a fallback quad only after its +interior and perimeter probes all hit the existing downward face. The L19 `farHorizonVersion` is 4 +for this shared boundary-cap change; hashes remain direct at LOD0–3 and salted at LOD4–8. No density +query, stratum position, scale, or asynchronous route changes. The strict LOD0–8 roof and paired-edge +audit and the matched before/after captures are in +[`Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md`](Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md). + ## 9. Multiplayer model (listen-server first, dedicated-friendly) > **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 4c85c4d..13b38d4 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -1096,11 +1096,11 @@ namespace VoxelForgeCore // LOD8-only planar horizon interior grid; zero follows the MC step, 128 restores S9. // Grille interieure LOD8 seulement; zero suit la maille MC, 128 restaure S9. std::int32_t lod8HorizonInteriorStepVoxels = 0; - // Bump when the distant cap representation changes. LOD4-8 mesh hashes are salted; - // density and LOD0-3 hashes remain unchanged. - // Incremente quand le plafond lointain change. Les hashes LOD4-8 incluent cette version; - // densite et hashes LOD0-3 restent inchanges. - std::uint32_t farHorizonVersion = 3; + // Bump when shared stratum-boundary caps or the distant horizon representation changes. + // LOD4-8 mesh hashes are salted; density and LOD0-3 keep their direct identities. + // Incremente quand les plafonds partages ou l'horizon lointain changent. + // Les hashes de maillage LOD4-8 incluent cette version; LOD0-3 gardent leur identite directe. + std::uint32_t farHorizonVersion = 4; // Linux research controls for reproducing L5 negative controls. Defaults preserve output. std::uint32_t diagnosticGlobalOctaveDrop = 0; std::uint32_t diagnosticCellStride = 1; diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index 53622d9..78f9b3c 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -894,7 +894,11 @@ struct FFarMeshColumnIndex bool HasDownwardCellCoverage(double X0,double Y0,double X1,double Y1, double Z,double Tolerance) const { - constexpr double Samples[3]={0.125,0.5,0.875}; + // Probe the perimeter as well as the interior: sparse interior-only probes can + // incorrectly hide a missing ceiling quad at an MC grid vertex. + // Sonder le perimetre et l'interieur: des sondes seulement interieures peuvent + // masquer un carre de plafond manquant sur un sommet de la grille MC. + constexpr double Samples[5]={0.0,0.25,0.5,0.75,1.0}; for(const double V:Samples)for(const double U:Samples) { const double X=X0+(X1-X0)*U; @@ -1185,7 +1189,7 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, struct FCandidate { FStandaloneShapeWorld::FResolvedStratumBounds Bounds; - std::vector Crossing; + std::vector SolidBelow; }; std::vector Candidates; for(const FStrateSlot& Slot:World.Manager->GetLayout()) @@ -1197,21 +1201,22 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, ||Candidate.Bounds.TopZ>Origin.Z+Extent+1.0e-6)continue; const double Epsilon=std::min(0.5,Candidate.Bounds.SealThickness*0.25); if(Epsilon<=0.0)continue; - Candidate.Crossing.resize(static_cast(GridSide)*GridSide,0); - bool AnyCrossing=false; + Candidate.SolidBelow.resize(static_cast(GridSide)*GridSide,0); + bool AnySolidBelow=false; for(int32 Y=0;Y(WorldX),static_cast(WorldY), static_cast(Candidate.Bounds.TopZ-Epsilon)); - const float Above=Shape.DensityAt(static_cast(WorldX),static_cast(WorldY), - static_cast(Candidate.Bounds.TopZ+Epsilon)); - const bool bCrosses=std::isfinite(Below)&&std::isfinite(Above)&&Below<0.0f&&Above>0.0f; - Candidate.Crossing[static_cast(Y*GridSide+X)]=bCrosses?1:0; - AnyCrossing=AnyCrossing||bCrosses; + // The shared upper boundary closes the stratum even where its raw field stays + // solid above the configured top. A zero-crossing test leaves holes there. + // Le plafond partage ferme la strate meme si le champ reste solide au-dessus. + const bool bSolidBelow=std::isfinite(Below)&&Below<0.0f; + Candidate.SolidBelow[static_cast(Y*GridSide+X)]=bSolidBelow?1:0; + AnySolidBelow=AnySolidBelow||bSolidBelow; } - if(AnyCrossing)Candidates.push_back(std::move(Candidate)); + if(AnySolidBelow)Candidates.push_back(std::move(Candidate)); } if(Candidates.empty())return; @@ -1248,8 +1253,8 @@ inline void AppendStratumBoundaryCeilings(const FStandaloneOwnerWorld& World, { const std::size_t I00=static_cast(Y*GridSide+X); const std::size_t I10=I00+1,I01=I00+GridSide,I11=I01+1; - if(!Candidate.Crossing[I00]||!Candidate.Crossing[I10] - ||!Candidate.Crossing[I01]||!Candidate.Crossing[I11])continue; + if(!Candidate.SolidBelow[I00]||!Candidate.SolidBelow[I10] + ||!Candidate.SolidBelow[I01]||!Candidate.SolidBelow[I11])continue; const double CellX0=Origin.X+X*GridStep,CellY0=Origin.Y+Y*GridStep; const double CellX1=CellX0+GridStep,CellY1=CellY0+GridStep; // Suppress a coincident plane only when it covers the full cell. A hit at just the diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/CaptureLinux19.py b/Standalone/Research/WorldAssemblerM3/Linux19/CaptureLinux19.py new file mode 100644 index 0000000..573e62d --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/CaptureLinux19.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Render matched L19 review captures from the L18 and L19 scene exports.""" +import os +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +CAPTURE = ROOT / "Standalone/bin/linux/VoxelForgeOffscreenCapture-linux19-clang" +BEFORE = ROOT / "Standalone/Research/WorldAssemblerM3/Linux18/exports" +AFTER = ROOT / "Saved/Linux/l19-diagnostics" +OUT = ROOT / "Standalone/Research/WorldAssemblerM3/Linux19/captures" +PNG_OUT = ROOT / "Saved/Linux/l19-captures-temp" +LOGS = ROOT / "Saved/Linux/l19-diagnostics" +OUT.mkdir(parents=True, exist_ok=True) +PNG_OUT.mkdir(parents=True, exist_ok=True) +env = os.environ.copy() +env["LP_NUM_THREADS"] = "12" +env["XDG_CACHE_HOME"] = str(ROOT / "Saved/Linux/mesa-cache") + +views = [ + ("floating-islands-flat-square-before", BEFORE / "floating-islands-flat-square.vfmesh", + (-11.612444, -118.221237, -19.8452), 0, 0, True), + ("floating-islands-flat-square-after", AFTER / "floating-islands-flat-square-after.vfmesh", + (-11.612444, -118.221237, -19.8452), 0, 0, True), + ("floating-islands-owner-angle-before", BEFORE / "floating-islands-island-view.vfmesh", + (-22, -19, -22.8), 0, -4, True), + ("floating-islands-owner-angle-after", AFTER / "floating-islands-island-view-after.vfmesh", + (-22, -19, -22.8), 0, -4, True), + ("floating-islands-owner-ground-before", AFTER / "floating-islands-owner-ground-before.vfmesh", + (-6.3, -19.06, -21.0), 0, -4, True), + ("floating-islands-owner-ground-after", AFTER / "floating-islands-owner-ground-after.vfmesh", + (-6.3, -19.06, -21.0), 0, -4, True), + ("floating-islands-ceiling-up-after", AFTER / "floating-islands-island-view-after.vfmesh", + (-22, -19, -22.8), 0, 80, True), + ("surface-world-l1-spawn-before", BEFORE / "surface-world-l1-spawn.vfmesh", + (0, -35, -487.94), 0, -22, False), + ("surface-world-l1-spawn-after", AFTER / "surface-world-l1-spawn-after.vfmesh", + (0, -35, -487.94), 0, -22, False), + ("surface-world-horizon-before", BEFORE / "surface-world-l1-spawn.vfmesh", + (0, -35, -487.94), 0, 0, False), + ("surface-world-horizon-after", AFTER / "surface-world-l1-spawn-after.vfmesh", + (0, -35, -487.94), 0, 0, False), + ("surface-world-rings-above-before", BEFORE / "surface-world-l1-spawn.vfmesh", + (0, -35, -280), 0, -90, True), + ("surface-world-rings-above-after", AFTER / "surface-world-l1-spawn-after.vfmesh", + (0, -35, -280), 0, -90, True), +] + +for name, scene, camera, yaw, pitch, tile_edges in views: + if not scene.exists(): + raise SystemExit(f"missing scene: {scene}") + png = PNG_OUT / f"{name}.png" + jpg = OUT / f"{name}.jpg" + render_log = LOGS / f"{name}-render.log" + convert_log = LOGS / f"{name}-jpeg.log" + command = [str(CAPTURE), str(scene), str(png), "--size=1600x900", + "--shade=readable", f"--tile-edges={'on' if tile_edges else 'off'}", + "--tile-edge-width-m=0.15", "--camera-m="+",".join(map(str, camera)), + f"--yaw-deg={yaw}", f"--pitch-deg={pitch}"] + with render_log.open("w", encoding="utf-8") as stream: + subprocess.run(command, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, check=True) + ffmpeg = ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-i", str(png), "-frames:v", "1", "-q:v", "4", str(jpg)] + with convert_log.open("w", encoding="utf-8") as stream: + subprocess.run(ffmpeg, cwd=ROOT, env=env, stdout=stream, + stderr=subprocess.STDOUT, check=True) + if jpg.stat().st_size > 300_000: + raise SystemExit(f"{jpg.name} exceeds the 300 KB review limit") + print(f"[Capture] {jpg.relative_to(ROOT)} bytes={jpg.stat().st_size} status=PASS", flush=True) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/CeilingContinuityL19.py b/Standalone/Research/WorldAssemblerM3/Linux19/CeilingContinuityL19.py new file mode 100644 index 0000000..19376b2 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/CeilingContinuityL19.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Strict L19 audit of FloatingIslands roof coverage at every LOD.""" +import array +import collections +import math +import struct +import sys + + +def read_exact(stream, size): + data = stream.read(size) + if len(data) != size: + raise EOFError(f"wanted {size} bytes, got {len(data)}") + return data + + +def height_at(triangles, x, y, preferred_z): + best = None + for (x0, y0, z0), (x1, y1, z1), (x2, y2, z2) in triangles: + den = (y1-y2)*(x0-x2)+(x2-x1)*(y0-y2) + if abs(den) < 1.0e-10: + continue + a = ((y1-y2)*(x-x2)+(x2-x1)*(y-y2))/den + b = ((y2-y0)*(x-x2)+(x0-x2)*(y-y2))/den + c = 1.0-a-b + if min(a,b,c) >= -1.0e-5: + z = a*z0+b*z1+c*z2 + if best is None or abs(z-preferred_z) < abs(best-preferred_z): + best = z + return best + + +def quantile(values, fraction): + if not values: + return math.nan + ordered = sorted(values) + return ordered[math.floor(fraction*(len(ordered)-1))] + + +if len(sys.argv) != 3: + raise SystemExit("usage: CeilingContinuity.py scene.vfmesh expected-cap-plane-metres") +scene_path = sys.argv[1] +root_z = float(sys.argv[2]) +tiles = {} +voxel_size_m = None +with open(scene_path, "rb") as stream: + if read_exact(stream, 8) != b"VFMSH001": + raise SystemExit("invalid VFMSH001 scene") + version, voxel_um, tile_count = struct.unpack("<3I", read_exact(stream, 12)) + camera = struct.unpack("<3d", read_exact(stream, 24)) + yaw, pitch = struct.unpack("<2d", read_exact(stream, 16)) + voxel_size_m = voxel_um/1_000_000.0 + for _ in range(tile_count): + key = struct.unpack("<4i", read_exact(stream, 16)) + surface_vertices, skirt_count, vertex_count, index_count, mask_count = struct.unpack( + "<5I", read_exact(stream, 20)) + positions = array.array("f") + positions.frombytes(read_exact(stream, vertex_count*12)) + normals = array.array("f") + normals.frombytes(read_exact(stream, vertex_count*12)) + indices = array.array("I") + indices.frombytes(read_exact(stream, index_count*4)) + masks = read_exact(stream, mask_count) + if sys.byteorder != "little": + positions.byteswap() + normals.byteswap() + indices.byteswap() + extent = 32*(1 << key[3])*voxel_size_m + z0, z1 = key[2]*extent, (key[2]+1)*extent + ceiling = [] + for triangle in range(index_count//3): + if masks and masks[triangle] != 0: + continue + a,b,c = indices[triangle*3:triangle*3+3] + if max(a,b,c) >= surface_vertices: + continue + nz = (normals[a*3+2]+normals[b*3+2]+normals[c*3+2])/3.0 + if nz > -0.65: + continue + points = tuple((positions[v*3],positions[v*3+1],positions[v*3+2]) + for v in (a,b,c)) + center_z = sum(point[2] for point in points)/3.0 + if abs(center_z-root_z) <= 1.0: + ceiling.append(points) + tiles[key] = { + "extent": extent, "x0": key[0]*extent, "y0": key[1]*extent, + "x1": (key[0]+1)*extent, "y1": (key[1]+1)*extent, + "z0": z0, "z1": z1, "ceiling": ceiling, + } + +by_lod = collections.defaultdict(dict) +for key, tile in tiles.items(): + if tile["z0"] <= root_z+1.0 and tile["z1"] >= root_z-1.0: + by_lod[key[3]][key] = tile + +print(f"scene={scene_path} version={version} voxel_um={voxel_um} tiles={tile_count} " + f"camera_m={camera} yaw={yaw} pitch={pitch} expected_cap_m={root_z:.9f}") +print("lod zband_tiles cap_tiles missing_tiles cap_triangles face_z_m_p05/median/p95 " + "tile_samples hit_samples max_abs_root_delta_cm shared_edges expected_edge_samples " + "paired_edge_samples edge_gaps edge_delta_cm_median/p95/max") +all_pass = True +all_misses = [] +for lod in range(9): + target = by_lod[lod] + cap_tiles = {key: tile for key,tile in target.items() if tile["ceiling"]} + face_z = [point[2] for tile in cap_tiles.values() for tri in tile["ceiling"] + for point in tri] + deltas = [] + sample_count = 0 + hit_count = 0 + for key,tile in target.items(): + if not tile["ceiling"]: + continue + for iy in range(16): + for ix in range(16): + x = tile["x0"]+(ix+0.5)*tile["extent"]/16 + y = tile["y0"]+(iy+0.5)*tile["extent"]/16 + sample_count += 1 + height = height_at(tile["ceiling"],x,y,root_z) + if height is not None: + hit_count += 1 + deltas.append(abs(height-root_z)*100.0) + else: + all_misses.append((lod,key,ix,iy,x,y)) + + edge_deltas = [] + edge_count = 0 + edge_gaps = 0 + edge_gap_samples = 0 + for key,a in cap_tiles.items(): + x,y,z,level = key + for neighbor_key,axis in (((x+1,y,z,level),"x"),((x,y+1,z,level),"y")): + b = cap_tiles.get(neighbor_key) + if b is None: + continue + edge_count += 1 + epsilon = min(0.02,a["extent"]*1.0e-4) + matched = 0 + for index in range(33): + t = (index+0.5)/33 + if axis == "x": + fixed = a["x1"] + varying = a["y0"]+(a["y1"]-a["y0"])*t + za = height_at(a["ceiling"],fixed-epsilon,varying,root_z) + zb = height_at(b["ceiling"],fixed+epsilon,varying,root_z) + else: + fixed = a["y1"] + varying = a["x0"]+(a["x1"]-a["x0"])*t + za = height_at(a["ceiling"],varying,fixed-epsilon,root_z) + zb = height_at(b["ceiling"],varying,fixed+epsilon,root_z) + if za is not None and zb is not None: + edge_deltas.append(abs(za-zb)*100.0) + matched += 1 + edge_gap_samples += 33-matched + if matched != 33: + edge_gaps += 1 + missing = len(target)-len(cap_tiles) + max_root = max(deltas) if deltas else math.nan + print(f"{lod} {len(target)} {len(cap_tiles)} {missing} " + f"{sum(len(tile['ceiling']) for tile in cap_tiles.values())} " + f"{quantile(face_z,.05):.6f}/{quantile(face_z,.5):.6f}/{quantile(face_z,.95):.6f} " + f"{sample_count} {hit_count} {max_root:.3f} {edge_count} {edge_count*33} {len(edge_deltas)} " + f"{edge_gaps} {quantile(edge_deltas,.5):.3f}/{quantile(edge_deltas,.95):.3f}/" + f"{max(edge_deltas) if edge_deltas else math.nan:.3f}") + if target and (missing or hit_count != sample_count or edge_gap_samples): + all_pass = False +for lod,key,ix,iy,x,y in all_misses: + print(f" miss lod={lod} key={key} sample=({ix},{iy}) xy_m=({x:.6f},{y:.6f})") +print(f"[CeilingContinuityL19] total_misses={len(all_misses)} " + f"result={'PASS' if all_pass else 'FAIL'} criteria=LOD0-8 every 16x16 interior ray hits; " + "every shared same-LOD edge sample has paired hits") +raise SystemExit(0 if all_pass else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/CheckL19Hashes.py b/Standalone/Research/WorldAssemblerM3/Linux19/CheckL19Hashes.py new file mode 100644 index 0000000..d16fcb9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/CheckL19Hashes.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Check deterministic matrices and protected SurfaceWorld identities against L14.""" +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +MATRIX = ROOT / "Saved/Linux/l19-matrix" +CROSSING = ROOT / "Saved/Linux/l19-crossing" +ROW = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +LOD = re.compile(r"^\[PipelineLODHash\] lod=(\d+) .*$", re.M) +failed = [] + + +def stable_rows(path): + rows = ROW.findall(path.read_text(encoding="utf-8", errors="replace")) + full = next((row for row in rows if row.startswith("[PipelineFullDetailHash]")), None) + exact = next((row for row in rows if row.startswith("[PipelineExactQueryHash]")), None) + lods = {int(match.group(1)): match.group(0) for match in LOD.finditer( + path.read_text(encoding="utf-8", errors="replace"))} + return rows, full, exact, {level: lods.get(level) for level in range(3)} + + +for compiler in ("clang-off", "gcc-default"): + baseline_name = "clang-off" if compiler == "clang-off" else "gcc-default" + baseline = ROOT / f"Saved/Linux/l14-final-hash-sw-{baseline_name}-w12-r5.log" + expected = stable_rows(baseline) + logs = sorted(MATRIX.glob(f"surface-world-{compiler}-w*-r*.log")) + if len(logs) != 7: + failed.append(f"matrix {compiler}: expected 7 rows, found {len(logs)}") + for path in logs: + actual = stable_rows(path) + if len(actual[0]) != 12 or actual[1:] != expected[1:]: + failed.append(f"protected SurfaceWorld mismatch: {path.name}") + + crossing_baseline_name = "clang" if compiler == "clang-off" else "gcc" + crossing_baseline = ROOT / f"Saved/Linux/l14-final-crossing-24m-{crossing_baseline_name}-w12.log" + expected_crossing = stable_rows(crossing_baseline) + crossing_logs = sorted(CROSSING.glob(f"{compiler}-w*-r*.log")) + if len(crossing_logs) != 7: + failed.append(f"crossing {compiler}: expected 7 rows, found {len(crossing_logs)}") + for path in crossing_logs: + actual = stable_rows(path) + if len(actual[0]) != 12 or actual[1:] != expected_crossing[1:]: + failed.append(f"protected crossing identity mismatch: {path.name}") + +print(f"[L19ProtectedHashGate] SurfaceWorld LOD0-2, full-detail, and exact query hashes " + f"result={'FAIL' if failed else 'PASS'}") +for failure in failed: + print(f"[L19HashMismatch] {failure}") +raise SystemExit(1 if failed else 0) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md new file mode 100644 index 0000000..7b239e4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/REPORT.md @@ -0,0 +1,123 @@ +# Linux19 — FloatingIslands ceiling and LOD ledges + +**Status:** The ceiling is now closed at every LOD. The lower-terrain ledges are diagnosed but remain visible: the required change affects protected SurfaceWorld LOD2 output, so I stopped before changing it and asked for your decision. + +## Findings and changes + +The ceiling holes came from two checks in the shared boundary-cap path. It required density to cross from solid just below a strate's configured top to air just above it. Some of the field remains solid above that top, so the old test omitted those cap cells. The duplicate-cap check also sampled only inside each cell and could miss partial perimeter coverage. The path now emits a cap wherever the field is solid just below the configured top, and checks a 5×5 grid including the perimeter before suppressing a fallback cap. + +The ledges are real upward-facing, non-skirt terrain triangles from the lower SurfaceWorld stratum visible below the FloatingIslands stratum. The combined recipe log identifies the SurfaceWorld field as strate index 1, chunks −16…−79, built from the owner surface parameters and biome map; the FloatingIslands plan is index 0, chunks 0…−15. At the worst LOD1→2 sample, the fine mesh is at z=−60.497 m and the coarse mesh at z=−61.996 m (149.862 cm apart). The LOD2→3 maximum is 773.318 cm, at z=−73.080 m versus −65.347 m. + +The ring probe excludes skirts and masked triangles. The far-ground refinement routine only runs at LOD4 and above, so it cannot cause the LOD1→2 or LOD2→3 steps. Density dispatch is coordinate-based rather than LOD-based; the meshes sample that field on progressively coarser grids (0.5 m, 1 m, and 2 m at LOD1–3). The evidence points to resolution-dependent surface extraction of the lower SurfaceWorld terrain near the strate transition, not a different LOD recipe or a ceiling triangle. The L13 route audit also found no page/native or exact-query mismatches. The owner’s lower-stratum reading is supported. + +SurfaceWorld at its L1 spawn has small L1→2 and L2→3 median differences, but it has larger outliers at farther rings: L3→4 reaches 1,190.974 cm, and L4→5 reaches 491.570 cm. Those high far-ring values are in the table below; the matched spawn, horizon, and above-ring captures are byte-identical before and after. + +I did not change the ledge geometry. The most direct fix needs to change SurfaceWorld LOD2, which your rule protects pending an owner decision. The pending question is whether to allow that change. + +The shared Core fix changes the recipe output identity version from `farHorizonVersion` 3 to 4. It changes no density query, stratum position, scale, or async route. No Unreal files were edited. + +## Ring tables + +Values are median / p95 / maximum absolute edge-height difference in cm, then the probe's no-hit edge count. `Before → after` is identical for every row because this round changed the ceiling path, not the lower upward-facing terrain. + +### FloatingIslands camera + +| LOD edge | Edges / samples | Before (cm; gaps) | After (cm; gaps) | +|---|---:|---:|---:| +| 0→1 | 0 / 0 | — | — | +| 1→2 | 33 / 231 | 147.141 / 148.403 / 149.862; 0 | 147.141 / 148.403 / 149.862; 0 | +| 2→3 | 49 / 308 | 128.603 / 200.789 / 773.318; 8 | 128.603 / 200.789 / 773.318; 8 | +| 3→4 | 64 / 277 | 3.269 / 47.774 / 188.531; 27 | 3.269 / 47.774 / 188.531; 27 | +| 4→5 | 72 / 544 | 29.992 / 124.373 / 443.482; 6 | 29.992 / 124.373 / 443.482; 6 | +| 5→6 | 64 / 266 | 12.234 / 127.224 / 536.573; 32 | 12.234 / 127.224 / 536.573; 32 | +| 6→7 | 32 / 264 | 29.013 / 301.752 / 42,624.479; 0 | 29.013 / 301.752 / 42,624.479; 0 | +| 7→8 | 64 / 275 | 83.843 / 1,225.416 / 3,755.407; 32 | 83.843 / 1,225.416 / 3,755.407; 32 | + +### SurfaceWorld L1 spawn camera + +| LOD edge | Edges / samples | Before (cm; gaps) | After (cm; gaps) | +|---|---:|---:|---:| +| 0→1 | 46 / 288 | 0.178 / 0.594 / 1.171; 10 | 0.178 / 0.594 / 1.171; 10 | +| 1→2 | 32 / 288 | 1.419 / 4.990 / 10.438; 0 | 1.419 / 4.990 / 10.438; 0 | +| 2→3 | 34 / 256 | 2.179 / 12.648 / 35.355; 2 | 2.179 / 12.648 / 35.355; 2 | +| 3→4 | 40 / 265 | 5.099 / 35.999 / 1,190.974; 5 | 5.099 / 35.999 / 1,190.974; 5 | +| 4→5 | 58 / 270 | 0.235 / 3.694 / 491.570; 14 | 0.235 / 3.694 / 491.570; 14 | +| 5→6 | 65 / 266 | 12.234 / 127.224 / 536.573; 33 | 12.234 / 127.224 / 536.573; 33 | +| 6→7 | 41 / 269 | 28.238 / 291.861 / 945.477; 7 | 28.238 / 291.861 / 945.477; 7 | +| 7→8 | 32 / 275 | 83.843 / 1,225.416 / 3,755.407; 0 | 83.843 / 1,225.416 / 3,755.407; 0 | + +## Ceiling continuity + +The strict audit casts 16×16 interior rays per tile and pairs every shared same-LOD edge sample. Before the fix it found three LOD1 interior misses, one LOD2 interior miss, and 10 unpaired LOD1 perimeter samples. After the fix it found zero misses and zero edge gaps at all LODs. + +| LOD | Cap tiles | Interior hits | Paired edge samples | Max root delta (cm) | Misses / gaps | +|---:|---:|---:|---:|---:|---:| +| 0 | 64/64 | 16,384/16,384 | 3,696/3,696 | 0 | 0 / 0 | +| 1 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0 | 0 / 0 | +| 2 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0.025 | 0 / 0 | +| 3 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0.025 | 0 / 0 | +| 4 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0 | 0 / 0 | +| 5 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0 | 0 / 0 | +| 6 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0 | 0 / 0 | +| 7 | 48/48 | 12,288/12,288 | 2,376/2,376 | 0 | 0 / 0 | +| 8 | 153/153 | 39,168/39,168 | 8,976/8,976 | 0 | 0 / 0 | + +Full output is in `Saved/Linux/l19-diagnostics/fi_roof_after.log`; the pre-fix strict audit is `Saved/Linux/l19-diagnostics/baseline-fi-ceiling-strict.log`. + +## Hashes and audits + +Clang 19 (`-ffp-contract=off`) and GCC 14 (default) passed 28/28 runs: FI and SW, W1/W8/W12, with W12 repeated five times. The 24 m crossing passed 14/14 runs, with 12 matching hash rows each; its W1/W8/W12×5 hashes also matched the L14 compiler-specific references. The protected SurfaceWorld hash gate passed across the matrix and crossing. SurfaceWorld LOD0–2, full-detail, and exact-query hashes match L14 across the matrix and crossing; the representative W12 LOD3 hash also matches. LOD4–8 mesh/payload/topology hashes include the version-4 salt, and the tested captures show no visible SW change. + +Representative Clang W12 hashes (density / mesh / payload / topology): + +| Output | Full detail | Whole view | +|---|---|---| +| FloatingIslands | `43034299d33f2536 / a41e8353ec4bbe9c / cd0e3e15eef827b0 / 0b3c3f13cc1d6f8a` | `00bc3607a7839854 / 4baa66127c2b0892 / e11ef05e851a1517 / 1278bc4b1fffb869` | +| SurfaceWorld | `0ee0579bd15cbbe4 / 32e5d4964d610fee / 1932b96ee4b1d910 / 69743c6bad3447dc` | `c10697a8f48380c7 / 4197fc1072f0ff58 / 7b3e616ec9459851 / c879e35592bf33e5` | + +SurfaceWorld LOD0–2 hashes (density / mesh / payload / topology): + +| LOD | Hash | +|---:|---| +| 0 | `769f2a706d585a72 / 33eeeb39fa443816 / 645621a1d968983f / c2457ed905036c9d` | +| 1 | `69cc5a5c0a2c60af / b48335f5410c57d5 / d15c5480162ab8ba / feca1bd025823604` | +| 2 | `85deed5683a77202 / 53469895987aeea2 / e55f8ae7e1532033 / d5feb12a0e85c09b` | + +Exact queries: FI density/unclamped `87e5c0dbf9ee9ee2`; SW density/unclamped `e4d4a8eb95b35933`; both detail `efcba20c0c70c415`, collision `e604823a249029bf`. + +L13 route parity passed in all four audit runs (both recipes, both compilers), with zero mismatches. FI checked 782,026 exact-query samples; SW checked 910,651. L9 proof audits reported zero violations: FI 9,192 blocks / 12,234,552 samples; SW 27,479 / 36,574,549 samples. Full logs are in `Saved/Linux/l19-audits/`. + +L3 FP-contract preamble/self-test, L4 `FLazy`, L7 shared cache, L9/L12 proofs, L11 readiness and crash handling, L13 route binding, the L18 shared ceiling, `ARCHITECTURE.md` §8.10, and the CLAUDE.md async rules remain intact. The only source edits are in shared Core; the only architecture addition is §8.38. + +## Captures + +Every JPEG is at most 50 KiB (limit 300 KB). `floating-islands-owner-ground-after.jpg` is the low view across the FI ground, matching the owner's angle. SurfaceWorld spawn, horizon, and above-ring before/after JPEGs are byte-identical. + +- FI: `captures/floating-islands-flat-square-before.jpg`, `captures/floating-islands-flat-square-after.jpg`, `captures/floating-islands-owner-angle-before.jpg`, `captures/floating-islands-owner-angle-after.jpg`, `captures/floating-islands-owner-ground-before.jpg`, `captures/floating-islands-owner-ground-after.jpg`, `captures/floating-islands-ceiling-up-after.jpg`. +- SW: `captures/surface-world-l1-spawn-before.jpg`, `captures/surface-world-l1-spawn-after.jpg`, `captures/surface-world-horizon-before.jpg`, `captures/surface-world-horizon-after.jpg`, `captures/surface-world-rings-above-before.jpg`, `captures/surface-world-rings-above-after.jpg`. + +## Failed and retried runs + +- The pre-fix strict FI ceiling audit failed as expected: 4 interior misses and 10 unpaired LOD1 edge samples. The earlier loose report had counted only the 3 LOD1 interior misses. The final strict audit passed. +- A ceiling audit was mistakenly run on the ordinary SW terrain export; it reported 47,837 non-roof ray misses. That was the wrong target for this audit and was discarded. +- The first continuity-audit attempt was interrupted before results. `/usr/bin/time` was unavailable (exit 127); the repository `MeasureBuild.py` wrapper was used instead. +- The first FI auto-camera export used clip radius 0 / max LOD 0 and exited with `std::bad_alloc` (1.710 s, peak RSS 2,151,292 KiB). Retrying at radius 3 / LOD8 / W12 succeeded (10.786 s, peak RSS 597,976 KiB). +- An exploratory native-heightfield refinement extension did not change the FI L1→2 or L2→3 ring values and was reverted. A temporary debug probe/build used to inspect the ceiling field was also removed before the final build. +- The first hash-gate run happened before crossing logs existed and failed for zero crossing rows. After the 24 m crossing completed, the gate passed. +- The first SW ring probe did not inherit the CPU affinity limit; it was rerun under `taskset -c 0-11`, and the constrained table is unchanged. +- `renice -n 0` was denied by the environment. Runs stayed at inherited nice level 5 (below normal priority); the governor was left unchanged at `powersave`. + +No build or run exceeded 15 minutes or 30 minutes. All final builds and run children used CPUs 0–11, at most 12 workers, `LP_NUM_THREADS=12` for llvmpipe, and the 6 GiB address-space cap. Peak RSS was 903,324 KiB (GCC build); the matrix, crossing, audits, roof audit, and captures peaked at 583,180, 550,832, 857,964, 541,972, and 510,680 KiB respectively. The outer matrix took 343.684 s; the crossing took 46.354 s. Governor: `powersave`. + +**Owner note:** The FI ceiling now closes cleanly at z=+4 m. The visible ledges are lower SurfaceWorld terrain and are a real LOD sampling mismatch, not skirts. SurfaceWorld L1→2 and L2→3 agree closely at the spawn, but its far rings have larger outliers. The ledge fix needs permission to change SurfaceWorld LOD2; no such mesh change is included here. Nothing was committed, pushed, or stashed. + +## Orchestrator review (Claude) + +- Merged. The ceiling specks seen in L18's flat-square capture are gone in the L19 capture (checked by me), + and the strict audit reads 0 misses / 0 gaps at all LODs. SurfaceWorld LOD0–2, full-detail and exact + queries are unchanged; the far mesh hashes carry the `farHorizonVersion` 3→4 salt, with byte-identical + captures. +- Luna correctly stopped at the protected SurfaceWorld LOD2 rule. Ledge decision: the defect is at the + strate interface (lower stratum top near z≈−60…−73 m, sampled on 0.5/1/2 m grids at LOD1–3), which is + where the two chunk-unit conventions meet. It goes into L20 (the units/layers design round) as its + concrete test case, rather than being patched alone first. diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/RunAuditsL19.py b/Standalone/Research/WorldAssemblerM3/Linux19/RunAuditsL19.py new file mode 100644 index 0000000..b9e41b4 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/RunAuditsL19.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Run the L9 uniform-block proof and L13 route parity audits.""" +import os +import signal +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l19-audits" +OUT.mkdir(parents=True, exist_ok=True) +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") +BINARIES = { + "clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-clang", + "gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-gcc", +} +RECIPES = { + "floating-islands": ("-22,-19,-22.8,0,-4", "--recipe=floating-islands"), + "surface-world": ("0,-35,-487.94,0,-22", "--recipe=surface-world"), +} +passed = True +for recipe, (camera, recipe_flag) in RECIPES.items(): + for compiler, binary in BINARIES.items(): + name = f"{recipe}-{compiler}" + logfile = OUT / f"{name}.log" + command = ["prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8", + "--workers=12", f"--camera={camera}", recipe_flag, + "--audit-uniform-block-proofs", "--audit-route-parity"] + started = time.monotonic() + peak_rss = 0 + timed_out = False + with logfile.open("w", encoding="utf-8") as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + start_new_session=True) + while process.poll() is None: + pending, seen = [process.pid], set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + pending.extend(int(value) for value in + Path(f"/proc/{pid}/task/{pid}/children").read_text().split()) + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + peak_rss = max(peak_rss, int(line.split()[1])) + break + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + pass + if time.monotonic() - started > 900: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + timed_out = True + passed = False + break + time.sleep(0.1) + code = process.wait() + elapsed = time.monotonic() - started + log = logfile.read_text(encoding="utf-8", errors="replace") + route_lines = [line for line in log.splitlines() if line.startswith("[RouteParityAudit]")] + proof_lines = [line for line in log.splitlines() if line.startswith("[UniformBlockProofAudit]")] + route_ok = bool(route_lines) and route_lines[-1].endswith("status=PASS") + proof_ok = bool(proof_lines) and "violations=0" in proof_lines[-1] + ok = code == 0 and not timed_out and route_ok and proof_ok + passed &= ok + print(f"[AuditRun] {name} exit={code} seconds={elapsed:.3f} peak_rss_kib={peak_rss} " + f"route={route_ok} proofs={proof_ok} status={'PASS' if ok else 'FAIL'}", flush=True) +print(f"[AuditSummary] rows={len(RECIPES)*len(BINARIES)} expected=4 " + f"result={'PASS' if passed else 'FAIL'} path={OUT}", flush=True) +raise SystemExit(0 if passed else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/RunCrossingL19.py b/Standalone/Research/WorldAssemblerM3/Linux19/RunCrossingL19.py new file mode 100644 index 0000000..fad74fb --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/RunCrossingL19.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Run the L19 24 m SurfaceWorld crossing hash matrix.""" +import os +import re +import signal +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l19-crossing" +OUT.mkdir(parents=True, exist_ok=True) +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") +BINARIES = { + "clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-clang", + "gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-gcc", +} +PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +RUNS = [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)] +passed = True +for compiler, binary in BINARIES.items(): + reference = None + for workers, repeat in RUNS: + name = f"{compiler}-w{workers}-r{repeat}" + logfile = OUT / f"{name}.log" + command = ["prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", "--max-clip-level=8", + f"--workers={workers}", "--camera=0,-35,-487.94,0,-22", + "--camera-step=24,0,0", "--region-crossing", "--recipe=surface-world"] + started = time.monotonic() + peak_rss = 0 + timed_out = False + with logfile.open("w", encoding="utf-8") as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + start_new_session=True) + while process.poll() is None: + pending, seen = [process.pid], set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + pending.extend(int(value) for value in + Path(f"/proc/{pid}/task/{pid}/children").read_text().split()) + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + peak_rss = max(peak_rss, int(line.split()[1])) + break + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + pass + if time.monotonic() - started > 900: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + timed_out = True + passed = False + break + time.sleep(0.1) + code = process.wait() + elapsed = time.monotonic() - started + hashes = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace")) + ok = code == 0 and not timed_out and len(hashes) == 12 + if reference is None: + reference = hashes + elif hashes != reference: + ok = False + print(f"[CrossingMismatch] {name}", flush=True) + passed &= ok + print(f"[CrossingRun] {name} exit={code} seconds={elapsed:.3f} " + f"peak_rss_kib={peak_rss} hash_rows={len(hashes)} " + f"status={'PASS' if ok else 'FAIL'}", flush=True) +print(f"[CrossingSummary] rows={len(BINARIES)*len(RUNS)} expected=14 " + f"result={'PASS' if passed else 'FAIL'} path={OUT}", flush=True) +raise SystemExit(0 if passed else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/RunMatrixL19.py b/Standalone/Research/WorldAssemblerM3/Linux19/RunMatrixL19.py new file mode 100644 index 0000000..544cb60 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux19/RunMatrixL19.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Run the L19 W1/W8/W12 x5 matrix under inherited taskset limits.""" +import os +import re +import signal +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l19-matrix" +OUT.mkdir(parents=True, exist_ok=True) +if not os.sched_getaffinity(0).issubset(set(range(12))): + raise SystemExit("run under taskset -c 0-11") +BINARIES = { + "clang-off": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-clang", + "gcc-default": ROOT / "Standalone/bin/linux/VoxelForgeCoreHeadless-linux19-gcc", +} +RECIPES = { + "floating-islands": "-22,-19,-22.8,0,-4", + "surface-world": "0,-35,-487.94,0,-22", +} +PATTERN = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M) +RUNS = [(1, 1), (8, 1), (12, 1), (12, 2), (12, 3), (12, 4), (12, 5)] +passed = True +for recipe, camera in RECIPES.items(): + for compiler, binary in BINARIES.items(): + if not binary.exists(): + raise SystemExit(f"missing binary: {binary}") + reference = None + for workers, repeat in RUNS: + name = f"{recipe}-{compiler}-w{workers}-r{repeat}" + logfile = OUT / f"{name}.log" + command = ["prlimit", "--as=6442450944", "--", str(binary), + str(ROOT / "Standalone/data/owner_world.json"), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--stream-hash", "--force-generate", "--clip-radius=3", + "--max-clip-level=8", f"--workers={workers}", f"--camera={camera}", + f"--recipe={recipe}"] + started = time.monotonic() + peak_rss = 0 + timed_out = False + with logfile.open("w", encoding="utf-8") as stream: + process = subprocess.Popen(command, stdout=stream, stderr=subprocess.STDOUT, + start_new_session=True) + while process.poll() is None: + pending, seen = [process.pid], set() + while pending: + pid = pending.pop() + if pid in seen: + continue + seen.add(pid) + try: + pending.extend(int(value) for value in + Path(f"/proc/{pid}/task/{pid}/children").read_text().split()) + for line in Path(f"/proc/{pid}/status").read_text().splitlines(): + if line.startswith("VmRSS:"): + peak_rss = max(peak_rss, int(line.split()[1])) + break + except (FileNotFoundError, PermissionError, ProcessLookupError, ValueError): + pass + if time.monotonic() - started > 900: + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait() + timed_out = True + passed = False + break + time.sleep(0.1) + code = process.wait() + elapsed = time.monotonic() - started + hashes = PATTERN.findall(logfile.read_text(encoding="utf-8", errors="replace")) + ok = code == 0 and not timed_out and len(hashes) == 12 + if reference is None: + reference = hashes + elif hashes != reference: + ok = False + print(f"[MatrixMismatch] {name}", flush=True) + passed &= ok + print(f"[MatrixRun] {name} exit={code} seconds={elapsed:.3f} " + f"peak_rss_kib={peak_rss} hash_rows={len(hashes)} " + f"status={'PASS' if ok else 'FAIL'}", flush=True) +print(f"[MatrixSummary] rows={len(RECIPES)*len(BINARIES)*len(RUNS)} expected=28 " + f"result={'PASS' if passed else 'FAIL'} path={OUT}", flush=True) +raise SystemExit(0 if passed else 1) diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-ceiling-up-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-ceiling-up-after.jpg new file mode 100644 index 0000000..85e1439 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-ceiling-up-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-after.jpg new file mode 100644 index 0000000..b13ba5d Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-before.jpg new file mode 100644 index 0000000..a88fef2 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-flat-square-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-after.jpg new file mode 100644 index 0000000..643b6dd Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-before.jpg new file mode 100644 index 0000000..c42dcd3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-angle-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-after.jpg new file mode 100644 index 0000000..ced1214 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-before.jpg new file mode 100644 index 0000000..0fe6153 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/floating-islands-owner-ground-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-after.jpg new file mode 100644 index 0000000..896ba4e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-before.jpg new file mode 100644 index 0000000..896ba4e Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-horizon-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-after.jpg new file mode 100644 index 0000000..bffdac3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-before.jpg new file mode 100644 index 0000000..bffdac3 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-l1-spawn-before.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-after.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-after.jpg new file mode 100644 index 0000000..d48cd2f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-after.jpg differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-before.jpg b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-before.jpg new file mode 100644 index 0000000..d48cd2f Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux19/captures/surface-world-rings-above-before.jpg differ