core: stamp passage route slabs into the tile cache (L125, evaluator v225)
Conservative route slabs with an outside sentinel, tight expanded segment bounds; DensityGrid exact fallbacks 479k -> 0 per run. Near-ready 2-14% faster than L119, still +4.7..+10.2% vs L109; whole view within -0.5..+8%. Same look (floor offset 7.884 mm, captures checked), seam audit bit-identical, fast tier + walk audits, Clang/GCC W1/W8/W12 determinism green. BVH and 25 cm tolerance trials rejected. Not for merge yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# Linux125 — passage slab stamping
|
||||
|
||||
## Result
|
||||
|
||||
**No-go for the L109 speed bar.** The v225 stamp candidate keeps the L119 10 cm curve tolerance and smooth half-pipe look. It is 4.72–10.19% slower than L109 for near-ready LOD0–2, though 1.96–14.23% faster than L119. Whole-view readiness ranges from 0.47% faster to 8.04% slower than L109.
|
||||
|
||||
The BVH trial was rejected: preparation was 5–6× slower. The final design stamps conservative route slabs with an outside sentinel and evaluates segment geometry over tight expanded bounds. The separate 25 cm tolerance trial was also rejected: it remained 11–23% slower than L109 and its maximum floor-root offset was 8.601 mm.
|
||||
|
||||
## Count first
|
||||
|
||||
The tile cache already stores prepared segment contributions. At LOD0, each stratum still evaluates 3.11–8.49M segment/sample pairs, while the exact one-sample band is 5.16–7.41% of grid samples and pair overlap is 67–73%. Stamping cuts exact fallback work, but preparation remains dominated by segment/sample work.
|
||||
|
||||
| Stratum | LOD0 pairs (M) | Exact band | Pair overlap L119 → stamp | Prepare ms L119 → stamp | Exact fallbacks (k) L119 → stamp | Max tile cache (MiB) | Process RSS MiB L119 → stamp |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| S0 | 3.11 | 5.16% | 67.3 → 67.2% | 246 → 272 | 426 → 73 | 3.93 | 724 → 723 |
|
||||
| S1 | 8.49 | 6.53% | 72.6 → 72.0% | 620 → 700 | 912 → 172 | 7.85 | 580 → 583 |
|
||||
| S2 | 8.30 | 7.41% | 72.6 → 72.0% | 606 → 621 | 892 → 180 | 7.85 | 643 → 641 |
|
||||
| S3 | 5.27 | 5.35% | 67.7 → 67.6% | 443 → 463 | 198 → 73 | 3.93 | 657 → 667 |
|
||||
|
||||
Across strata, mean preparation time by LOD was **LOD0 479→514 ms, LOD1 121→130 ms, LOD2 3.8→4.1 ms**. Exact-band fractions were 6.18%, 4.14%, and 6.16%; pair overlap was about 71%, 67%, and 68% respectively. Per-caller LOD0 exact fallbacks show the main gain: DensityGrid fell from 479k calls per run on average to zero. Remaining callers account for about 124k calls per run, led by skirt refinement (75k), midpoint correction (19k), and skirt extrusion (18k). Off-grid misses remain on the canonical exact path: 96–235k per stratum at LOD0.
|
||||
|
||||
## Timing
|
||||
|
||||
Five interleaved L80 measurements per cell, one warmup per version, all four strata. The final quiet-machine gate was 1-minute load 0.63 and 98.8% CPU idle. Values are medians in seconds; near-ready is LOD0–2.
|
||||
|
||||
| Stratum | Workers | Near L109 / L119 / v225 | Whole view L109 / L119 / v225 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| S0 | 8 | 0.745 / 0.850 / 0.780 | 1.694 / 1.762 / 1.686 |
|
||||
| S0 | 12 | 0.767 / 0.878 / 0.807 | 1.680 / 1.783 / 1.714 |
|
||||
| S1 | 8 | 0.921 / 1.144 / 0.981 | 1.771 / 2.017 / 1.855 |
|
||||
| S1 | 12 | 0.887 / 1.082 / 0.962 | 1.711 / 1.912 / 1.794 |
|
||||
| S2 | 8 | 0.973 / 1.165 / 1.025 | 1.811 / 2.020 / 1.861 |
|
||||
| S2 | 12 | 0.905 / 1.070 / 0.965 | 1.689 / 1.862 / 1.758 |
|
||||
| S3 | 8 | 0.809 / 0.943 / 0.879 | 1.667 / 1.871 / 1.801 |
|
||||
| S3 | 12 | 0.760 / 0.855 / 0.838 | 1.587 / 1.687 / 1.674 |
|
||||
|
||||
## Quality and verification
|
||||
|
||||
- Floor probe: 9 stations × 51 samples; maximum root offset 7.884 mm, matching the L119 probe. Route centerline openness passed for 21,425/21,425 samples; maximum grade was 10.695°.
|
||||
- Fast tier and walk audits: PASS, 264 checks across 80 runs.
|
||||
- Seams: the audit exits nonzero on any shared-sample mismatch. Four adjacent LOD0 tile pairs passed, with 1,225 shared samples per pair (4,900 total), bit-identical and max delta 0.
|
||||
- Determinism: all 18 Clang/GCC W1/W8/W12 runs matched across owner4 surface-world, owner2 surface-world, and owner2 floating-islands. Output changed; evaluator version 225 and v225 canonical baselines were added. Source stamp: `c1ac9c910e8bb26eba5e25f0396a3b68845627b27bf52cf3f951a37c873783f3`.
|
||||
- Captures: 23/23 L119 views rendered at the same poses; 19 pass and 4 low-contrast-visible. [Compare captures side by side](captures/COMPARE.html).
|
||||
- Linux VFHostEditor: PASS, 31/31 actions, 75.24 s total with `-NoUBA -MaxParallelActions=4`. Unreal ran without `prlimit`.
|
||||
|
||||
Logs, counts, probes, hashes, and timings are under `Saved/Linux/l125/`.
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the exact 23 L119 camera views with the L125 candidate."""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
RESEARCH = Path(__file__).resolve().parent
|
||||
CAPTURES = RESEARCH / "captures"
|
||||
SAVED = ROOT / "Saved/Linux/l125/captures"
|
||||
BASELINE_INDEX = (ROOT / "Standalone/Research/WorldAssemblerM3/Linux119"
|
||||
/ "captures/L119/capture-index.json")
|
||||
LAYOUT = ROOT / "Saved/Linux/l125/hashes/owner4-surface-world-w12-clang.layout.json"
|
||||
CLANG = ROOT / os.environ.get("L125_CAPTURE_CLANG",
|
||||
"Saved/Linux/l125/bin/VoxelForgeCoreHeadless-l125-clang")
|
||||
RENDERER = ROOT / os.environ.get("L125_CAPTURE_RENDERER",
|
||||
"Saved/Linux/l125/bin/VoxelForgeOffscreenCapture-l125-clang")
|
||||
HELPER_PATH = ROOT / "Standalone/Research/WorldAssemblerM3/Linux113/RunL113Captures.py"
|
||||
|
||||
SPEC = importlib.util.spec_from_file_location("l113_capture_helpers", HELPER_PATH)
|
||||
if SPEC is None or SPEC.loader is None:
|
||||
raise SystemExit("cannot load the verified L113 capture helpers")
|
||||
HELPERS = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(HELPERS)
|
||||
HELPERS.RESEARCH = RESEARCH
|
||||
HELPERS.CAPTURES = CAPTURES
|
||||
HELPERS.SAVED = SAVED
|
||||
HELPERS.BINARIES = {"L125": CLANG}
|
||||
HELPERS.RENDERERS = {"L125": RENDERER}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (BASELINE_INDEX, LAYOUT, CLANG, RENDERER)):
|
||||
raise SystemExit("missing L119 camera set, L125 layout, or capture binary")
|
||||
baseline = json.loads(BASELINE_INDEX.read_text(encoding="utf-8"))
|
||||
layout = json.loads(LAYOUT.read_text(encoding="utf-8"))
|
||||
rows = []
|
||||
CAPTURES.mkdir(parents=True, exist_ok=True)
|
||||
SAVED.mkdir(parents=True, exist_ok=True)
|
||||
for view in baseline["captures"]:
|
||||
row = HELPERS.capture("L125", view, layout)
|
||||
row["variant"] = "L125-v225"
|
||||
if view["kind"] == "rock-floor-close":
|
||||
scene = ROOT / row["scene"]
|
||||
anchor = HELPERS.nearest_surface_anchor(scene, view["target"], margin_m=5.0,
|
||||
preferred_normal=(0.0, 0.0, 1.0), minimum_alignment=0.85)
|
||||
alignment = HELPERS.dot(anchor["normal"], (0.0, 0.0, 1.0))
|
||||
camera_distance = math.dist(row["camera_actual_m"], anchor["position_m"])
|
||||
row.update({"floor_anchor_position_m": anchor["position_m"],
|
||||
"floor_anchor_normal": anchor["normal"],
|
||||
"closeup_camera_to_floor_distance_m": camera_distance,
|
||||
"closeup_floor_normal_alignment": alignment,
|
||||
"verified_upward_floor_normal": alignment >= 0.85})
|
||||
if (not row["verified"] or not 2.0 <= camera_distance <= 3.2
|
||||
or anchor["distance_m"] > 0.25 or alignment < 0.85
|
||||
or row["drawn_triangles"] <= 0
|
||||
or row["pixel_quality_status"] not in
|
||||
("pass", "low_contrast_geometry_visible")):
|
||||
raise RuntimeError(f"L125 floor close-up failed at {view['name']}")
|
||||
rows.append(row)
|
||||
if len(rows) != len(baseline["captures"]) or len(rows) != 23:
|
||||
raise RuntimeError(f"expected all 23 L119 views, captured {len(rows)}")
|
||||
index = CAPTURES / "L125" / "capture-index.json"
|
||||
index.parent.mkdir(parents=True, exist_ok=True)
|
||||
index.write_text(json.dumps({"captures": rows}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
|
||||
html = ["<!doctype html><meta charset='utf-8'>",
|
||||
"<title>L119 / L125 tunnel capture comparison</title>",
|
||||
"<style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}"
|
||||
"section{margin:28px 0}h2{font-size:18px}div{display:grid;"
|
||||
"grid-template-columns:1fr 1fr;gap:12px}img{width:100%;height:auto}"
|
||||
"p{color:#bbb}</style>",
|
||||
"<h1>L119 / L125 v225: identical views</h1>"]
|
||||
old_by_name = {Path(row["capture"]).name: row for row in baseline["captures"]}
|
||||
for row in rows:
|
||||
name = Path(row["capture"]).name
|
||||
old = old_by_name[name]
|
||||
old_path = os.path.relpath(ROOT / "Standalone/Research/WorldAssemblerM3/Linux119"
|
||||
/ old["capture"], CAPTURES)
|
||||
new_path = os.path.relpath(RESEARCH / row["capture"], CAPTURES)
|
||||
html.extend(["<section>", f"<h2>{row['passage']} — {row['station']}</h2>",
|
||||
f"<p>{row['kind']} · L119 {old['pixel_quality_status']} · "
|
||||
f"L125 {row['pixel_quality_status']}</p>", "<div>",
|
||||
f"<figure><figcaption>L119</figcaption><img src='{old_path}'></figure>",
|
||||
f"<figure><figcaption>L125 v225</figcaption><img src='{new_path}'></figure>",
|
||||
"</div>", "</section>"])
|
||||
(CAPTURES / "COMPARE.html").write_text("\n".join(html) + "\n",
|
||||
encoding="utf-8")
|
||||
print(f"[L125Captures] rendered={len(rows)} views index={index.relative_to(ROOT)}",
|
||||
flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe reconstructed tunnel floor roots at the nine L119 stations."""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("L125_PROBE_DIR", "Saved/Linux/l125/density-probe")
|
||||
BINARY = ROOT / os.environ.get("L125_PROBE_BINARY",
|
||||
"Saved/Linux/l125/bin/VoxelForgeCoreHeadless-l125-clang")
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
SLOPE = re.compile(
|
||||
r"^\[L115DensitySlopeSample\] passage=(\S+) station=(\S+) s_m=([\d.]+) "
|
||||
r".*?planned_floor_m=(-?[\d.]+) root_floor_m=(-?[\d.]+|nan) "
|
||||
r"residual_m=(-?[\d.]+|nan)", re.M)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
log, layout = OUT / "owner4-clang.log", OUT / "layout.json"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARY), str(DATA), "--world-assembler-prototype",
|
||||
"--stream-benchmark", "--stream-pipeline", "--workers=12", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8",
|
||||
"--camera=8,16,-372.2626953125,0,0", "--audit-resolved-layout-passages",
|
||||
f"--resolved-layout-output={layout}",
|
||||
]
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT,
|
||||
env={**os.environ, "VOXELFORGE_L115_DENSITY_PROFILE": "1"},
|
||||
stdout=stream, stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
if result.returncode:
|
||||
raise SystemExit(f"density probe failed: {log}; exit={result.returncode}")
|
||||
text = log.read_text(encoding="utf-8", errors="replace")
|
||||
groups: dict[tuple[str, str], list[dict]] = {}
|
||||
for passage, station, distance, planned, root, residual in SLOPE.findall(text):
|
||||
root_value = float(root) if root != "nan" else math.nan
|
||||
residual_value = float(residual) if residual != "nan" else math.nan
|
||||
groups.setdefault((passage, station), []).append({
|
||||
"route_s_m": float(distance), "planned_floor_m": float(planned),
|
||||
"root_floor_m": root_value, "offset_m": residual_value})
|
||||
summary = []
|
||||
with (OUT / "zero-crossing-offsets.csv").open("w", newline="", encoding="utf-8") as stream:
|
||||
writer = csv.DictWriter(stream, fieldnames=("passage", "station", "route_s_m",
|
||||
"planned_floor_m", "root_floor_m", "offset_m"))
|
||||
writer.writeheader()
|
||||
for (passage, station), samples in sorted(groups.items()):
|
||||
for sample in samples:
|
||||
writer.writerow({"passage": passage, "station": station, **sample})
|
||||
finite = [sample["offset_m"] for sample in samples
|
||||
if math.isfinite(sample["offset_m"])]
|
||||
summary.append({"passage": passage, "station": station,
|
||||
"samples": len(samples), "finite_roots": len(finite),
|
||||
"max_abs_offset_m": max(map(abs, finite)) if finite else None,
|
||||
"median_abs_offset_m": statistics.median(map(abs, finite))
|
||||
if finite else None,
|
||||
"p95_abs_offset_m": sorted(map(abs, finite))[
|
||||
min(len(finite)-1, math.ceil(0.95*len(finite))-1)]
|
||||
if finite else None})
|
||||
if not summary or any(item["samples"] != 51 for item in summary):
|
||||
raise RuntimeError("density profile is missing 51-sample route stations")
|
||||
(OUT / "zero-crossing-summary.json").write_text(
|
||||
json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L125DensityProbe] exit={result.returncode} stations={len(summary)} "
|
||||
f"log={log.relative_to(ROOT)}", flush=True)
|
||||
return result.returncode
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check Clang/GCC W1/W8/W12 hashes and compare the candidate with L119."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("L125_HASH_DIR", "Saved/Linux/l125/hashes")
|
||||
DATASETS = {
|
||||
"owner4": ROOT / "Standalone/data/owner_world.json",
|
||||
"owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json",
|
||||
}
|
||||
BINARIES = {
|
||||
"clang": ROOT / os.environ.get("L125_HASH_CLANG",
|
||||
"Saved/Linux/l125/bin-stamp10/VoxelForgeCoreHeadless-l125-stamp10-clang"),
|
||||
"gcc": ROOT / os.environ.get("L125_HASH_GCC",
|
||||
"Saved/Linux/l125/bin-stamp10/VoxelForgeCoreHeadless-l125-stamp10-gcc"),
|
||||
}
|
||||
HASH = re.compile(r"^\[Pipeline(?:FullDetailHash|WholeViewHash|LODHash|ExactQueryHash)\].*$", re.M)
|
||||
STAMP = re.compile(r"^\[VoxelForgeSourceStamp\] sha256=(\S+)$", re.M)
|
||||
|
||||
|
||||
def run(dataset: str, recipe: str, workers: int, compiler: str) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"{dataset}-{recipe}-w{workers}-{compiler}"
|
||||
log = OUT / f"{label}.log"
|
||||
layout = OUT / f"{label}.layout.json"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[compiler]), str(DATASETS[dataset]),
|
||||
f"--workers={workers}", "--world-assembler-prototype", "--stream-benchmark",
|
||||
"--stream-pipeline", "--force-generate", "--clip-radius=3",
|
||||
"--max-clip-level=8", "--max-level=8", f"--recipe={recipe}",
|
||||
"--stream-hash", f"--resolved-layout-output={layout}",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=stream,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
hashes = HASH.findall(output)
|
||||
if result.returncode or len(hashes) != 12:
|
||||
raise SystemExit(f"hash run failed or incomplete: {label}; exit={result.returncode} "
|
||||
f"hashes={len(hashes)} log={log}")
|
||||
row = {"dataset": dataset, "recipe": recipe, "workers": workers,
|
||||
"compiler": compiler, "exit_code": result.returncode, "hashes": hashes,
|
||||
"source_stamp": (STAMP.search(output).group(1)
|
||||
if STAMP.search(output) else None),
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L125HashRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if any(not path.is_file() for path in (*DATASETS.values(), *BINARIES.values())):
|
||||
raise SystemExit("missing L125 hash input")
|
||||
previous_path = ROOT / "Saved/Linux/l119/hashes/summary.json"
|
||||
previous_doc = json.loads(previous_path.read_text(encoding="utf-8"))
|
||||
previous = {(row["dataset"], row["recipe"], row["workers"], row["compiler"]): row
|
||||
for row in previous_doc["runs"]}
|
||||
rows = []
|
||||
recipes = {"owner4": ("surface-world",),
|
||||
"owner2": ("surface-world", "floating-islands")}
|
||||
for dataset, dataset_recipes in recipes.items():
|
||||
for recipe in dataset_recipes:
|
||||
for workers in (1, 8, 12):
|
||||
for compiler in ("clang", "gcc"):
|
||||
rows.append(run(dataset, recipe, workers, compiler))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
groups = {}
|
||||
for dataset, dataset_recipes in recipes.items():
|
||||
for recipe in dataset_recipes:
|
||||
key = f"{dataset}/{recipe}"
|
||||
group = [row for row in rows if row["dataset"] == dataset
|
||||
and row["recipe"] == recipe]
|
||||
reference = group[0]["hashes"]
|
||||
stable = all(row["hashes"] == reference for row in group)
|
||||
old = previous[(dataset, recipe, 12, "clang")]["hashes"]
|
||||
groups[key] = {
|
||||
"all_worker_compiler_hashes_match": stable,
|
||||
"matches_l119_v224": next(row for row in group
|
||||
if row["workers"] == 12 and row["compiler"] == "clang")["hashes"] == old,
|
||||
"source_stamps": sorted({row["source_stamp"] for row in group}),
|
||||
"runs": [{"workers": row["workers"], "compiler": row["compiler"],
|
||||
"log": row["log"]} for row in group],
|
||||
}
|
||||
if not stable:
|
||||
raise SystemExit(f"determinism mismatch: {key}")
|
||||
summary = {"runs": rows, "groups": groups}
|
||||
(OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
baseline_dir = ROOT / "Standalone/Tools/verify/baselines/owner-hash/canonical"
|
||||
for dataset, recipe, style, baseline_name in (
|
||||
("owner4", "surface-world", 1,
|
||||
"owner-hash-owner4-surface-world-style1-v225.baseline"),
|
||||
("owner2", "surface-world", 1,
|
||||
"owner-hash-surface-world-style1-v225.baseline"),
|
||||
("owner2", "floating-islands", 2,
|
||||
"owner-hash-floating-islands-style2-v225.baseline"),
|
||||
):
|
||||
canonical = next(row for row in rows if row["dataset"] == dataset
|
||||
and row["recipe"] == recipe and row["workers"] == 12
|
||||
and row["compiler"] == "clang")
|
||||
baseline = baseline_dir / baseline_name
|
||||
baseline.write_text("\n".join(canonical["hashes"]) + "\n",
|
||||
encoding="utf-8")
|
||||
print("[L125HashSummary] " + json.dumps(groups, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measure passage sweep work by stratum and LOD."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("L125_COUNTS_DIR", "Saved/Linux/l125/counts")
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
BINARY = ROOT / os.environ.get("L125_COUNTS_BINARY",
|
||||
"Saved/Linux/l125/bin/VoxelForgeCoreHeadless-l125-clang")
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
CALLERS = (
|
||||
"Unknown", "DensityGrid", "GradientNormalFallback", "MidpointCorrection",
|
||||
"SkirtSurfaceVertex", "SkirtExtrudedVertex", "SkirtClippedVertex",
|
||||
"SkirtRefinementFace", "SkirtCoarseCentroid", "FarShellSurfaceQuery",
|
||||
"FarShellProjectionNormal", "FarShellStratumRefinement",
|
||||
)
|
||||
|
||||
|
||||
def records(path: Path, tag: str) -> dict[str, dict[str, str]]:
|
||||
result = {}
|
||||
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if line.startswith(tag + " "):
|
||||
row = dict(FIELDS.findall(line))
|
||||
result[row["key"]] = row
|
||||
return result
|
||||
|
||||
|
||||
def lod_of(key: str) -> int:
|
||||
return int(key.rsplit("lod", 1)[1].rstrip(")"))
|
||||
|
||||
|
||||
def run(strate: int, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"s{strate}-w8-profile{replicate}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARY), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--workers=8", "--clip-radius=3", "--max-clip-level=8", "--max-level=8",
|
||||
f"--camera={camera}", "--profile-lod-work", "--profile-passage-composer",
|
||||
]
|
||||
started = time.monotonic()
|
||||
peak_tree_rss_kib = 0
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
process = subprocess.Popen(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT)
|
||||
deadline = started + 1740
|
||||
page_kib = os.sysconf("SC_PAGE_SIZE") // 1024
|
||||
while process.poll() is None:
|
||||
pending = [process.pid]
|
||||
seen = set()
|
||||
rss_kib = 0
|
||||
while pending:
|
||||
pid = pending.pop()
|
||||
if pid in seen:
|
||||
continue
|
||||
seen.add(pid)
|
||||
try:
|
||||
with open(f"/proc/{pid}/statm", encoding="ascii") as statm:
|
||||
rss_kib += int(statm.read().split()[1]) * page_kib
|
||||
with open(f"/proc/{pid}/task/{pid}/children", encoding="ascii") as children:
|
||||
pending.extend(int(value) for value in children.read().split())
|
||||
except (FileNotFoundError, ProcessLookupError, PermissionError,
|
||||
IndexError, ValueError):
|
||||
pass
|
||||
peak_tree_rss_kib = max(peak_tree_rss_kib, rss_kib)
|
||||
if time.monotonic() >= deadline:
|
||||
process.kill()
|
||||
process.wait()
|
||||
raise SystemExit(f"profile exceeded 30 minutes: {log}")
|
||||
time.sleep(0.01)
|
||||
return_code = process.wait()
|
||||
if return_code:
|
||||
raise SystemExit(f"profile failed: {log}; exit={return_code}")
|
||||
|
||||
composer = records(log, "[PassageComposerTile]")
|
||||
stages = records(log, "[CpuTileStages]")
|
||||
counts = records(log, "[PassageSweepCount]")
|
||||
per_lod: dict[int, dict] = defaultdict(lambda: {
|
||||
"tiles": 0, "grid_samples": 0, "sweep_pairs": 0,
|
||||
"route_candidates": 0, "carve_segment_visits": 0,
|
||||
"sweep_prepare_ms": 0.0, "passage_worker_ms": 0.0,
|
||||
"classify_ms": 0.0, "column_ms": 0.0, "mc_ms": 0.0,
|
||||
"touched": 0, "touched_unique": 0, "body_band": 0,
|
||||
"stamped_outside": 0, "cache_valid": 0, "cache_valid_unique": 0,
|
||||
"shell_band": 0, "exact_band_unique": 0,
|
||||
"contributing_pairs": 0, "overlap_pairs": 0, "cross_passage_overlap": 0,
|
||||
"floor_profile_calls": 0,
|
||||
"cache_bytes": 0, "scratch_bytes": 0,
|
||||
"cache_bytes_max_tile": 0, "scratch_bytes_max_tile": 0,
|
||||
"lookups": 0, "cache_hits": 0, "missing": 0,
|
||||
"offgrid_miss": 0, "unprepared_miss": 0,
|
||||
"missing_by_caller": [0] * len(CALLERS),
|
||||
"fallback_by_caller": [0] * len(CALLERS),
|
||||
})
|
||||
for key, comp in composer.items():
|
||||
lod = lod_of(key)
|
||||
if lod > 2 or int(comp.get("features", "0")) == 0:
|
||||
continue
|
||||
stage = stages.get(key)
|
||||
count = counts.get(key)
|
||||
if stage is None or count is None:
|
||||
raise SystemExit(f"missing stage/count row for {label} {key}")
|
||||
row = per_lod[lod]
|
||||
row["tiles"] += 1
|
||||
row["grid_samples"] += int(comp["sweep_grid_samples"])
|
||||
for name in ("sweep_pairs", "route_candidates", "carve_segment_visits",
|
||||
"lookups", "cache_hits", "missing"):
|
||||
source = {"route_candidates": "route_candidates",
|
||||
"carve_segment_visits": "carve_segment_visits",
|
||||
"lookups": "sweep_lookups", "cache_hits": "sweep_cache_hits",
|
||||
"missing": "sweep_missing"}.get(name, name)
|
||||
row[name] += int(comp[source])
|
||||
row["sweep_prepare_ms"] += float(comp["sweep_prepare_ms"])
|
||||
row["passage_worker_ms"] += float(comp["worker_ms"])
|
||||
for name in ("classify_ms", "column_ms", "mc_ms"):
|
||||
row[name] += float(stage[name])
|
||||
for name in ("touched", "stamped_outside", "cache_valid", "cache_valid_unique",
|
||||
"touched_unique", "body_band", "shell_band",
|
||||
"exact_band_unique", "overlap_pairs", "cross_passage_overlap",
|
||||
"contributing_pairs", "floor_profile_calls",
|
||||
"cache_bytes", "scratch_bytes", "offgrid_miss",
|
||||
"unprepared_miss"):
|
||||
row[name] += int(count[name])
|
||||
row["cache_bytes_max_tile"] = max(row["cache_bytes_max_tile"],
|
||||
int(count["cache_bytes"]))
|
||||
row["scratch_bytes_max_tile"] = max(row["scratch_bytes_max_tile"],
|
||||
int(count["scratch_bytes"]))
|
||||
for name, field in (("missing_by_caller", "sweep_missing_by_caller"),
|
||||
("fallback_by_caller", "sweep_exact_fallback_by_caller")):
|
||||
values = [int(value) for value in stage[field].split(",")]
|
||||
row[name] = [a + b for a, b in zip(row[name], values)]
|
||||
|
||||
result_row = {
|
||||
"strate": strate, "workers": 8, "replicate": replicate,
|
||||
"elapsed_s": round(time.monotonic() - started, 3),
|
||||
"peak_tree_rss_kib": peak_tree_rss_kib,
|
||||
"by_lod": {str(lod): values for lod, values in sorted(per_lod.items())},
|
||||
"log": str(log.relative_to(ROOT)),
|
||||
}
|
||||
print("[L125PassageCounts] " + json.dumps(result_row, sort_keys=True), flush=True)
|
||||
return result_row
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not DATA.is_file() or not BINARY.is_file():
|
||||
raise SystemExit("missing L125 passage-counter input")
|
||||
runs = [run(strate, replicate) for strate in range(4) for replicate in (1, 2)]
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"runs": runs, "caller_names": CALLERS}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Five interleaved L80 readiness runs for a candidate against L109/L119."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import statistics
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT = ROOT / os.environ.get("L125_TIMING_DIR", "Saved/Linux/l125/timing")
|
||||
DATA = ROOT / "Standalone/data/owner_world.json"
|
||||
CANDIDATE = os.environ.get("L125_CANDIDATE_LABEL", "L125-stamp10")
|
||||
BINARIES = {
|
||||
"L109": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang",
|
||||
"L119": ROOT / "Saved/Linux/l119/bin/VoxelForgeCoreHeadless-l119-clang",
|
||||
CANDIDATE: ROOT / os.environ.get("L125_CANDIDATE_BINARY",
|
||||
"Saved/Linux/l125/bin-stamp10/VoxelForgeCoreHeadless-l125-stamp10-clang"),
|
||||
}
|
||||
CAMERAS = {
|
||||
0: (8.0, 16.0, -372.2626953125),
|
||||
1: (25.8369140625, 3.19921875, -737.0),
|
||||
2: (19.166015625, -14.2001953125, -753.3740234375),
|
||||
3: (-20.0625, -5.068359375, -900.353515625),
|
||||
}
|
||||
PIPELINE = re.compile(r"^\[StreamPipeline\] (.*)$", re.M)
|
||||
FIELDS = re.compile(r"([a-zA-Z0-9_]+)=([^ ]+)")
|
||||
NEAR = re.compile(
|
||||
r"^\[WorldAssemblerNearLODReady\] lods=0-2 tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",
|
||||
re.M)
|
||||
|
||||
|
||||
def run(strate: int, workers: int, version: str, phase: str, replicate: int) -> dict:
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
label = f"s{strate}-w{workers}-{version}-{phase}{replicate}"
|
||||
log = OUT / f"{label}.log"
|
||||
camera = ",".join(f"{value:.12f}" for value in CAMERAS[strate]) + ",0,0"
|
||||
command = [
|
||||
"taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--",
|
||||
"timeout", "1740s", str(BINARIES[version]), str(DATA),
|
||||
"--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
f"--workers={workers}", "--clip-radius=3", "--max-clip-level=8",
|
||||
"--max-level=8", f"--camera={camera}", "--profile-lod-work",
|
||||
"--stream-scheduler-policy=3",
|
||||
]
|
||||
started = time.monotonic()
|
||||
with log.open("w", encoding="utf-8") as output:
|
||||
result = subprocess.run(command, cwd=ROOT, stdout=output,
|
||||
stderr=subprocess.STDOUT, check=False, timeout=1740)
|
||||
content = log.read_text(encoding="utf-8", errors="replace")
|
||||
pipeline_match, near_match = PIPELINE.search(content), NEAR.search(content)
|
||||
if result.returncode or pipeline_match is None or near_match is None:
|
||||
raise SystemExit(f"readiness run failed: {log}; exit={result.returncode}")
|
||||
pipeline = dict(FIELDS.findall(pipeline_match.group(1)))
|
||||
row = {"strate": strate, "workers": workers, "version": version,
|
||||
"phase": phase, "replicate": replicate, "exit_code": result.returncode,
|
||||
"wall_s": round(time.monotonic() - started, 3),
|
||||
"lod02_ready_s": float(near_match.group(3)),
|
||||
"whole_view_ready_s": float(pipeline["whole_view_ready"].removesuffix("s")),
|
||||
"whole_tiles": int(pipeline["whole_tiles"].split("/")[0]),
|
||||
"log": str(log.relative_to(ROOT))}
|
||||
print("[L125ToleranceTimingRun] " + json.dumps(row, sort_keys=True), flush=True)
|
||||
return row
|
||||
|
||||
|
||||
def cpu_idle_percent(interval_s: float = 2.0) -> float:
|
||||
def totals() -> tuple[int, int]:
|
||||
fields = Path("/proc/stat").read_text(encoding="utf-8").splitlines()[0].split()
|
||||
values = [int(value) for value in fields[1:]]
|
||||
return sum(values), values[3] + (values[4] if len(values) > 4 else 0)
|
||||
total_before, idle_before = totals()
|
||||
time.sleep(interval_s)
|
||||
total_after, idle_after = totals()
|
||||
return 100.0 * (idle_after-idle_before) / max(1, total_after-total_before)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not DATA.is_file() or any(not binary.is_file() for binary in BINARIES.values()):
|
||||
raise SystemExit("missing timing input")
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
load_1m, idle_pct = os.getloadavg()[0], cpu_idle_percent()
|
||||
uptime = subprocess.run(["uptime"], cwd=ROOT, text=True,
|
||||
capture_output=True, check=True).stdout.strip()
|
||||
top = subprocess.run(["ps", "-eo", "pid,pcpu,comm", "--sort=-pcpu"],
|
||||
cwd=ROOT, text=True, capture_output=True, check=True).stdout
|
||||
(OUT / "machine-load-before.txt").write_text(
|
||||
f"uptime: {uptime}\n1-minute load: {load_1m:.2f}\n"
|
||||
f"2-second CPU idle: {idle_pct:.1f}%\ntop CPU users:\n{top}",
|
||||
encoding="utf-8")
|
||||
print(f"[L125ToleranceTimingGate] load_1m={load_1m:.2f} "
|
||||
f"cpu_idle_2s={idle_pct:.1f}% uptime={uptime}", flush=True)
|
||||
if load_1m >= 0.9 or idle_pct < 97.0:
|
||||
raise SystemExit("timing refused: machine is above the quiet-load gate")
|
||||
|
||||
rows = []
|
||||
versions = ("L109", "L119", CANDIDATE)
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
for version in versions:
|
||||
run(strate, workers, version, "warmup", 0)
|
||||
for replicate in range(1, 6):
|
||||
offset = (replicate - 1) % len(versions)
|
||||
order = versions[offset:] + versions[:offset]
|
||||
for version in order:
|
||||
rows.append(run(strate, workers, version, "measured", replicate))
|
||||
(OUT / "runs-partial.json").write_text(
|
||||
json.dumps(rows, indent=2) + "\n", encoding="utf-8")
|
||||
summary = []
|
||||
for strate in range(4):
|
||||
for workers in (8, 12):
|
||||
item = {"strate": strate, "workers": workers, "replicates": 5}
|
||||
medians = {}
|
||||
for version in versions:
|
||||
group = [row for row in rows if row["strate"] == strate
|
||||
and row["workers"] == workers and row["version"] == version]
|
||||
medians[version] = {}
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
values = [row[key] for row in group]
|
||||
medians[version][key] = statistics.median(values)
|
||||
item[f"{version}_{key}_values"] = values
|
||||
item[f"{version}_{key}_median"] = medians[version][key]
|
||||
for comparison in ("L109", "L119"):
|
||||
for key in ("lod02_ready_s", "whole_view_ready_s"):
|
||||
item[f"L125_vs_{comparison}_{key}_delta_pct"] = round(
|
||||
100.0 * (medians[CANDIDATE][key]
|
||||
/medians[comparison][key]-1.0), 2)
|
||||
summary.append(item)
|
||||
(OUT / "summary.json").write_text(
|
||||
json.dumps({"runs": rows, "summary": summary}, indent=2) + "\n",
|
||||
encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,188 @@
|
||||
<!doctype html><meta charset='utf-8'>
|
||||
<title>L119 / L125 tunnel capture comparison</title>
|
||||
<style>body{font:14px system-ui;margin:24px;background:#17191d;color:#eee}section{margin:28px 0}h2{font-size:18px}div{display:grid;grid-template-columns:1fr 1fr;gap:12px}img{width:100%;height:auto}p{color:#bbb}</style>
|
||||
<h1>L119 / L125 v225: identical views</h1>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — rock-floor/401.7m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-rock-floor-station-1.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-rock-floor-station-1.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — rock-floor/792.8m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-rock-floor-station-2.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-rock-floor-station-2.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — rock-floor/1634.5m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-rock-floor-station-3.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-rock-floor-station-3.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — rock-floor/27.0m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-rock-floor-station-1.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-rock-floor-station-1.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — rock-floor/52.9m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-rock-floor-station-2.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-rock-floor-station-2.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — rock-floor/78.9m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-rock-floor-station-3.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-rock-floor-station-3.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — rock-floor/323.4m</h2>
|
||||
<p>rock-floor-close · L119 low_contrast_geometry_visible · L125 low_contrast_geometry_visible</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-rock-floor-station-1.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-rock-floor-station-1.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — rock-floor/333.4m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-rock-floor-station-2.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-rock-floor-station-2.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — rock-floor/343.4m</h2>
|
||||
<p>rock-floor-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-rock-floor-station-3.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-rock-floor-station-3.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-1/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-1-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-1-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-1/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 low_contrast_geometry_visible · L125 low_contrast_geometry_visible</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-1-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-1-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-2/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-2-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-2-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-2/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-2-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-2-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-3/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-3-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-3-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/0-1/0 — section-3/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-0-1-0-openair-3-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-0-1-0-openair-3-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — section-1/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-openair-1-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-openair-1-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — section-1/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-openair-1-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-openair-1-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — section-2/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-openair-2-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-openair-2-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/1-2/0 — section-2/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-1-2-0-openair-2-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-1-2-0-openair-2-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — section-1/4m-outside</h2>
|
||||
<p>open-air-close · L119 low_contrast_geometry_visible · L125 low_contrast_geometry_visible</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-openair-1-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-openair-1-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — section-1/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 low_contrast_geometry_visible · L125 low_contrast_geometry_visible</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-openair-1-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-openair-1-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — section-2/4m-outside</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-openair-2-4m-outside.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-openair-2-4m-outside.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h2>passage/2-3/0 — section-2/4m-interior-wall</h2>
|
||||
<p>open-air-close · L119 pass · L125 pass</p>
|
||||
<div>
|
||||
<figure><figcaption>L119</figcaption><img src='../../Linux119/captures/L119/L115/L115/passage-2-3-0-openair-2-4m-interior-wall.png'></figure>
|
||||
<figure><figcaption>L125 v225</figcaption><img src='L125/passage-2-3-0-openair-2-4m-interior-wall.png'></figure>
|
||||
</div>
|
||||
</section>
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 177 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 106 KiB |