feat(core): profiled heightfields, slice 1: periodicHills, mirroredRelief, flatField (L53); research R22
WorldAssemblySurfaceProfileV1 on the native SurfaceWorld recipe and plan; version 0 'natural' is omitted from serialized identity, so the owner world is unchanged. periodicHills (seeded cosine hills, plan-time grade <= 15 deg), mirroredRelief (cap mirrors the floor by lambda, clearance checked) and flatField share one profile function for points, lattice classification and exact cosine-extrema intervals (0 interval misses in brute-force audits). Style-override fixtures with planned passages. Cold whole view about 1.33 s for each profile vs 1.74 s natural SW (Clang). Owner hashes identical to L52; fast/medium/full PASS. Research R22: SurfaceWorld critical path to about 1 s. 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
408c14baff
commit
20f6c98ec5
17 files changed
+1620
-23
No files matched your search
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the three named style-override inputs for the profiled heightfield slice."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = ROOT / "Standalone/data/owner_world.json"
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l53"))
|
||||
PROFILES = {
|
||||
"periodicHills": {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 1.5,
|
||||
"amplitude_y_m": 1.5,
|
||||
"wavelength_x_m": 96.0,
|
||||
"wavelength_y_m": 96.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
},
|
||||
"mirroredRelief": {
|
||||
"version": 1,
|
||||
"kind": "mirroredRelief",
|
||||
"amplitude_x_m": 1.5,
|
||||
"amplitude_y_m": 1.5,
|
||||
"wavelength_x_m": 128.0,
|
||||
"wavelength_y_m": 128.0,
|
||||
"mirror_lambda": 0.875,
|
||||
"minimum_arena_clearance_m": 8.0,
|
||||
},
|
||||
"flatField": {
|
||||
"version": 1,
|
||||
"kind": "flatField",
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT / "fixtures")
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
base = json.loads(OWNER.read_text(encoding="utf-8"))
|
||||
for name, override in PROFILES.items():
|
||||
fixture = dict(base)
|
||||
fixture["surface_profile_override"] = override
|
||||
path = args.out / f"style-override-profile-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L53ProfileFixture] name={path.stem} profile={name} path={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture eye-height and overview views for the three L53 heightfield profiles."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l53"))
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
PROFILES = ("periodicHills", "mirroredRelief", "flatField")
|
||||
Q_PER_METRE = 1024.0
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values: tuple[float, ...]) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def route_frame(manifest_path: Path) -> tuple[tuple[float, float, float],
|
||||
tuple[float, float, float]]:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
# Core profile coordinates are 1/256 of a Core voxel. In this world that is 1024 q/m.
|
||||
scale = 1.0 / Q_PER_METRE
|
||||
candidates = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not candidates:
|
||||
raise RuntimeError(f"no planned route points in {manifest_path}")
|
||||
route = candidates[0]["route"]
|
||||
points = [tuple(float(value) * scale for value in item["position_q"])
|
||||
for item in route]
|
||||
lengths = [math.dist(a, b) for a, b in zip(points, points[1:])]
|
||||
total = sum(lengths)
|
||||
if total <= 1.0e-6:
|
||||
raise RuntimeError(f"degenerate planned route in {manifest_path}")
|
||||
|
||||
def at(distance: float) -> tuple[float, float, float]:
|
||||
left = max(0.0, min(total, distance))
|
||||
for a, b, length in zip(points, points[1:], lengths):
|
||||
if left <= length or (a, b) == (points[-2], points[-1]):
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = left / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
left -= length
|
||||
return points[-1]
|
||||
|
||||
center = at(total * 0.5)
|
||||
before, after = at(total * 0.5 - 1.0), at(total * 0.5 + 1.0)
|
||||
dx, dy = after[0] - before[0], after[1] - before[1]
|
||||
length = math.hypot(dx, dy)
|
||||
if length <= 1.0e-9:
|
||||
raise RuntimeError(f"route tangent is degenerate in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def run_step(name: str, command: list[str], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False) -> None:
|
||||
expected = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(command[0])
|
||||
probe = bounded_run([str(binary), "--print-source-stamp"], timeout=15)
|
||||
if probe is None or probe.returncode != 0 or first_stamp(probe.stdout or "") != expected:
|
||||
raise RuntimeError(f"{name} refused an unstamped or stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, "timeout", "1740s", *map(str, command)],
|
||||
cwd=ROOT, env=env, stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=1745, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"[L53CaptureRun] step={name} elapsed={elapsed:.1f}s exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
if stamped and (first_stamp(output) != expected or current_stamp() != expected):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if name.endswith("-quality"):
|
||||
for line in output.splitlines():
|
||||
if line.startswith("[CaptureQuality]"):
|
||||
print(line, flush=True)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
|
||||
|
||||
def camera_angles(camera: tuple[float, float, float],
|
||||
target: tuple[float, float, float]) -> tuple[float, float]:
|
||||
dx, dy, dz = (target[index] - camera[index] for index in range(3))
|
||||
return math.degrees(math.atan2(dx, dy)), math.degrees(math.atan2(dz, math.hypot(dx, dy)))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--fixture-dir", type=Path)
|
||||
parser.add_argument("--artifact-dir", type=Path,
|
||||
default=ROOT / "Standalone/Research/WorldAssemblerM3/Linux53/captures")
|
||||
parser.add_argument("--headless", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
out = args.out.resolve()
|
||||
fixture_dir = (args.fixture_dir or out / "fixtures").resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
png_dir = out / "capture-png"
|
||||
mesh_dir = out / "profile-meshes"
|
||||
for directory in (out, png_dir, mesh_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")
|
||||
|
||||
for profile in PROFILES:
|
||||
fixture = fixture_dir / f"style-override-profile-{profile}.json"
|
||||
manifest = fixture_dir / f"style-override-profile-{profile}-manifest.json"
|
||||
if not fixture.is_file() or not manifest.is_file():
|
||||
raise RuntimeError(f"missing profile fixture or manifest for {profile}")
|
||||
center, tangent = route_frame(manifest)
|
||||
cx, cy, cz = center
|
||||
tx, ty, _ = tangent
|
||||
eye = (cx, cy, cz + 0.72)
|
||||
eye_target = (cx + tx * 15.0, cy + ty * 15.0, cz + 0.72)
|
||||
overview_camera = (cx - tx * 48.0 - ty * 25.0,
|
||||
cy - ty * 48.0 + tx * 25.0, cz + 43.0)
|
||||
overview_target = (cx + tx * 22.0, cy + ty * 22.0, cz + 0.4)
|
||||
|
||||
for view, camera, target, clip_radius in (
|
||||
("eye", eye, eye_target, 3),
|
||||
("overview", overview_camera, overview_target, 6),
|
||||
):
|
||||
stem = f"{profile}-{view}"
|
||||
mesh = mesh_dir / f"{stem}.vfmesh"
|
||||
png = png_dir / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
camera_spec = (*camera, yaw, pitch)
|
||||
headless_command = [args.headless, fixture, "--workers=12",
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
"--max-clip-level=0", "--max-level=0", "--render-distance-chunks=0",
|
||||
"--recipe=surface-world", f"--camera={fmt(camera_spec)}",
|
||||
f"--mesh-export={mesh}",
|
||||
f"--world-assembler-manifest-output={manifest}"]
|
||||
run_step(f"{stem}-export", headless_command,
|
||||
out / f"capture-{stem}-export.log", env=env, stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"headless export produced no mesh for {stem}")
|
||||
focus = (*target, 54.0 if view == "overview" else 26.0)
|
||||
renderer_command = [args.renderer, mesh, png, "--size=1600x900",
|
||||
"--fov-deg=72", f"--camera-m={fmt(camera)}", f"--yaw-deg={yaw:.6f}",
|
||||
f"--pitch-deg={pitch:.6f}", "--tile-skirts=off", "--shade=readable",
|
||||
"--headlamp=on", "--height-tint=on", f"--height-center-m={cz:.6f}",
|
||||
"--height-range-m=10", "--tile-edges=off", f"--focus-region={fmt(focus)}"]
|
||||
run_step(f"{stem}-render", renderer_command,
|
||||
out / f"capture-{stem}-render.log", env=env, stamped=True)
|
||||
run_step(f"{stem}-jpeg", ["ffmpeg", "-nostdin", "-hide_banner",
|
||||
"-loglevel", "error", "-y", "-i", png, "-frames:v", "1", "-q:v", "4", image],
|
||||
out / f"capture-{stem}-jpeg.log", env=env)
|
||||
quality = ROOT / "Standalone/Tools/verify/capture_quality.py"
|
||||
run_step(f"{stem}-quality", [sys.executable, quality, image],
|
||||
out / f"capture-{stem}-quality.log", env=env)
|
||||
if not image.is_file() or not 0 < image.stat().st_size <= 300_000:
|
||||
raise RuntimeError(f"capture size is invalid for {stem}")
|
||||
print(f"[L53HeightfieldCapture] profile={profile} view={view} file={image} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) "
|
||||
f"bytes={image.stat().st_size} status=PASS", flush=True)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L53HeightfieldCapture] status=FAIL error={error}", file=sys.stderr, flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,53 @@
|
||||
# Linux 53 — profiled heightfields, slice 1
|
||||
|
||||
## Profile API
|
||||
|
||||
`WorldAssemblySurfaceProfileV1` is attached to the native SurfaceWorld recipe and stratum plan. Version 0 is the all-zero `natural` profile; it is omitted from serialized identity, preserving the existing owner world. Non-default profiles use version 1, serialize into recipe and plan JSON, and contribute to canonical hashes. Dimensions use fixed metre-derived subvoxels, and periodic phases are derived deterministically from the recipe seed and axis.
|
||||
|
||||
## Profiles and bounds
|
||||
|
||||
- **natural** retains today's noise heightfield.
|
||||
- **periodicHills** uses `h = h0 + Ax cos(2πx/Lx + φx) + Ay cos(2πy/Ly + φy)`. The fixture uses `Ax=Ay=1.5 m`, `Lx=Ly=96 m`; seeded phases are Q0.32 turns `1258571205` and `82173144`. Its maximum analytic grade is 7.90° (the plan-time limit is 15°). Supported ranges follow R20: amplitudes 0.5–3 m and wavelengths 80–160 m.
|
||||
- **mirroredRelief** uses `g = Ax cos(...) + Ay cos(...)`, floor `h0+g`, and ceiling `c0−λg`. The fixture uses 1.5 m amplitudes, 128 m wavelengths, and `λ=0.875`. R20 ranges are 0.5–2 m, 80–160 m, and `λ=0.75–1`. The plan checks `c0−h0−(1+λ)(Ax+Ay)` against the required clearance; the fixture requests 8 m. Its maximum grade is 5.94°.
|
||||
- **flatField** sets the floor to `h0` and retains the plan's flat cap.
|
||||
|
||||
For each horizontal box, the cosine range checks its endpoints and all included stationary points; spans of a full period use `[-1,1]`. The two axis ranges are combined for floor and mirrored-cap bounds. Fixed-point rounding is padded outward by two subvoxels; flat-field bounds are constant. The same profile function feeds point evaluation, lattice classification, and intervals. Each profile audit checked 96 boxes against 23,328 brute-force density samples: zero interval misses and zero classifier mismatches. Each also certified 24 air and 24 solid boxes; the separate bound audit checked 265 boxes and 7,155 samples. The density sign remains negative for solid and nonnegative for air.
|
||||
|
||||
## Fixtures, routes, and captures
|
||||
|
||||
The fixtures are named `style-override-profile-periodicHills.json`, `style-override-profile-mirroredRelief.json`, and `style-override-profile-flatField.json` under `Saved/Linux/l53/fixtures/`. Each plans an inter-stratum arrival/departure passage and a 32-segment supported route. Profile proofs and passage audits pass, including interface-band and negative-thickness controls. Measured route grades are 7.904°, 5.945°, and 0° respectively; mirrored clearance is at least the requested 8 m.
|
||||
|
||||
Eye-height and overview captures all pass quality gates (6/6):
|
||||
|
||||
- Periodic hills: [eye](captures/periodicHills-eye.jpg), [overview](captures/periodicHills-overview.jpg)
|
||||
- Mirrored relief: [eye](captures/mirroredRelief-eye.jpg), [overview](captures/mirroredRelief-overview.jpg)
|
||||
- Flat field: [eye](captures/flatField-eye.jpg), [overview](captures/flatField-overview.jpg)
|
||||
|
||||
## Timings and hashes
|
||||
|
||||
Cold whole-view readiness, seconds:
|
||||
|
||||
| Compiler | Natural SurfaceWorld | periodicHills | mirroredRelief | flatField |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Clang 19 | 1.742 | 1.336 | 1.323 | 1.330 |
|
||||
| GCC | 1.911 | 1.436 | 1.409 | 1.382 |
|
||||
|
||||
Both compilers were freshly rebuilt from this source stamp with FP contraction off and 12 jobs. Every profile is under the 1.75 s budget; periodic hills and flat field are faster than their natural SurfaceWorld reference. Profile layout IDs are `bf3b90bc4678497e` (periodic), `bd45e0817b6338fa` (mirrored), and `dea9d3b61447f9b1` (flat).
|
||||
|
||||
Floating Islands and natural SurfaceWorld owner hashes each match L52 exactly: 12/12 canonical rows on Clang and GCC at W12. Profile worker hashes match at W1/W8/W12, and seeded random-camera geometry/material hashes match across Clang and GCC. Source stamp: `99389e29037f59841b46b31f16899355b718ba8def14ff73c43f8900dbb75c0b`.
|
||||
|
||||
## Verification and failed runs
|
||||
|
||||
Fast passed in 149.6 s; medium passed in 606.2 s; full passed once in 1,088.3 s. Full included 30-cell determinism and five paired baseline cost measurements. The median candidate changes were +0.87% for SurfaceWorld and +0.84% for Floating Islands.
|
||||
|
||||
Earlier trials exposed a lower-landing capsule-support failure and a transition-audit harness short-circuit; route radius conversion was corrected and transition checks were split. One medium attempt then failed its speed gate because passage audits contaminated `whole_view_ready` (21.666 s). The final tier measures cold views separately without audit flags and passes at the times above. No final tier gates failed.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted as code. The stamp matches the final Clang and GCC builds (`99389e29…`); the owner hashes are identical to
|
||||
L52; profile proofs show 0 misses. **All three profiles run faster than natural SW** (about 1.33 s vs 1.74 s on Clang).
|
||||
**The captures don't show the profiles.** The eye views are a single soft horizon against black: the cap is never
|
||||
visible, so mirrored relief can't be judged. The "periodicHills overview" frames the FI anchor spiral column from the
|
||||
upper strate, which still shows its old shard and terrace artifacts, instead of the hills. The next profile round
|
||||
must frame the hill field and the mirrored ceiling, for example a wide eye-level view with the cap in frame and a
|
||||
high oblique overview over several periods.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,49 @@
|
||||
# R22 — What stands between SurfaceWorld and ~1 s?
|
||||
|
||||
**Metric:** This report treats the target as *all-view CPU tile completion*. In the profiled SW run, the near ring is CPU-ready at 0.524 s and the whole view at 1.572 s. Neither timestamp means the tiles are uploaded, resident, or presented; those need a separate viewer measure.
|
||||
|
||||
## Critical path
|
||||
|
||||
`World::compile` builds the shape world before tile generation ([`World::compile`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1381>), [`FStandaloneShapeWorld::Build`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2208>)). In the headless pipeline, the pool starts with shape-free tiles, then waits for the shape-build future before queueing the full desired set. L52 had **0 of 2,370** shape-free tiles, so no tile work overlapped shape setup ([pipeline barrier](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:1281>)).
|
||||
|
||||
Shape construction is partly parallel: interval, candidate-band, and detail row scans use the host scheduler, which caps helper threads at eight even when the request specifies twelve ([scheduler](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StandaloneCoreScheduler.h:17>)). Sign-edge completion and far-parent/bin construction are serial portions.
|
||||
|
||||
After shape publication, twelve persistent tile workers pull jobs dynamically from a shared priority heap ([worker loop](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:931>)). The queue prioritizes the central 27 LOD0 tiles, then coarser LODs from 8 down to 2, then distant LOD0/1 detail ([priority assignment](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:704>)). That is a dispatch preference, not an LOD completion barrier. Each worker generates one tile at a time through [`World::generateTile`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1960>).
|
||||
|
||||
For SW, the profiled worker sum is 12.745 worker-seconds: a 1.062 s ideal drain on twelve workers, against about 1.065 s from shape completion to the last tile. That indicates an **aggregate work-saturated tail**; the data leaves little room for scheduling or idle-time gains. The last ready group is LOD0/1 detail: `full_detail_ready` and `whole_view_ready` coincide. Queue order can contribute to when those tiles run, but the current trace cannot quantify queue delay or identify a slow final tile. Per-LOD averages do not rule out an outlier. `WholeViewReady` is the maximum CPU `Tile.CompletedAt`, not GPU residency or presentation ([readiness calculation](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:4267>)).
|
||||
|
||||
## Shape setup
|
||||
|
||||
SW’s 0.437 s setup breaks down to about 36 ms local shapes, 78 ms intervals and signs, 259 ms surface band, and 54 ms far parents. The surface band includes about 241 ms of parallel detail application and 18 ms of serial sign-edge completion. FI’s 0.887 s setup has a similar interval stage, but its 707 ms surface band includes about 479 ms of serial sign-edge repair and 227 ms of detail application. These splits are in the [SW profile](</home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l52/profile/final-surface-world.log:59>) and [FI profile](</home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/l52/profile/final-floating-islands.log:66>).
|
||||
|
||||
The interval stage packs rock runs and passage void crossings into candidate bands; the surface-band stage applies detail and repairs sign-edge distances; far-parent construction builds coarse geometry and spatial bins ([shape stages](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:2246>)). Raising the helper cap from eight to twelve has an ideal ceiling near 0.10 s for SW and 0.09 s for FI. FI’s sign-edge pass has a larger theoretical opportunity, up to about 0.44 s, but needs deterministic parallel handling of shared packed-grid words.
|
||||
|
||||
Regional compilation could help near-ring latency, but `compileTileRegion` requires an already compiled base world ([region API](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1487>)). The viewer has a neighborhood-first/background-full-build scaffold, but current call sites select full-build mode ([viewer build path](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StandaloneViewer.cpp:2583>)). Deferring far parents can move roughly 54 ms for SW or 50 ms for FI off the near-ring path, but does not eliminate work needed for far tiles.
|
||||
|
||||
A warm shape cache could save at most the build time before load/decode cost. `planHash` alone is not a safe key: the two L52 logs share a plan hash but differ in resolved-layout identity. The key must include resolved layout, bounds, build switches, and version. The current shape arrays exceed 337 MiB, and a cache cannot help a cold run.
|
||||
|
||||
## Far LODs and safe skips
|
||||
|
||||
SW LOD6 connector repair costs 1.335 worker-seconds and adds 5,070 triangles; LOD8 costs 2.884 worker-seconds, including 1.348 seconds for its marching-cubes grid. Together LOD6–8 account for about 0.49 s of ideal twelve-worker drain. That is a ceiling for removing all their work, not a safe forecast: connector walls close gaps, and LOD8 contributes to the horizon. The existing screen-space gate checks only the closest native-heightfield tile at each LOD, not every far tile or connector ([gate](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkPipeline.h:4218>)).
|
||||
|
||||
Earlier gates found that doubling connector station spacing introduced holes, while doubling refinement spacing failed image and topology checks ([Linux15 quality gate](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux15/REPORT.md:5>)). LOD8 interior sampling was already reduced to marching-cubes spacing ([Round14 change](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Round14/REPORT.md:7>)). Prefer exact kernel improvements or later dispatch until current-source quality gates prove a coarser setting safe.
|
||||
|
||||
SW’s block proof found 960 unknown boxes and no whole-tile skips; FI’s proof found 216 whole-tile skips, but classification itself cost 22.5 worker-seconds. Also distinguish proof skips from the mesher’s existing Z-band early return, which can yield an empty mesh before density sampling ([Z-band return](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:529>)). L52 does not count those returns per tile.
|
||||
|
||||
## Ranked plan
|
||||
|
||||
1. **Instrument the tile drain first.** This has no direct speed gain, but is required to choose the right optimization. Keep the unprofiled paired medians as the performance score. SW’s median was 1.718 s, so reaching 1 s requires about 0.72 s off; in the profiled decomposition, tile drain would need to fall from about 1.065 s to 0.493 s. Even after the ideal shape scheduler gain, about 5.63 worker-seconds of tile work still need to go.
|
||||
|
||||
2. **Target measured tile kernels.** This is the main path to the SW target. One exact-output candidate is skipping skirt faces that the viewer masks as hidden; the absolute ceiling is about 0.15 s at twelve-way ideal drain, and the real saving will be smaller. FI needs a much larger reduction: roughly 32–38 worker-seconds of its 48.3 worker-seconds, depending on shape improvements. LOD0 alone costs 28.6 worker-seconds, so far-tier work cannot get FI near 2 s.
|
||||
|
||||
3. **Reduce serial shape cost where measured.** Test the helper cap increase and, for FI, a deterministic parallel sign-edge repair. The ceilings are about 0.10 s for SW and about 0.53 s for FI combined. These are useful gains, but insufficient alone.
|
||||
|
||||
4. **Optimize first playable content separately.** Neighborhood-first shape setup and deferred far parents may improve the near ring or first presentation, not all-view completion. Measure CPU ring-ready, upload/resident, and first-presented times separately.
|
||||
|
||||
5. **Consider a warm shape cache only for repeated runs.** It can save up to 0.44 s SW or 0.89 s FI before cache I/O, but provides no cold-run gain.
|
||||
|
||||
## Instrumentation spec
|
||||
|
||||
`CpuTileTimeline` already supports queued, started, and finished events, but L52’s headless logs do not include them ([timeline hooks](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:451>)). Enable it in the headless path and add, per event, one monotonic timestamp, tile key and LOD, enqueue rank/tier, worker ID, queue depth, and active-worker count. Derive queue wait (`dispatch − enqueue`) and service time (`finish − dispatch`), then report p50/p95/max by LOD and priority tier, plus the last 20 completions and worker idle intervals while the queue is nonempty.
|
||||
|
||||
Add per-tile stage spans for classification, field/column sampling, marching-cubes work, connector repair, ground/cap, skirts, and mesh assembly; count Z-band rejects separately from proof skips. Buffer events per worker and dump after the run so logging does not sit inside the queue lock. In the viewer, pair the CPU trace with upload, residency, and first-useful-present events; those are separate from CPU whole-view readiness.
|
||||
Reference in new issue
Block a user