From 89504ecc97bef6cdb4e3d351ad9048ac9bd7d321 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Fri, 30 Oct 2026 03:04:07 +0100 Subject: [PATCH] research: chord-bound + z-column fine-segment pruning (L116, exact, not merged) Fine pairs 48.9M -> 38.8M (-21%) with L115-identical hashes, but no readiness change (still +15..+30% near-ready vs L109). Pair evaluation is not where the regression lives. Kept as patch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1 --- .../WorldAssemblerM3/Linux116/REPORT.md | 20 ++ .../Linux116/RunL116HashMatrix.py | 89 ++++++ .../Linux116/RunL116PairCounts.py | 64 ++++ .../Linux116/RunL116Timing.py | 125 ++++++++ .../chord-bound-zcolumn-not-merged.patch | 302 ++++++++++++++++++ 5 files changed, 600 insertions(+) create mode 100644 Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md create mode 100644 Standalone/Research/WorldAssemblerM3/Linux116/RunL116HashMatrix.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux116/RunL116PairCounts.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux116/RunL116Timing.py create mode 100644 Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch diff --git a/Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md new file mode 100644 index 0000000..d15c06a --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux116/REPORT.md @@ -0,0 +1,20 @@ +# Linux L116 + +L116 preserves L115 output: all 18 Clang/GCC hash-matrix cases matched, including all four owner views at W1/W8/W12. The tightened chord bound and z-column cache reduced exact fine-segment/sample evaluations from 48,947,915 in L115 to 38,783,101 (-20.8%). L109 was 19,812,455; L116 still performs 1.96x as many pairs. + +Timing used five interleaved L80 runs per version, stratum, and worker count. The pre-run one-minute load was 0.94; top reported 97.1% idle. + +| Stratum | Workers | LOD0-2 ready L109 / L115 / L116 (s) | L116 vs L109 | Whole view L109 / L115 / L116 (s) | +|---:|---:|---:|---:|---:| +| 0 | 8 | 0.770 / 0.908 / 0.947 | +23.1% | 1.791 / 1.940 / 1.907 | +| 0 | 12 | 0.788 / 0.917 / 0.930 | +18.1% | 1.732 / 1.871 / 1.886 | +| 1 | 8 | 0.951 / 1.236 / 1.235 | +29.9% | 1.833 / 2.163 / 2.194 | +| 1 | 12 | 0.906 / 1.174 / 1.174 | +29.5% | 1.756 / 2.040 / 2.038 | +| 2 | 8 | 1.015 / 1.264 / 1.248 | +22.9% | 1.905 / 2.166 / 2.102 | +| 2 | 12 | 0.940 / 1.177 / 1.173 | +24.8% | 1.757 / 1.999 / 1.995 | +| 3 | 8 | 0.882 / 1.011 / 1.016 | +15.2% | 1.850 / 1.958 / 2.009 | +| 3 | 12 | 0.796 / 0.933 / 0.950 | +19.4% | 1.663 / 1.809 / 1.826 | + +The requested near-ready target of about 5% over L109 was not met. Fast tier passed (344.3 s). The Linux VFHostEditor build passed with `-NoUBA -MaxParallelActions=4` (71.11 s; no `prlimit`). + +Logs and summaries are under `Saved/Linux/l116/`; timing summary is `timing/summary.json`, pair counts are in `pairs/summary.json`, and the hash result is `hashes/summary.json`. diff --git a/Standalone/Research/WorldAssemblerM3/Linux116/RunL116HashMatrix.py b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116HashMatrix.py new file mode 100644 index 0000000..56fedb9 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116HashMatrix.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Compare the L116 hash matrix directly with the saved L115 matrix.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l116/hashes" +BASE = ROOT / "Saved/Linux/l115/hashes" +DATASETS = { + "owner4": ROOT / "Standalone/data/owner_world.json", + "owner2": ROOT / "Standalone/data/regression/owner_world_2strate_l61.json", +} +BINARIES = { + "clang": ROOT / "Saved/Linux/l116/bin/VoxelForgeCoreHeadless-l116-clang", + "gcc": ROOT / "Saved/Linux/l116/bin/VoxelForgeCoreHeadless-l116-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) + baseline_log = BASE / f"{label}.log" + baseline_hashes = HASH.findall(baseline_log.read_text(encoding="utf-8", errors="replace")) + if result.returncode or len(hashes) != 12 or hashes != baseline_hashes: + raise SystemExit(f"hash mismatch or incomplete run: {label}; exit={result.returncode} " + f"l116={len(hashes)} l115={len(baseline_hashes)} log={log}") + row = { + "dataset": dataset, "recipe": recipe, "workers": workers, + "compiler": compiler, "exit_code": result.returncode, + "matches_l115": True, "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)), + "l115_log": str(baseline_log.relative_to(ROOT)), + } + print("[L116HashRun] " + 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 L116 hash input") + 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") + summary = { + "runs": rows, + "all_hashes_match_l115": all(row["matches_l115"] for row in rows), + "owner4_all_four_strata": "owner4/surface-world", + "worker_counts": [1, 8, 12], + "compilers": ["clang", "gcc"], + } + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux116/RunL116PairCounts.py b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116PairCounts.py new file mode 100644 index 0000000..06594e8 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116PairCounts.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Count exact fine-segment/sample evaluations in the four L80 views.""" +from __future__ import annotations + +import json +import re +import subprocess +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[4] +OUT = ROOT / "Saved/Linux/l116/pairs" +DATA = ROOT / "Standalone/data/owner_world.json" +BIN = ROOT / "Saved/Linux/l116/bin/VoxelForgeCoreHeadless-l116-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), +} +PAIRS = re.compile(r"\bsweep_pairs=(\d+)") + + +def run(strate: int) -> dict: + OUT.mkdir(parents=True, exist_ok=True) + log = OUT / f"L116-s{strate}.log" + camera = ",".join(f"{v:.12f}" for v in CAMERAS[strate]) + ",0,0" + command = [ + "taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--", + "timeout", "1740s", str(BIN), str(DATA), + "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline", + "--workers=12", "--clip-radius=3", "--max-clip-level=8", "--max-level=8", + f"--camera={camera}", "--profile-passage-composer", + "--stream-scheduler-policy=3", + ] + 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) + values = [int(value) for value in PAIRS.findall( + log.read_text(encoding="utf-8", errors="replace"))] + if result.returncode or not values: + raise SystemExit(f"pair profile missing: {log}; exit={result.returncode}") + row = {"strate": strate, "tiles": len(values), "segment_sample_pairs": sum(values), + "elapsed_s": round(time.monotonic() - started, 3), + "log": str(log.relative_to(ROOT))} + print("[L116PairCount] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if not BIN.is_file() or not DATA.is_file(): + raise SystemExit("missing L116 pair-count input") + rows = [run(strate) for strate in range(4)] + summary = {"strata": rows, + "total_segment_sample_pairs": sum(row["segment_sample_pairs"] + for row in rows)} + (OUT / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", + encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux116/RunL116Timing.py b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116Timing.py new file mode 100644 index 0000000..e8d5f97 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux116/RunL116Timing.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Five interleaved L80 readiness runs for L109, L115, and L116.""" +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 / "Saved/Linux/l116/timing" +DATA = ROOT / "Standalone/data/owner_world.json" +BINARIES = { + "L109": ROOT / "Saved/Linux/l109/bin-final/VoxelForgeCoreHeadless-l109-clang", + "L115": ROOT / "Saved/Linux/l115/bin/VoxelForgeCoreHeadless-l115-clang", + "L116": ROOT / "Saved/Linux/l116/bin/VoxelForgeCoreHeadless-l116-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"{v:.12f}" for v 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", + ] + start = 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 = PIPELINE.search(content) + near_match = NEAR.search(content) + if result.returncode or pipeline_match is None or near_match is None: + raise SystemExit(f"readiness run failed or trace missing: {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() - start, 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("[L116TimingRun] " + json.dumps(row, sort_keys=True), flush=True) + return row + + +def main() -> int: + if any(not path.is_file() for path in (*BINARIES.values(), DATA)): + raise SystemExit("missing L109/L115/L116 data or timing binary") + OUT.mkdir(parents=True, exist_ok=True) + load_1m = os.getloadavg()[0] + uptime = subprocess.run(["uptime"], cwd=ROOT, text=True, capture_output=True, + check=True).stdout.strip() + top = subprocess.run(["top", "-b", "-n1"], 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}\ntop:\n{top}", + encoding="utf-8") + print(f"[L116TimingGate] load_1m={load_1m:.2f} uptime={uptime}", flush=True) + if load_1m >= 1.5: + raise SystemExit(f"timing refused: 1-minute load {load_1m:.2f} is not below 1.5") + rows = [] + versions = ("L109", "L115", "L116") + 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", "L115"): + for key in ("lod02_ready_s", "whole_view_ready_s"): + item[f"L116_vs_{comparison}_{key}_delta_pct"] = round( + 100.0 * (medians["L116"][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()) diff --git a/Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch b/Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch new file mode 100644 index 0000000..423900f --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux116/patches/chord-bound-zcolumn-not-merged.patch @@ -0,0 +1,302 @@ +diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h +index c266bd8..216b0a2 100644 +--- a/Standalone/Core/src/ShapeWorldInternal.h ++++ b/Standalone/Core/src/ShapeWorldInternal.h +@@ -11481,6 +11481,12 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + last=std::min(sweep->dimension-1, + static_cast(std::floor(relativeMaximum))); + }; ++ struct FColumnProjectionCache ++ { ++ double projectionXY=0.0; ++ double floorT=0.0,floorTClamped=0.0,floorDepth=0.0,floorCenterZ=0.0; ++ bool hasProjection=false,hasFloorProfile=false; ++ }; + + for(std::size_t passageIndex=0;passageIndex=segmentEndIndex)continue; ++ constexpr std::size_t MaxCandidateSegments=8; ++ const std::size_t candidateSegmentCount=segmentEndIndex-candidate.segmentIndex; ++ std::array chordProjectionMinimum{}; ++ std::array chordProjectionMaximum{}; ++ std::array maximumRadius{}; ++ std::array maximumChordDeviation{}; ++ std::array,MaxCandidateSegments> segmentMinimum{}; ++ std::array,MaxCandidateSegments> segmentMaximum{}; ++ std::array chordOrigin{},chordDirection{}; ++ bool canCertifyChordWindow=candidateSegmentCount<=MaxCandidateSegments; ++ if(canCertifyChordWindow) ++ { ++ chordOrigin=segments[candidate.segmentIndex].a; ++ const auto& lastSegment=segments[segmentEndIndex-1]; ++ const double chordX=lastSegment.b[0]-chordOrigin[0]; ++ const double chordY=lastSegment.b[1]-chordOrigin[1]; ++ const double chordZ=lastSegment.b[2]-chordOrigin[2]; ++ const double chordLengthSq=chordX*chordX+chordY*chordY+chordZ*chordZ; ++ const double chordLength=chordLengthSq>1.0e-12 ++ ?FStandaloneShapeWorld::DeterministicSqrt(chordLengthSq):0.0; ++ canCertifyChordWindow=chordLength>1.0e-6&&std::isfinite(chordLength); ++ if(canCertifyChordWindow) ++ { ++ const double inverseChordLength=1.0/chordLength; ++ chordDirection={chordX*inverseChordLength,chordY*inverseChordLength, ++ chordZ*inverseChordLength}; ++ for(std::size_t localIndex=0;localIndex first{},last{}; + for(std::size_t axis=0;axis<3;++axis) + gridRange(axis,candidate.sweepMinimumSubvoxels[axis], + candidate.sweepMaximumSubvoxels[axis],first[axis],last[axis]); + if(first[0]>last[0]||first[1]>last[1]||first[2]>last[2])continue; ++ const std::size_t columnWidth=static_cast(last[0]-first[0]+1); ++ const std::size_t columnHeight=static_cast(last[1]-first[1]+1); ++ const std::size_t columnsPerSegment=columnWidth*columnHeight; ++ std::vector columnProjectionCache( ++ candidateSegmentCount*columnsPerSegment); + + for(std::int32_t gz=first[2];gz<=last[2];++gz) + { +@@ -11516,9 +11594,65 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + +static_cast(gy)*sweep->stepSubvoxels); + const double qz=static_cast(sweep->gridMinimumSubvoxels[2] + +static_cast(gz)*sweep->stepSubvoxels); +- for(std::size_t segmentIndex=candidate.segmentIndex; +- segmentIndex query={qx,qy,qz}; ++ const std::size_t sampleIndex=(static_cast(gz) ++ *static_cast(sweep->dimension)+static_cast(gy)) ++ *static_cast(sweep->dimension)+static_cast(gx); ++ auto& sample=passageSamples.samples[sampleIndex]; ++ std::array segmentOrder{}; ++ std::size_t anchorIndex=0; ++ double queryChordProjection=0.0; ++ double queryChordPerpendicularDistanceSq=0.0; ++ if(canCertifyChordWindow) ++ { ++ queryChordProjection=(qx-chordOrigin[0])*chordDirection[0] ++ +(qy-chordOrigin[1])*chordDirection[1] ++ +(qz-chordOrigin[2])*chordDirection[2]; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ const double perpendicular=(query[axis]-chordOrigin[axis]) ++ -queryChordProjection*chordDirection[axis]; ++ queryChordPerpendicularDistanceSq+=perpendicular*perpendicular; ++ } ++ double nearestChordGap=std::numeric_limits::infinity(); ++ for(std::size_t localIndex=0;localIndex=offset) ++ segmentOrder[orderCount++]=anchorIndex-offset; ++ if(orderCount(gy-first[1])*columnWidth ++ +static_cast(gx-first[0]); ++ auto& column=columnProjectionCache[columnIndex]; + const auto& segment=segments[segmentIndex]; + if(segment.lengthSq<=0.0 + ||qxsegment.sweepMaximumSubvoxels[1] + ||qzsegment.sweepMaximumSubvoxels[2])continue; +- const double projectionXY=(qx-segment.a[0])*segment.delta[0] +- +(qy-segment.a[1])*segment.delta[1]; ++ if(canCertifyChordWindow) ++ { ++ // Every point on a fine segment stays within ++ // maximumChordDeviation of the coarse chord. Combine the ++ // chord-axis gap with the query's perpendicular distance, then ++ // subtract that deviation. The segment AABB is a second lower ++ // bound. Skip only when both bounds exclude every exact minimum. ++ const double chordGap=std::max({chordProjectionMinimum[localIndex] ++ -queryChordProjection,0.0, ++ queryChordProjection-chordProjectionMaximum[localIndex]}); ++ const double chordDistanceSq=queryChordPerpendicularDistanceSq ++ +chordGap*chordGap; ++ double boxDistanceSq=0.0; ++ for(std::size_t axis=0;axis<3;++axis) ++ { ++ const double gap=std::max({segmentMinimum[localIndex][axis] ++ -query[axis],0.0, ++ query[axis]-segmentMaximum[localIndex][axis]}); ++ boxDistanceSq+=gap*gap; ++ } ++ const double boundMargin=1.0e-6*qPerMetre; ++ const double tubeBaseLimit=sample.roundSignedDistance ++ +maximumRadius[localIndex]+boundMargin; ++ const double tubeChordLimit=tubeBaseLimit ++ +maximumChordDeviation[localIndex]; ++ const bool canImproveTube=tubeBaseLimit>=0.0&&tubeChordLimit>=0.0 ++ &&boxDistanceSq<=tubeBaseLimit*tubeBaseLimit ++ &&chordDistanceSq<=tubeChordLimit*tubeChordLimit; ++ const double bestFloorDistance=std::isfinite( ++ sample.floorCenterDistanceSq) ++ ?FStandaloneShapeWorld::DeterministicSqrt( ++ sample.floorCenterDistanceSq) ++ :std::numeric_limits::infinity(); ++ const double floorBaseLimit=bestFloorDistance+boundMargin; ++ const double floorChordLimit=floorBaseLimit ++ +maximumChordDeviation[localIndex]; ++ const bool canImproveFloor=floorBaseLimit>=0.0 ++ &&floorChordLimit>=0.0 ++ &&boxDistanceSq<=floorBaseLimit*floorBaseLimit ++ &&chordDistanceSq<=floorChordLimit*floorChordLimit; ++ const double shellBaseLimit=sample.shellSignedDistance ++ +shellThickness+maximumRadius[localIndex]+boundMargin; ++ const double shellChordLimit=shellBaseLimit ++ +maximumChordDeviation[localIndex]; ++ const bool canImproveShell=shellBaseLimit>=0.0 ++ &&shellChordLimit>=0.0 ++ &&boxDistanceSq<=shellBaseLimit*shellBaseLimit ++ &&chordDistanceSq<=shellChordLimit*shellChordLimit; ++ if(!canImproveTube&&!canImproveFloor&&!canImproveShell)continue; ++ } ++ if(!column.hasProjection) ++ { ++ column.projectionXY=(qx-segment.a[0])*segment.delta[0] ++ +(qy-segment.a[1])*segment.delta[1]; ++ column.hasProjection=true; ++ } ++ const double projectionXY=column.projectionXY; + const double t=std::clamp((projectionXY + +(qz-segment.a[2])*segment.delta[2])/segment.lengthSq,0.0,1.0); + double cx=segment.a[0]+t*segment.delta[0]; +@@ -11538,18 +11727,44 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + const double ex=qx-cx,ey=qy-cy,ez=qz-cz; + const double signedDistance=FStandaloneShapeWorld::DeterministicSqrt( + ex*ex+ey*ey+ez*ez)-radius; +- const double floorT=segment.horizontalLengthSq>1.0e-12 +- ?projectionXY/segment.horizontalLengthSq:t; +- const double floorTClamped=std::clamp(floorT,0.0,1.0); + const double floorCenterDistanceSq=ex*ex+ey*ey+ez*ez; +- const double floorRadius=segment.radiusA +- +floorTClamped*segment.radiusDelta; +- const double floorDepth=FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels( +- constants,segmentIndex,floorTClamped,floorRadius,qPerMetre); +- const double floorCenterZ= +- FStandaloneShapeWorld::PassageFlatFloorCenterZSubvoxels( +- constants,segments,segmentIndex,floorT,segment, +- qx,qy,floorDepth,qPerMetre); ++ double floorT=0.0,floorTClamped=0.0,floorDepth=0.0,floorCenterZ=0.0; ++ if(segment.horizontalLengthSq>1.0e-12) ++ { ++ if(!column.hasFloorProfile) ++ { ++ column.floorT=projectionXY/segment.horizontalLengthSq; ++ column.floorTClamped=std::clamp(column.floorT,0.0,1.0); ++ const double floorRadius=segment.radiusA ++ +column.floorTClamped*segment.radiusDelta; ++ column.floorDepth= ++ FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels( ++ constants,segmentIndex,column.floorTClamped,floorRadius, ++ qPerMetre); ++ column.floorCenterZ= ++ FStandaloneShapeWorld::PassageFlatFloorCenterZSubvoxels( ++ constants,segments,segmentIndex,column.floorT,segment, ++ qx,qy,column.floorDepth,qPerMetre); ++ column.hasFloorProfile=true; ++ } ++ floorT=column.floorT; ++ floorTClamped=column.floorTClamped; ++ floorDepth=column.floorDepth; ++ floorCenterZ=column.floorCenterZ; ++ } ++ else ++ { ++ floorT=t; ++ floorTClamped=std::clamp(floorT,0.0,1.0); ++ const double floorRadius=segment.radiusA ++ +floorTClamped*segment.radiusDelta; ++ floorDepth=FStandaloneShapeWorld::PassageFlatFloorDepthSubvoxels( ++ constants,segmentIndex,floorTClamped,floorRadius,qPerMetre); ++ floorCenterZ= ++ FStandaloneShapeWorld::PassageFlatFloorCenterZSubvoxels( ++ constants,segments,segmentIndex,floorT,segment, ++ qx,qy,floorDepth,qPerMetre); ++ } + const double flatDistance=FStandaloneShapeWorld::PassageFlatBottomDistance( + static_cast(signedDistance),floorCenterZ,qz,floorDepth,qPerMetre); + if(profile)++profileStats_->passageSweepSegmentSamplePairs; +@@ -11557,10 +11772,6 @@ inline void FStandaloneShapeWorld::FScopedPassageTileQueries::prepareSweptTubeBa + { + if(profile)++profileStats_->passageSweepOutsideInfluencePairs; + } +- const std::size_t sampleIndex=(static_cast(gz) +- *static_cast(sweep->dimension)+static_cast(gy)) +- *static_cast(sweep->dimension)+static_cast(gx); +- auto& sample=passageSamples.samples[sampleIndex]; + if(floorCenterDistanceSq