Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e214561f | ||
|
|
6cd9e4da76 | ||
|
|
9c6d261d01 | ||
|
|
eb47d72258 | ||
|
|
95bf654fbc |
No files matched your search
@@ -0,0 +1,57 @@
|
||||
# AGENTS.md — VoxelForge (read automatically by Codex)
|
||||
|
||||
You are the implementation agent. The orchestrator (Claude) writes each round's brief, reviews your diff against the
|
||||
code, and commits. The owner decides direction. Be warm and direct; credit what worked; say plainly what didn't.
|
||||
|
||||
Read first, as the task needs: `CURRENT-STATE.md` (what ships, reference numbers, **closed experiments: don't reopen
|
||||
them without new evidence**), then `CODEMAP.md` and `ARCHITECTURE.md`. Round reports live in
|
||||
`Standalone/Research/WorldAssemblerM3/Linux*/REPORT.md`.
|
||||
|
||||
## Standing rules (every round, unless the brief says otherwise)
|
||||
|
||||
- Run as user `jahni`. Write only inside this plugin, plus `/home/jahni/VFHost` and `/home/jahni/UnrealEngine` when
|
||||
the brief asks for a UE build. No installs, no system changes.
|
||||
- **No `git commit`, `push` or `stash`.** Leave changes in the working tree; the orchestrator reviews and commits.
|
||||
- Headless processes and builds: `taskset -c 0-11 prlimit --as=6442450944` (12 P-core threads, 6 GiB). Never use all
|
||||
cores.
|
||||
- Unreal (UBT, the editor, commandlets): `taskset -c 0-11` only, **never `prlimit`** (CoreCLR fails). UE builds use
|
||||
`-NoUBA -MaxParallelActions=4`, never while timing.
|
||||
- Stop any single run that passes 30 minutes. Logs go in `Saved/Linux/l<round>/`.
|
||||
- **Timing:** quiet machine only (check `uptime` first; load < ~1.5, nothing else building). L80 protocol: one warm-up
|
||||
per binary, ≥5 interleaved runs, W8 and W12, all four owner layers. Every number states the window that produced it.
|
||||
- **Same world for everyone:** a seed must give the same world on every machine and worker count (Clang and GCC,
|
||||
W1/W8/W12). Byte-identity with older versions is NOT required. If the output changes: bump
|
||||
`WorldAssemblyEvaluatorVersion`, add baselines, and show captures.
|
||||
- Density sign: negative = solid, positive = air. Core voxels are 25 cm. Never mix metres and voxels without an
|
||||
explicit conversion (v225 shipped a reach bug from exactly that).
|
||||
- The fast tier (`Standalone/Tools/verify/verify.py fast`) must stay green. Don't loosen an audit to make a change pass.
|
||||
|
||||
## How to work
|
||||
|
||||
1. **Count before you build.** Measure where the time or the failure actually is (stack sampler, census, counters).
|
||||
State which parts are measured and which are inferred.
|
||||
2. **Suspect the measurement first.** An alarming or contradictory number is the instrument until a control rules it
|
||||
out (A/A runs, an ablation that verifiably removes the work).
|
||||
3. **No fix without a root cause.** Reproduce it, trace it to its source, fix the cause. A guard that hides the symptom
|
||||
is not a fix.
|
||||
4. **One change at a time,** each with its own check.
|
||||
5. **Subtract before you add.** Prefer removing work to reorganising it. The smallest edit that solves the problem.
|
||||
6. **Attack the premise.** When two fixes sharing one premise fail the same gate, write that premise in one sentence
|
||||
and question it before trying a third. **After three failed fixes, stop:** it is probably the design, not the
|
||||
hypothesis. Report it instead of trying fix #4.
|
||||
7. **Tests must be able to fail.** Name what would make each check fail. Add a negative control when you add an audit.
|
||||
8. **Refactors:** pin the behaviour (hashes), change the structure, prove nothing changed.
|
||||
9. **Verify before you claim.** Don't report a check as passed unless it ran on the final source. Say which checks
|
||||
didn't run and why.
|
||||
10. Changes that speed things up go in the shared layer, so every generator benefits.
|
||||
|
||||
## Report
|
||||
|
||||
Write the round's `REPORT.md`, short and plain:
|
||||
- the result first;
|
||||
- what changed and why;
|
||||
- the measurements, with their windows;
|
||||
- the checks run, and those not run;
|
||||
- remaining risks.
|
||||
|
||||
List any changed Unreal `Source/` files, because the owner compiles them on Windows.
|
||||
@@ -891,6 +891,8 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV3 = 3;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV4 = 4;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV5 = 5;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
@@ -904,7 +906,8 @@ namespace VoxelForgeCore
|
||||
fillPillar = 3,
|
||||
fillDais = 4,
|
||||
fillMonolith = 5,
|
||||
fillPlatform = 6
|
||||
fillPlatform = 6,
|
||||
fillSweptDeckSegment = 7
|
||||
};
|
||||
|
||||
struct ResolvedLayoutStampRequestV1
|
||||
@@ -914,6 +917,11 @@ namespace VoxelForgeCore
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
// A preferred center and a bounded deterministic search radius.
|
||||
std::array<std::int64_t, 3> anchorPositionMicrometres{};
|
||||
// Present only for fillSweptDeckSegment; defines its directed centerline.
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
// V5 permits one continuous swept deck to carry its complete sampled centerline.
|
||||
// Empty retains the V4 single-segment interpretation.
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
std::int64_t smoothWidthMicrometres = 0;
|
||||
@@ -933,6 +941,8 @@ namespace VoxelForgeCore
|
||||
std::int32_t strateIndex = -1;
|
||||
ResolvedSdfStampKindV1 kind = ResolvedSdfStampKindV1::carveEllipsoid;
|
||||
std::array<std::int64_t, 3> positionMicrometres{};
|
||||
std::array<std::array<std::int64_t, 3>, 2> sweepEndpointsMicrometres{};
|
||||
std::vector<std::array<std::int64_t, 3>> sweepPathMicrometres;
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::array<std::int64_t, 3> halfExtentsMicrometres{};
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace
|
||||
{
|
||||
const auto value=static_cast<std::uint8_t>(kind);
|
||||
return value>=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::carveEllipsoid)
|
||||
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillPlatform);
|
||||
&&value<=static_cast<std::uint8_t>(ResolvedSdfStampKindV1::fillSweptDeckSegment);
|
||||
}
|
||||
bool LayoutStampIsMotif(ResolvedSdfStampKindV1 kind)
|
||||
{return kind==ResolvedSdfStampKindV1::fillMonolith
|
||||
@@ -598,6 +598,28 @@ namespace
|
||||
<<",\"irregularity_wavelength_um\":"<<stamp.irregularityWavelengthMicrometres
|
||||
<<",\"irregularity_seed\":"<<stamp.irregularitySeed
|
||||
<<",\"yaw_quarter_turns\":"<<static_cast<unsigned>(stamp.yawQuarterTurns);
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
out<<",\"sweep_endpoints_um\":[["
|
||||
<<stamp.sweepEndpointsMicrometres[0][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[0][1]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[0][2]<<"],["
|
||||
<<stamp.sweepEndpointsMicrometres[1][0]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][1]<<','
|
||||
<<stamp.sweepEndpointsMicrometres[1][2]<<"]]";
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
out<<",\"sweep_path_um\":[";
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
if(pointIndex)out<<',';
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
out<<'['<<point[0]<<','<<point[1]<<','<<point[2]<<']';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
}
|
||||
if(s.stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV3)
|
||||
out<<",\"rounding_radius_um\":"<<stamp.roundingRadiusMicrometres
|
||||
<<",\"minimum_spacing_um\":"<<stamp.minimumSpacingMicrometres
|
||||
@@ -1134,8 +1156,10 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
}
|
||||
else if(stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV1
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV2
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1, 2 or 3");
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV3
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV4
|
||||
&&stampVocabularyVersion!=ResolvedLayoutStampVocabularyVersionV5)
|
||||
add("stamp_vocabulary_version","resolved stamps require supported vocabulary version 1 through 5");
|
||||
std::vector<std::string> stampIds;
|
||||
for(const auto& stamp:stamps)
|
||||
{
|
||||
@@ -1145,6 +1169,8 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
else stampIds.push_back(stamp.stableId);
|
||||
const bool motif=LayoutStampIsMotif(stamp.kind);
|
||||
if(!LayoutStampKindValid(stamp.kind)||stamp.yawQuarterTurns>3
|
||||
||(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV4)
|
||||
||(motif&&stampVocabularyVersion<ResolvedLayoutStampVocabularyVersionV3))
|
||||
add("stamp_kind","stamp has an unsupported kind or orientation");
|
||||
bool validExtents=true;
|
||||
@@ -1159,6 +1185,45 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
||(stamp.irregularityAmplitudeMicrometres>0
|
||||
&&stamp.irregularityWavelengthMicrometres<=0))
|
||||
add("stamp_parameters",stamp.stableId+" has invalid or unbounded geometry parameters");
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double lengthSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto a=stamp.sweepEndpointsMicrometres[0][axis];
|
||||
const auto b=stamp.sweepEndpointsMicrometres[1][axis];
|
||||
if(a<-1000000000000ll||a>1000000000000ll
|
||||
||b<-1000000000000ll||b>1000000000000ll)
|
||||
add("stamp_sweep",stamp.stableId+" has an unbounded endpoint");
|
||||
const double delta=static_cast<double>(stamp.sweepEndpointsMicrometres[1][axis])
|
||||
-stamp.sweepEndpointsMicrometres[0][axis];
|
||||
lengthSquared+=delta*delta;
|
||||
}
|
||||
if(stamp.sweepPathMicrometres.empty()&&(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*stamp.halfExtentsMicrometres[0]+1.0))
|
||||
add("stamp_sweep",stamp.stableId+" has invalid endpoints or longitudinal extent");
|
||||
if(!stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
bool validPath=stampVocabularyVersion>=ResolvedLayoutStampVocabularyVersionV5
|
||||
&&stamp.sweepPathMicrometres.size()>=2
|
||||
&&stamp.sweepPathMicrometres.size()<=1025
|
||||
&&stamp.sweepPathMicrometres.front()==stamp.sweepEndpointsMicrometres[0]
|
||||
&&stamp.sweepPathMicrometres.back()==stamp.sweepEndpointsMicrometres[1];
|
||||
for(std::size_t pointIndex=0;pointIndex<stamp.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=stamp.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
validPath=validPath&&coordinate>=-1000000000000ll
|
||||
&&coordinate<=1000000000000ll;
|
||||
if(pointIndex>0)
|
||||
validPath=validPath
|
||||
&&stamp.sweepPathMicrometres[pointIndex-1][0]<point[0];
|
||||
}
|
||||
if(!validPath)add("stamp_sweep_path",stamp.stableId
|
||||
+" has an invalid V5 path or non-increasing longitudinal axis");
|
||||
}
|
||||
}
|
||||
if(motif)
|
||||
{
|
||||
const auto minimumExtent=*std::min_element(stamp.halfExtentsMicrometres.begin(),
|
||||
@@ -1186,16 +1251,63 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
const std::int64_t pad=motif
|
||||
?(stamp.smoothWidthMicrometres+5)/6+stamp.irregularityAmplitudeMicrometres
|
||||
:stamp.smoothWidthMicrometres+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]-extents[axis]
|
||||
||stamp.boundsMaximumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]+extents[axis])
|
||||
add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters");
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
{
|
||||
const double expanded=static_cast<double>(extent+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const auto radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
if(stamp.boundsMinimumMicrometres[axis]!=minimum-radius
|
||||
||stamp.boundsMaximumMicrometres[axis]!=maximum+radius)
|
||||
add("stamp_bounds",stamp.stableId+" conservative sweep bounds do not match its parameters");
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const auto radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]!=minimum[axis]-radius
|
||||
||stamp.boundsMaximumMicrometres[axis]!=maximum[axis]+radius)
|
||||
add("stamp_bounds",stamp.stableId
|
||||
+" conservative continuous-sweep bounds do not match its path");
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.boundsMinimumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]-extents[axis]
|
||||
||stamp.boundsMaximumMicrometres[axis]
|
||||
!=stamp.positionMicrometres[axis]+extents[axis])
|
||||
add("stamp_bounds",stamp.stableId+" conservative bounds do not match its parameters");
|
||||
}
|
||||
std::string safetyReason;
|
||||
if(!LayoutStampSafe(*this,stamp,&safetyReason))
|
||||
add("stamp_placement",stamp.stableId+" is unsafe: "+safetyReason);
|
||||
@@ -1497,6 +1609,21 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("position_um",stamp.positionMicrometres);
|
||||
const auto& sweepEndpoints=v.Get("sweep_endpoints_um").AsArray();
|
||||
for(std::size_t endpoint=0;endpoint<2&&endpoint<sweepEndpoints.size();++endpoint)
|
||||
{
|
||||
const auto& values=sweepEndpoints[endpoint].AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
stamp.sweepEndpointsMicrometres[endpoint][axis]=values[axis].Integer();
|
||||
}
|
||||
for(const auto& value:v.Get("sweep_path_um").AsArray())
|
||||
{
|
||||
std::array<std::int64_t,3> point{};
|
||||
const auto& values=value.AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
point[axis]=values[axis].Integer();
|
||||
stamp.sweepPathMicrometres.push_back(point);
|
||||
}
|
||||
readTriplet("bounds_min_um",stamp.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",stamp.boundsMaximumMicrometres);
|
||||
readTriplet("half_extents_um",stamp.halfExtentsMicrometres);
|
||||
@@ -1619,7 +1746,15 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.stampVocabularyVersion=ResolvedLayoutStampVocabularyVersionV1;
|
||||
const bool hasContinuousSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty();});
|
||||
const bool hasSweptDeck=std::any_of(ordered.begin(),ordered.end(),[](const auto& request)
|
||||
{return request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment;});
|
||||
candidate.stampVocabularyVersion=hasContinuousSweptDeck
|
||||
?ResolvedLayoutStampVocabularyVersionV5
|
||||
:hasSweptDeck?ResolvedLayoutStampVocabularyVersionV4
|
||||
:ResolvedLayoutStampVocabularyVersionV1;
|
||||
candidate.stamps.clear();candidate.stamps.reserve(ordered.size());
|
||||
for(std::size_t requestIndex=0;requestIndex<ordered.size();++requestIndex)
|
||||
{
|
||||
@@ -1630,6 +1765,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
if(!LayoutStampKindValid(request.kind)||request.yawQuarterTurns>3
|
||||
||request.placementRadiusMicrometres<0
|
||||
||request.placementRadiusMicrometres>100000000
|
||||
||(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&request.placementRadiusMicrometres!=0)
|
||||
||request.smoothWidthMicrometres<0||request.smoothWidthMicrometres>10000000
|
||||
||request.irregularityAmplitudeMicrometres<0
|
||||
||request.irregularityAmplitudeMicrometres>10000000
|
||||
@@ -1648,6 +1785,43 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
for(const auto coordinate:request.anchorPositionMicrometres)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded anchor";return false;}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&request.sweepPathMicrometres.empty())
|
||||
{
|
||||
double lengthSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto a=request.sweepEndpointsMicrometres[0][axis];
|
||||
const auto b=request.sweepEndpointsMicrometres[1][axis];
|
||||
if(a<-1000000000000ll||a>1000000000000ll
|
||||
||b<-1000000000000ll||b>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded sweep endpoint";return false;}
|
||||
const double delta=static_cast<double>(b)-a;
|
||||
lengthSquared+=delta*delta;
|
||||
}
|
||||
if(!(lengthSquared>0.0)
|
||||
||std::sqrt(lengthSquared)>2.0*request.halfExtentsMicrometres[0]+1.0)
|
||||
{error="stamp request "+request.stableId+" has invalid swept-segment extents";return false;}
|
||||
}
|
||||
if(request.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&!request.sweepPathMicrometres.empty())
|
||||
{
|
||||
if(request.sweepPathMicrometres.size()<2
|
||||
||request.sweepPathMicrometres.size()>1025
|
||||
||request.sweepPathMicrometres.front()!=request.sweepEndpointsMicrometres[0]
|
||||
||request.sweepPathMicrometres.back()!=request.sweepEndpointsMicrometres[1])
|
||||
{error="stamp request "+request.stableId+" has an invalid continuous deck path";return false;}
|
||||
for(std::size_t pointIndex=0;pointIndex<request.sweepPathMicrometres.size();
|
||||
++pointIndex)
|
||||
{
|
||||
const auto& point=request.sweepPathMicrometres[pointIndex];
|
||||
for(const auto coordinate:point)
|
||||
if(coordinate<-1000000000000ll||coordinate>1000000000000ll)
|
||||
{error="stamp request "+request.stableId+" has an unbounded path point";return false;}
|
||||
if(pointIndex>0&&request.sweepPathMicrometres[pointIndex-1][0]>=point[0])
|
||||
{error="stamp request "+request.stableId+" path X coordinates must increase strictly";return false;}
|
||||
}
|
||||
}
|
||||
|
||||
// The shared JSON value type stores numbers as IEEE-754 doubles; keep the
|
||||
// serialized seed in its exact integer range so the canonical round trip holds.
|
||||
@@ -1666,6 +1840,8 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
{
|
||||
ResolvedLayoutStampV1 stamp;stamp.stableId=request.stableId;
|
||||
stamp.strateIndex=request.strateIndex;stamp.kind=request.kind;
|
||||
stamp.sweepEndpointsMicrometres=request.sweepEndpointsMicrometres;
|
||||
stamp.sweepPathMicrometres=request.sweepPathMicrometres;
|
||||
stamp.halfExtentsMicrometres=request.halfExtentsMicrometres;
|
||||
stamp.smoothWidthMicrometres=request.smoothWidthMicrometres;
|
||||
stamp.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
@@ -1687,14 +1863,61 @@ bool ResolveResolvedLayoutStampsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
stamp.positionMicrometres[axis]=request.anchorPositionMicrometres[axis]+offset[axis];
|
||||
const std::int64_t pad=stamp.smoothWidthMicrometres
|
||||
+stamp.irregularityAmplitudeMicrometres;
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&stamp.sweepPathMicrometres.empty())
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
double radiusSquared=0.0;
|
||||
for(const auto extent:stamp.halfExtentsMicrometres)
|
||||
{
|
||||
const double expanded=static_cast<double>(extent+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const std::int64_t radius=static_cast<std::int64_t>(std::ceil(std::sqrt(radiusSquared)));
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const auto minimum=std::min(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
const auto maximum=std::max(stamp.sweepEndpointsMicrometres[0][axis],
|
||||
stamp.sweepEndpointsMicrometres[1][axis]);
|
||||
stamp.boundsMinimumMicrometres[axis]=minimum-radius;
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum+radius;
|
||||
}
|
||||
}
|
||||
else if(stamp.kind==ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
{
|
||||
double radiusSquared=0.0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
{
|
||||
const double expanded=static_cast<double>(stamp.halfExtentsMicrometres[axis]+pad);
|
||||
radiusSquared+=expanded*expanded;
|
||||
}
|
||||
const std::int64_t radius=static_cast<std::int64_t>(
|
||||
std::ceil(std::sqrt(radiusSquared)));
|
||||
std::array<std::int64_t,3> minimum=stamp.sweepPathMicrometres.front();
|
||||
std::array<std::int64_t,3> maximum=minimum;
|
||||
for(const auto& point:stamp.sweepPathMicrometres)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
minimum[axis]=std::min(minimum[axis],point[axis]);
|
||||
maximum[axis]=std::max(maximum[axis],point[axis]);
|
||||
}
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=minimum[axis]-radius;
|
||||
stamp.boundsMaximumMicrometres[axis]=maximum[axis]+radius;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::array<std::int64_t,3> extents={
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?1:0]+pad,
|
||||
stamp.halfExtentsMicrometres[stamp.yawQuarterTurns%2?0:1]+pad,
|
||||
stamp.halfExtentsMicrometres[2]+pad};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
stamp.boundsMinimumMicrometres[axis]=stamp.positionMicrometres[axis]-extents[axis];
|
||||
stamp.boundsMaximumMicrometres[axis]=stamp.positionMicrometres[axis]+extents[axis];
|
||||
}
|
||||
}
|
||||
if(!LayoutStampSafe(candidate,stamp))continue;
|
||||
candidate.stamps.push_back(std::move(stamp));placed=true;break;
|
||||
|
||||
@@ -3535,6 +3535,8 @@ private:
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutStampV1* record=nullptr;
|
||||
std::array<float,3> positionVoxels{},halfExtentsVoxels{};
|
||||
std::array<std::array<float,3>,2> sweepEndpointsVoxels{};
|
||||
std::vector<std::array<float,3>> sweepPathVoxels;
|
||||
float smoothWidthVoxels=0.0f;
|
||||
float irregularityAmplitudeVoxels=0.0f;
|
||||
float irregularityWavelengthVoxels=0.0f;
|
||||
@@ -3700,9 +3702,24 @@ private:
|
||||
StampSpatialBounds.push_back(bound);
|
||||
FStampRuntime runtime;runtime.record=&stamp;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
runtime.positionVoxels[axis]=static_cast<float>(
|
||||
static_cast<double>(stamp.positionMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
runtime.sweepEndpointsVoxels[endpoint][axis]=static_cast<float>(
|
||||
static_cast<double>(stamp.sweepEndpointsMicrometres[endpoint][axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
}
|
||||
runtime.sweepPathVoxels.reserve(stamp.sweepPathMicrometres.size());
|
||||
for(const auto& pointMicrometres:stamp.sweepPathMicrometres)
|
||||
{
|
||||
std::array<float,3> point{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]=static_cast<float>(static_cast<double>(pointMicrometres[axis])
|
||||
/Owner->CoreVoxelSizeMicrons);
|
||||
runtime.sweepPathVoxels.push_back(point);
|
||||
}
|
||||
const std::size_t xAxis=stamp.yawQuarterTurns%2?1:0;
|
||||
const std::size_t yAxis=stamp.yawQuarterTurns%2?0:1;
|
||||
runtime.halfExtentsVoxels={
|
||||
@@ -5162,6 +5179,20 @@ private:
|
||||
}
|
||||
|
||||
public:
|
||||
static std::int32_t& CurrentStampSamplingStep() noexcept
|
||||
{
|
||||
static thread_local std::int32_t Step=0;
|
||||
return Step;
|
||||
}
|
||||
struct FScopedStampSamplingStep
|
||||
{
|
||||
explicit FScopedStampSamplingStep(std::int32_t Step)
|
||||
:Previous(CurrentStampSamplingStep())
|
||||
{CurrentStampSamplingStep()=std::max(0,Step);}
|
||||
~FScopedStampSamplingStep(){CurrentStampSamplingStep()=Previous;}
|
||||
std::int32_t Previous=0;
|
||||
};
|
||||
|
||||
bool GetNativeSurfaceHeightsAt(float X,float Y,int32 ChunkZ,
|
||||
float& OutGroundZ,float& OutCeilingZ) const
|
||||
{
|
||||
@@ -7116,6 +7147,111 @@ private:
|
||||
const float top=std::max(StampEllipsoidSdf(x,y,z,halfSize),-z);
|
||||
return SmoothMinDistance(lower,top,round*2.0f);
|
||||
}
|
||||
static float StampContinuousSweptDeckSdf(const FStampRuntime& stamp,
|
||||
float x,float y,float z)
|
||||
{
|
||||
const auto& path=stamp.sweepPathVoxels;
|
||||
if(path.size()<2)return std::numeric_limits<float>::infinity();
|
||||
const float halfWidth=stamp.halfExtentsVoxels[1];
|
||||
const float halfThickness=stamp.halfExtentsVoxels[2];
|
||||
const float round=std::min(0.35f,std::min(halfWidth,halfThickness)*0.15f);
|
||||
const float searchMargin=halfWidth+halfThickness+2.0f;
|
||||
const auto first=std::lower_bound(path.begin(),path.end(),x-searchMargin,
|
||||
[](const auto& point,float value){return point[0]<value;});
|
||||
const auto last=std::upper_bound(first,path.end(),x+searchMargin,
|
||||
[](float value,const auto& point){return value<point[0];});
|
||||
const std::size_t firstIndex=static_cast<std::size_t>(first-path.begin());
|
||||
const std::size_t lastIndex=static_cast<std::size_t>(last-path.begin());
|
||||
const std::size_t segmentBegin=std::min(path.size()-2,
|
||||
firstIndex==0?std::size_t{0}:firstIndex-1);
|
||||
const std::size_t segmentEnd=std::min(path.size()-2,lastIndex);
|
||||
std::size_t nearestSegment=path.size();
|
||||
float nearestProjection=0.0f;
|
||||
float nearestDistanceSquared=std::numeric_limits<float>::infinity();
|
||||
for(std::size_t segment=segmentBegin;segment<=segmentEnd;++segment)
|
||||
{
|
||||
const auto& a=path[segment];const auto& b=path[segment+1];
|
||||
const float dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const float horizontalSquared=dx*dx+dy*dy;
|
||||
if(!(horizontalSquared>1.0e-10f))continue;
|
||||
const float projection=std::clamp(((x-a[0])*dx+(y-a[1])*dy)
|
||||
/horizontalSquared,0.0f,1.0f);
|
||||
const float nearestX=a[0]+projection*dx;
|
||||
const float nearestY=a[1]+projection*dy;
|
||||
const float offsetX=x-nearestX,offsetY=y-nearestY;
|
||||
const float distanceSquared=offsetX*offsetX+offsetY*offsetY;
|
||||
if(distanceSquared<nearestDistanceSquared
|
||||
||(distanceSquared==nearestDistanceSquared&&segment<nearestSegment))
|
||||
{
|
||||
nearestSegment=segment;
|
||||
nearestProjection=projection;
|
||||
nearestDistanceSquared=distanceSquared;
|
||||
}
|
||||
}
|
||||
if(nearestSegment>=path.size()-1)return std::numeric_limits<float>::infinity();
|
||||
const auto evalCenterline=[&](float parameter,std::array<float,3>& center,
|
||||
std::array<float,3>& tangent)
|
||||
{
|
||||
const auto& p0=path[nearestSegment];
|
||||
const auto& p1=path[nearestSegment+1];
|
||||
const auto& before=path[nearestSegment==0?nearestSegment:nearestSegment-1];
|
||||
const auto& after=path[nearestSegment+2<path.size()
|
||||
?nearestSegment+2:nearestSegment+1];
|
||||
const float t2=parameter*parameter,t3=t2*parameter;
|
||||
const float h00=2.0f*t3-3.0f*t2+1.0f;
|
||||
const float h10=t3-2.0f*t2+parameter;
|
||||
const float h01=-2.0f*t3+3.0f*t2;
|
||||
const float h11=t3-t2;
|
||||
const float d00=6.0f*t2-6.0f*parameter;
|
||||
const float d10=3.0f*t2-4.0f*parameter+1.0f;
|
||||
const float d01=-6.0f*t2+6.0f*parameter;
|
||||
const float d11=3.0f*t2-2.0f*parameter;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const float m0=nearestSegment==0?p1[axis]-p0[axis]
|
||||
:0.5f*(p1[axis]-before[axis]);
|
||||
const float m1=nearestSegment+2>=path.size()?p1[axis]-p0[axis]
|
||||
:0.5f*(after[axis]-p0[axis]);
|
||||
center[axis]=h00*p0[axis]+h10*m0+h01*p1[axis]+h11*m1;
|
||||
tangent[axis]=d00*p0[axis]+d10*m0+d01*p1[axis]+d11*m1;
|
||||
}
|
||||
};
|
||||
std::array<float,3> center{},tangent{};
|
||||
for(int refinement=0;refinement<2;++refinement)
|
||||
{
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float derivativeSquared=tangent[0]*tangent[0]+tangent[1]*tangent[1];
|
||||
if(!(derivativeSquared>1.0e-10f))return std::numeric_limits<float>::infinity();
|
||||
const float offsetX=center[0]-x,offsetY=center[1]-y;
|
||||
nearestProjection=std::clamp(nearestProjection
|
||||
-(offsetX*tangent[0]+offsetY*tangent[1])/derivativeSquared,0.0f,1.0f);
|
||||
}
|
||||
evalCenterline(nearestProjection,center,tangent);
|
||||
const float tangentLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]+tangent[2]*tangent[2]);
|
||||
if(!(tangentLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
for(float& component:tangent)component/=tangentLength;
|
||||
const float horizontalLength=std::sqrt(tangent[0]*tangent[0]
|
||||
+tangent[1]*tangent[1]);
|
||||
if(!(horizontalLength>1.0e-8f))return std::numeric_limits<float>::infinity();
|
||||
const std::array<float,3> side={-tangent[1]/horizontalLength,
|
||||
tangent[0]/horizontalLength,0.0f};
|
||||
const std::array<float,3> up={-tangent[2]*side[1],tangent[2]*side[0],
|
||||
tangent[0]*side[1]-tangent[1]*side[0]};
|
||||
const std::array<float,3> relative={x-center[0],y-center[1],z-center[2]};
|
||||
const auto dot=[&](const std::array<float,3>& axis)
|
||||
{return relative[0]*axis[0]+relative[1]*axis[1]+relative[2]*axis[2];};
|
||||
const float lateral=dot(side),vertical=dot(up);
|
||||
if((nearestSegment==0&&nearestProjection<=0.0f)
|
||||
||(nearestSegment+2==path.size()&&nearestProjection>=1.0f))
|
||||
return StampRoundedBoxSdf(dot(tangent),lateral,vertical,
|
||||
{halfThickness+round,halfWidth,halfThickness},round);
|
||||
const float qSide=std::abs(lateral)-halfWidth+round;
|
||||
const float qUp=std::abs(vertical)-halfThickness+round;
|
||||
const float outside=std::sqrt(std::max(qSide,0.0f)*std::max(qSide,0.0f)
|
||||
+std::max(qUp,0.0f)*std::max(qUp,0.0f));
|
||||
return outside+std::min(std::max(qSide,qUp),0.0f)-round;
|
||||
}
|
||||
float StampSdf(const FStampRuntime& stamp,float x,float y,float z) const
|
||||
{
|
||||
float localX=x-stamp.positionVoxels[0];
|
||||
@@ -7152,6 +7288,40 @@ private:
|
||||
std::min(0.35f,std::min({stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2]})*0.15f));
|
||||
break;
|
||||
case VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment:
|
||||
{
|
||||
if(!stamp.sweepPathVoxels.empty())
|
||||
{
|
||||
distance=StampContinuousSweptDeckSdf(stamp,x,y,z);
|
||||
break;
|
||||
}
|
||||
std::array<float,3> tangent{},side{},up{},delta{};
|
||||
float lengthSquared=0.0f,horizontalSquared=0.0f;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
delta[axis]=stamp.sweepEndpointsVoxels[1][axis]
|
||||
-stamp.sweepEndpointsVoxels[0][axis];
|
||||
lengthSquared+=delta[axis]*delta[axis];
|
||||
if(axis<2)horizontalSquared+=delta[axis]*delta[axis];
|
||||
}
|
||||
const float segmentLength=std::sqrt(lengthSquared);
|
||||
const float horizontalLength=std::sqrt(horizontalSquared);
|
||||
if(!(segmentLength>1.0e-5f&&horizontalLength>1.0e-5f))
|
||||
return std::numeric_limits<float>::infinity();
|
||||
for(std::size_t axis=0;axis<3;++axis)tangent[axis]=delta[axis]/segmentLength;
|
||||
side={-delta[1]/horizontalLength,delta[0]/horizontalLength,0.0f};
|
||||
up={-tangent[2]*side[1],tangent[2]*side[0],
|
||||
tangent[0]*side[1]-tangent[1]*side[0]};
|
||||
const std::array<float,3> offset={
|
||||
x-stamp.positionVoxels[0],y-stamp.positionVoxels[1],z-stamp.positionVoxels[2]};
|
||||
const auto dot=[&](const std::array<float,3>& axis)
|
||||
{return offset[0]*axis[0]+offset[1]*axis[1]+offset[2]*axis[2];};
|
||||
const float rounded=std::min(0.35f,
|
||||
std::min(stamp.halfExtentsVoxels[1],stamp.halfExtentsVoxels[2])*0.15f);
|
||||
distance=StampRoundedBoxSdf(dot(tangent),dot(side),dot(up),
|
||||
stamp.halfExtentsVoxels,rounded);
|
||||
break;
|
||||
}
|
||||
default:return std::numeric_limits<float>::infinity();
|
||||
}
|
||||
if(stamp.irregularityAmplitudeVoxels>0.0f)
|
||||
@@ -7163,6 +7333,15 @@ private:
|
||||
|
||||
static double StampAbsMinimum(double minimum,double maximum)
|
||||
{return minimum<=0.0&&maximum>=0.0?0.0:std::min(std::abs(minimum),std::abs(maximum));}
|
||||
static bool UsesCoarsePillarProxy(const FStampRuntime& stamp,std::int32_t sampleStep)
|
||||
{
|
||||
if(!stamp.record||stamp.record->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillPillar
|
||||
||sampleStep<=0||stamp.irregularityAmplitudeVoxels>0.0f)return false;
|
||||
return static_cast<float>(sampleStep)>std::min(stamp.halfExtentsVoxels[0],
|
||||
stamp.halfExtentsVoxels[1])
|
||||
||static_cast<float>(sampleStep)>stamp.halfExtentsVoxels[2];
|
||||
}
|
||||
static double StampRoundedBoxSdfDouble(const std::array<double,3>& absolute,
|
||||
const std::array<double,3>& halfSize,double rounding)
|
||||
{
|
||||
@@ -7264,6 +7443,7 @@ private:
|
||||
if(index>=StampRuntime.size())return;
|
||||
const auto& runtime=StampRuntime[index];const auto& record=*runtime.record;
|
||||
if(record.strateIndex!=bound->StrateIndex)return;
|
||||
if(UsesCoarsePillarProxy(runtime,CurrentStampSamplingStep()))return;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(worldMicrometres[axis]<record.boundsMinimumMicrometres[axis]
|
||||
||worldMicrometres[axis]>record.boundsMaximumMicrometres[axis])return;
|
||||
@@ -7281,6 +7461,154 @@ private:
|
||||
apply(static_cast<std::uint32_t>(index));
|
||||
return composed;
|
||||
}
|
||||
|
||||
public:
|
||||
std::uint32_t AppendCoarsePillarProxies(FVoxelMeshData& mesh,
|
||||
const FIntVector& tileOrigin,std::int32_t tileExtent,std::int32_t sampleStep) const
|
||||
{
|
||||
if(!Owner||Owner->CoreVoxelSizeMicrons==0||tileExtent<=0||sampleStep<=0
|
||||
||StampRuntime.empty())return 0;
|
||||
constexpr int32 sideCount=24;
|
||||
constexpr double TwoPi=6.2831853071795864769;
|
||||
const int32 originalVertexCount=mesh.Vertices.Num();
|
||||
const int32 originalSurfaceVertices=std::clamp(mesh.NumSurfaceVertices,0,
|
||||
originalVertexCount);
|
||||
const bool hasNormals=mesh.Normals.Num()==originalVertexCount;
|
||||
const bool hasUvs=mesh.UVs.Num()==originalVertexCount;
|
||||
const bool hasColors=mesh.Colors.Num()==originalVertexCount;
|
||||
const double voxelCentimetres=Owner->CoreVoxelSizeMicrons/10000.0;
|
||||
const std::int64_t tileMinimum[3]={tileOrigin.X,tileOrigin.Y,tileOrigin.Z};
|
||||
const std::int64_t tileMaximum[3]={static_cast<std::int64_t>(tileOrigin.X)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Y)+tileExtent,
|
||||
static_cast<std::int64_t>(tileOrigin.Z)+tileExtent};
|
||||
std::vector<int32> proxyIndices;
|
||||
std::uint32_t proxyCount=0;
|
||||
const auto addVertex=[&](const FVector& position,const FVector& normal,
|
||||
const FVector2f& uv)
|
||||
{
|
||||
const int32 index=mesh.Vertices.Add(position);
|
||||
if(hasNormals)mesh.Normals.Add(normal);
|
||||
if(hasUvs)mesh.UVs.Add(uv);
|
||||
if(hasColors)mesh.Colors.Add(FColor::White);
|
||||
return index;
|
||||
};
|
||||
for(const auto& stamp:StampRuntime)
|
||||
{
|
||||
if(!UsesCoarsePillarProxy(stamp,sampleStep))continue;
|
||||
const auto& center=stamp.positionVoxels;
|
||||
if(center[0]<tileMinimum[0]||center[0]>=tileMaximum[0]
|
||||
||center[1]<tileMinimum[1]||center[1]>=tileMaximum[1]
|
||||
||center[2]<tileMinimum[2]||center[2]>=tileMaximum[2])continue;
|
||||
const double radiusX=stamp.halfExtentsVoxels[0];
|
||||
const double radiusY=stamp.halfExtentsVoxels[1];
|
||||
const double bottomVoxel=center[2]-stamp.halfExtentsVoxels[2]-1.0;
|
||||
const double topVoxel=center[2]+stamp.halfExtentsVoxels[2];
|
||||
const auto point=[&](double angle,double z)
|
||||
{
|
||||
return FVector((center[0]+radiusX*std::cos(angle))*voxelCentimetres,
|
||||
(center[1]+radiusY*std::sin(angle))*voxelCentimetres,
|
||||
z*voxelCentimetres);
|
||||
};
|
||||
const int32 capTopCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,topVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,1.0),FVector2f(0.5f,0.5f));
|
||||
const int32 capBottomCenter=addVertex(FVector(center[0]*voxelCentimetres,
|
||||
center[1]*voxelCentimetres,bottomVoxel*voxelCentimetres),
|
||||
FVector(0.0,0.0,-1.0),FVector2f(0.5f,0.5f));
|
||||
for(int32 sideIndex=0;sideIndex<sideCount;++sideIndex)
|
||||
{
|
||||
const double u0=static_cast<double>(sideIndex)/sideCount;
|
||||
const double u1=static_cast<double>(sideIndex+1)/sideCount;
|
||||
const double a0=TwoPi*u0,a1=TwoPi*u1;
|
||||
const FVector n0=FVector(std::cos(a0)/radiusX,
|
||||
std::sin(a0)/radiusY,0.0).GetSafeNormal();
|
||||
const FVector n1=FVector(std::cos(a1)/radiusX,
|
||||
std::sin(a1)/radiusY,0.0).GetSafeNormal();
|
||||
const int32 bottomA=addVertex(point(a0,bottomVoxel),n0,FVector2f(u0,0.0f));
|
||||
const int32 bottomB=addVertex(point(a1,bottomVoxel),n1,FVector2f(u1,0.0f));
|
||||
const int32 topB=addVertex(point(a1,topVoxel),n1,FVector2f(u1,1.0f));
|
||||
const int32 topA=addVertex(point(a0,topVoxel),n0,FVector2f(u0,1.0f));
|
||||
proxyIndices.insert(proxyIndices.end(),{bottomA,bottomB,topB,
|
||||
bottomA,topB,topA});
|
||||
|
||||
const int32 topAIndex=addVertex(point(a0,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 topBIndex=addVertex(point(a1,topVoxel),FVector(0.0,0.0,1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),{capTopCenter,topAIndex,topBIndex});
|
||||
|
||||
const int32 bottomAIndex=addVertex(point(a0,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a0)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a0))));
|
||||
const int32 bottomBIndex=addVertex(point(a1,bottomVoxel),FVector(0.0,0.0,-1.0),
|
||||
FVector2f(static_cast<float>(0.5+0.5*std::cos(a1)),
|
||||
static_cast<float>(0.5+0.5*std::sin(a1))));
|
||||
proxyIndices.insert(proxyIndices.end(),
|
||||
{capBottomCenter,bottomBIndex,bottomAIndex});
|
||||
}
|
||||
++proxyCount;
|
||||
}
|
||||
if(proxyIndices.empty())return 0;
|
||||
const int32 proxyVertexCount=mesh.Vertices.Num()-originalVertexCount;
|
||||
const auto insertProxyVertices=[&](auto& values)
|
||||
{
|
||||
if(values.Num()!=originalVertexCount+proxyVertexCount)return;
|
||||
using FArray=std::remove_reference_t<decltype(values)>;
|
||||
FArray reordered;
|
||||
reordered.Reserve(values.Num());
|
||||
for(int32 index=0;index<originalSurfaceVertices;++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalVertexCount;index<values.Num();++index)
|
||||
reordered.Add(values[index]);
|
||||
for(int32 index=originalSurfaceVertices;index<originalVertexCount;++index)
|
||||
reordered.Add(values[index]);
|
||||
values=MoveTemp(reordered);
|
||||
};
|
||||
insertProxyVertices(mesh.Vertices);
|
||||
if(hasNormals)insertProxyVertices(mesh.Normals);
|
||||
if(hasUvs)insertProxyVertices(mesh.UVs);
|
||||
if(hasColors)insertProxyVertices(mesh.Colors);
|
||||
|
||||
const int32 oldTriangleCount=mesh.Triangles.Num()/3;
|
||||
const int32 ceilingStart=std::max(0,mesh.Triangles.Num()-mesh.NumCeilingTriangles*3);
|
||||
const int32 groundTriangleCount=ceilingStart/3;
|
||||
const int32 proxyTriangleCount=static_cast<int32>(proxyIndices.size()/3);
|
||||
TArray<int32> mergedIndices;
|
||||
mergedIndices.Reserve(mesh.Triangles.Num()+static_cast<int32>(proxyIndices.size()));
|
||||
for(int32 index=0;index<ceilingStart;++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
for(const int32 index:proxyIndices)
|
||||
mergedIndices.Add(originalSurfaceVertices+index-originalVertexCount);
|
||||
for(int32 index=ceilingStart;index<mesh.Triangles.Num();++index)
|
||||
{
|
||||
const int32 oldIndex=mesh.Triangles[index];
|
||||
mergedIndices.Add(oldIndex>=originalSurfaceVertices
|
||||
?oldIndex+proxyVertexCount:oldIndex);
|
||||
}
|
||||
mesh.Triangles=MoveTemp(mergedIndices);
|
||||
|
||||
TArray<uint8> mergedMasks;
|
||||
mergedMasks.Reserve(oldTriangleCount+proxyTriangleCount);
|
||||
for(int32 triangle=0;triangle<groundTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
for(int32 triangle=0;triangle<proxyTriangleCount;++triangle)mergedMasks.Add(0);
|
||||
if(groundTriangleCount<oldTriangleCount)
|
||||
for(int32 triangle=groundTriangleCount;triangle<oldTriangleCount;++triangle)
|
||||
mergedMasks.Add(mesh.TriangleSkirtFaceMasks.IsValidIndex(triangle)
|
||||
?mesh.TriangleSkirtFaceMasks[triangle]:0);
|
||||
mesh.TriangleSkirtFaceMasks=MoveTemp(mergedMasks);
|
||||
mesh.NumSurfaceVertices=originalSurfaceVertices+proxyVertexCount;
|
||||
return proxyCount;
|
||||
}
|
||||
|
||||
private:
|
||||
float ApplyResolvedCavernGraph(float density,float X,float Y,float Z,
|
||||
float* nativeFloorSupport=nullptr) const
|
||||
{
|
||||
|
||||
@@ -2889,12 +2889,16 @@ namespace VoxelForgeCore
|
||||
VoxelForgeCore::Heightfield2D::threadColumnCacheStats().columnComputeMilliseconds;
|
||||
const auto MarchingCubesStart=std::chrono::steady_clock::now();
|
||||
FVoxelUniformBlockProofAuditStats BlockProofAudit;
|
||||
FVoxelMeshData mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
FVoxelMeshData mesh;
|
||||
{
|
||||
FStandaloneShapeWorld::FScopedStampSamplingStep stampSamplingStep(MeshStep);
|
||||
mesh=mesher.GenerateMesh(origin,MeshStep,MeshCells,nullptr,
|
||||
MeshBandMinZVoxel,MeshBandMaxZVoxel,&sampleCount,nullptr,nullptr,
|
||||
(options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr,
|
||||
options.profileMesherStages?&McStageTimings:nullptr,
|
||||
options.auditUniformBlockProofs?&BlockProofAudit:nullptr,
|
||||
options.forceFullDensityCapture);
|
||||
}
|
||||
passageFeatureScope.copySweptTubeRuntimeCounts();
|
||||
if(options.auditUniformBlockProofs)
|
||||
{
|
||||
@@ -3107,6 +3111,13 @@ namespace VoxelForgeCore
|
||||
AppendStratumBoundaryFloors(world,farShape,nativeKey,step,mesh);
|
||||
AppendStratumBoundaryCeilings(world,farShape,nativeKey,step,mesh);
|
||||
}
|
||||
const std::uint32_t coarsePillarProxyCount=tileShape.AppendCoarsePillarProxies(
|
||||
mesh,origin,extent,MeshStep);
|
||||
if(coarsePillarProxyCount>0)
|
||||
std::printf("[CoarsePillarProxy] key=(%d,%d,%d,lod%d) sampling_step_vox=%d "
|
||||
"pillars=%u mode=exact_capped_cylinder status=EMITTED\n",
|
||||
key.coordinate.x,key.coordinate.y,key.coordinate.z,key.lod,MeshStep,
|
||||
coarsePillarProxyCount);
|
||||
l77RefineStage.Restore();
|
||||
|
||||
out.densitySampleCount=static_cast<std::uint64_t>(std::max<int64>(0,sampleCount));
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# L133 — native floating islands
|
||||
|
||||
Stopped after the second route design. The native v9 plan binds the owner’s resolved mouths and passes raw route audit, strict layout (0 issues), 3/3 interface audits, shell (0 floor violations), and ripple (3/3). The candidate dispatches FI natively.
|
||||
|
||||
The composed FI walk still fails: 2 capsule collisions, 40 unsupported centers, 0 grade failures; mouth binding passes. The return lane is carved by `passage/0-1/0` near `(19.755, 9.605, -733.553) m` (4.25 m from its 4.5 m tube). At `passage/1-2/0`, the approach has collisions at `(14.402, -10.552, -732.556) m` and `(14.242, -10.712, -732.556) m`; the departure landing probe `(19.166, -14.650, -733.331) m` gets 0/5 support probes. The exact endpoints alone are not enough to place a route clear of both composed passage volumes and retain a supported landing.
|
||||
|
||||
No captures, timing matrix, compiler determinism matrix, baseline update, or UE host build were run after the required composed walk gate failed. The owner’s default import remains on its legacy FI fallback. Logs and candidate fixtures are under `Saved/Linux/l133/`.
|
||||
@@ -1,511 +0,0 @@
|
||||
diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||||
index cb2b2dd..5a94ab8 100644
|
||||
--- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||||
+++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h
|
||||
@@ -496,8 +496,9 @@ namespace VoxelForgeCore
|
||||
// continuous floor while relaxing the spline chord tolerance and bounding shell work.
|
||||
// Version 226 unifies the passage influence reach in metres and converts it to core
|
||||
// voxels for every broadphase and proof bound. Version 227 uses the exact smooth-max
|
||||
- // support width/6 when deriving that reach.
|
||||
- inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 227;
|
||||
+ // support width/6 when deriving that reach. Version 228 permits the native FI route
|
||||
+ // planner to bind its fixed endpoints to the resolved passage mouths.
|
||||
+ inline constexpr std::uint32_t WorldAssemblyEvaluatorVersion = 228;
|
||||
|
||||
enum class FieldChannel : std::uint32_t
|
||||
{
|
||||
@@ -788,6 +789,15 @@ namespace VoxelForgeCore
|
||||
std::uint64_t contentHash = 0;
|
||||
};
|
||||
|
||||
+ // The two resolved inter-strate walk points owned by a FloatingIslands slot.
|
||||
+ // The route planner treats these as fixed player-capsule centers and places
|
||||
+ // the island chain between them.
|
||||
+ struct FloatingIslandRouteEndpointsV1
|
||||
+ {
|
||||
+ std::array<std::int64_t, 3> arrivalSubvoxels{};
|
||||
+ std::array<std::int64_t, 3> departureSubvoxels{};
|
||||
+ };
|
||||
+
|
||||
struct WorldAssemblyFeatureGroupBoundsV1
|
||||
{
|
||||
std::array<std::int64_t, 3> minimumSubvoxels{};
|
||||
@@ -1377,6 +1387,9 @@ namespace VoxelForgeCore
|
||||
// Planner output is ordinary reviewed data; clients can load it without re-running the planner.
|
||||
VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
const FloatingIslandRecipeV1& recipe, std::string& error);
|
||||
+ VFCORE_API std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
+ const FloatingIslandRecipeV1& recipe,
|
||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error);
|
||||
VFCORE_API FloatingIslandBoundsAuditResult AuditFloatingIslandBounds(
|
||||
const FloatingIslandFieldV1& field, std::uint32_t boxCount = 256,
|
||||
std::uint64_t seed = 0x49534c414e444231ull);
|
||||
diff --git a/Standalone/Core/src/FloatingIslandsInternal.h b/Standalone/Core/src/FloatingIslandsInternal.h
|
||||
index 33d97d6..42f855a 100644
|
||||
--- a/Standalone/Core/src/FloatingIslandsInternal.h
|
||||
+++ b/Standalone/Core/src/FloatingIslandsInternal.h
|
||||
@@ -285,7 +285,7 @@ namespace floating_island_detail
|
||||
? recipe.ownerVoxelSizeMicrons / ScaleGcd : 0;
|
||||
const std::uint32_t ExpectedDenominator = ScaleGcd > 0
|
||||
? recipe.coreVoxelSizeMicrons / ScaleGcd : 0;
|
||||
- if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 8
|
||||
+ if (recipe.schemaVersion != 1 || recipe.geometryVersion < 1 || recipe.geometryVersion > 9
|
||||
|| (recipe.geometryVersion == 1
|
||||
&& (recipe.crustDepthBonusQ16 != 0 || recipe.rootDepthRatioQ16 != 0))
|
||||
|| recipe.unitsPerVoxel != Q
|
||||
@@ -1473,7 +1473,8 @@ namespace floating_island_detail
|
||||
static_cast<double>(anchorIsland->centerSubvoxels[0]),
|
||||
static_cast<double>(anchorIsland->centerSubvoxels[1]));
|
||||
if(anchorDistance>recipe.anchorPlacementRadiusSubvoxels
|
||||
- ||recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels
|
||||
+ ||(recipe.geometryVersion<8
|
||||
+ &&recipe.anchorRootBottomZSubvoxels>=anchorIsland->bottomZSubvoxels)
|
||||
||recipe.anchorMouthOffsetSubvoxels+recipe.routeConstraints.capsuleRadiusSubvoxels
|
||||
>=anchorIsland->radiusSubvoxels)
|
||||
{error="floating-island anchor island or its mouth leaves the passage radius or rock envelope";return false;}
|
||||
@@ -1607,7 +1608,7 @@ namespace floating_island_detail
|
||||
||to.ordinal==recipe.anchorIslandOrdinal);
|
||||
const bool westernSupportConnector=WorldAssemblyEvaluatorVersion>=178
|
||||
&&((from.ordinal==0&&to.ordinal==1)||(from.ordinal==1&&to.ordinal==0));
|
||||
- const double routeDeckLift=recipe.geometryVersion>=2
|
||||
+ const double routeDeckLift=recipe.geometryVersion>=9?0.0:recipe.geometryVersion>=2
|
||||
?(WorldAssemblyEvaluatorVersion>=178
|
||||
?(westernSupportConnector
|
||||
?(WorldAssemblyEvaluatorVersion>=181?-1.0
|
||||
@@ -2264,6 +2265,335 @@ std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
return manifest;
|
||||
}
|
||||
|
||||
+std::optional<FloatingIslandManifestV1> PlanFloatingIslandCluster(
|
||||
+ const FloatingIslandRecipeV1& inputRecipe,
|
||||
+ const FloatingIslandRouteEndpointsV1& endpoints, std::string& error)
|
||||
+{
|
||||
+ FloatingIslandManifestV1 manifest;
|
||||
+ manifest.recipe = inputRecipe;
|
||||
+ auto& recipe = manifest.recipe;
|
||||
+ recipe.geometryVersion = 9;
|
||||
+ recipe.primaryIslandCount = 9;
|
||||
+ recipe.anchorIslandOrdinal = recipe.primaryIslandCount - 1;
|
||||
+ constexpr std::int64_t step = 192 * floating_island_detail::Q;
|
||||
+ recipe.spacingSubvoxels = step;
|
||||
+ if (recipe.routeConstraints.version == 0)
|
||||
+ {
|
||||
+ recipe.routeConstraints.version = 1;
|
||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels = 348;
|
||||
+ recipe.routeConstraints.capsuleHeightSubvoxels = 1802;
|
||||
+ recipe.routeConstraints.maximumGradeQ16 = 17560;
|
||||
+ recipe.routeConstraints.sweepSpacingSubvoxels = 128;
|
||||
+ recipe.routeConstraints.supportProbeRadiusSubvoxels = 230;
|
||||
+ recipe.routeConstraints.supportSearchDepthSubvoxels = 512;
|
||||
+ recipe.routeConstraints.minimumClearanceSubvoxels = 128;
|
||||
+ recipe.routeConstraints.minimumSupportProbes = 3;
|
||||
+ }
|
||||
+ for (const auto& value : endpoints.arrivalSubvoxels)
|
||||
+ if (!floating_island_detail::ValidScalar(value))
|
||||
+ { error = "resolved floating-island arrival mouth is outside the fixed-point range"; return std::nullopt; }
|
||||
+ for (const auto& value : endpoints.departureSubvoxels)
|
||||
+ if (!floating_island_detail::ValidScalar(value))
|
||||
+ { error = "resolved floating-island departure mouth is outside the fixed-point range"; return std::nullopt; }
|
||||
+
|
||||
+ const double qPerMetre = static_cast<double>(floating_island_detail::Q)
|
||||
+ * 1000000.0 / recipe.coreVoxelSizeMicrons;
|
||||
+ const double dx = static_cast<double>(endpoints.departureSubvoxels[0]
|
||||
+ - endpoints.arrivalSubvoxels[0]);
|
||||
+ const double dy = static_cast<double>(endpoints.departureSubvoxels[1]
|
||||
+ - endpoints.arrivalSubvoxels[1]);
|
||||
+ const double endpointDistance = std::hypot(dx, dy);
|
||||
+ const std::int64_t maximumMouthOffset = 64 * floating_island_detail::Q
|
||||
+ - recipe.routeConstraints.capsuleRadiusSubvoxels - floating_island_detail::Q;
|
||||
+ const std::int64_t desiredMouthOffset = std::max(recipe.anchorMouthOffsetSubvoxels,
|
||||
+ static_cast<std::int64_t>(std::llround(qPerMetre * 6.6)));
|
||||
+ recipe.anchorMouthOffsetSubvoxels = std::min({desiredMouthOffset,
|
||||
+ maximumMouthOffset, static_cast<std::int64_t>(std::llround(endpointDistance))});
|
||||
+ if (recipe.anchorMouthOffsetSubvoxels <= 0)
|
||||
+ { error = "resolved island mouths cannot provide a bounded departure approach"; return std::nullopt; }
|
||||
+ recipe.anchorRootRadiusSubvoxels = std::max(recipe.anchorRootRadiusSubvoxels,
|
||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
||||
+ + floating_island_detail::Q);
|
||||
+ recipe.anchorIslandMinimumRadiusSubvoxels = std::max(recipe.minimumRadiusSubvoxels,
|
||||
+ recipe.anchorMouthOffsetSubvoxels + recipe.routeConstraints.capsuleRadiusSubvoxels
|
||||
+ + floating_island_detail::Q + 1);
|
||||
+ // Keep the resolved fixed mouth on a capsule-grade ramp at the anchor island lip.
|
||||
+ recipe.topFlattenQ16 = std::max<std::int64_t>(recipe.topFlattenQ16, 52429);
|
||||
+ recipe.bridgeHalfWidthSubvoxels = std::max(recipe.bridgeHalfWidthSubvoxels,
|
||||
+ recipe.routeConstraints.capsuleRadiusSubvoxels + 2 * floating_island_detail::Q);
|
||||
+ if (!floating_island_detail::ValidateRecipe(recipe, error)) return std::nullopt;
|
||||
+
|
||||
+ auto& islands = manifest.plan.islands;
|
||||
+ auto& edges = manifest.plan.clusterEdges;
|
||||
+ auto& route = manifest.plan.route;
|
||||
+ constexpr std::uint32_t count = 9;
|
||||
+ double tangentX = endpointDistance > 1.0
|
||||
+ ? dx / endpointDistance : 1.0;
|
||||
+ double tangentY = endpointDistance > 1.0
|
||||
+ ? dy / endpointDistance : 0.0;
|
||||
+ if (endpointDistance <= 1.0)
|
||||
+ {
|
||||
+ const double angle = static_cast<double>(floating_island_detail::UnitQ16(
|
||||
+ recipe.seed, 0x900000000ull, 0x524f555445ull))
|
||||
+ * (2.0 * floating_island_detail::Pi / 65536.0);
|
||||
+ tangentX = std::cos(angle);
|
||||
+ tangentY = std::sin(angle);
|
||||
+ }
|
||||
+ const double normalX = -tangentY;
|
||||
+ const double normalY = tangentX;
|
||||
+ const auto localPoint = [&](double along, double side)
|
||||
+ {
|
||||
+ return std::array<std::int64_t, 2>{
|
||||
+ floating_island_detail::RoundQ(
|
||||
+ static_cast<double>(endpoints.arrivalSubvoxels[0])
|
||||
+ + tangentX * along + normalX * side),
|
||||
+ floating_island_detail::RoundQ(
|
||||
+ static_cast<double>(endpoints.arrivalSubvoxels[1])
|
||||
+ + tangentY * along + normalY * side)};
|
||||
+ };
|
||||
+ std::array<std::array<std::int64_t, 2>, count> primaryXY{};
|
||||
+ primaryXY[0] = {endpoints.arrivalSubvoxels[0], endpoints.arrivalSubvoxels[1]};
|
||||
+ primaryXY[1] = localPoint(0.0, static_cast<double>(step));
|
||||
+ primaryXY[2] = localPoint(static_cast<double>(step), static_cast<double>(step));
|
||||
+ primaryXY[3] = localPoint(static_cast<double>(step), 0.0);
|
||||
+ primaryXY[4] = localPoint(static_cast<double>(step), -static_cast<double>(step));
|
||||
+ primaryXY[5] = localPoint(0.0, -static_cast<double>(step));
|
||||
+ primaryXY[6] = localPoint(-static_cast<double>(step), -static_cast<double>(step));
|
||||
+ // Keep the return lane outside the arrival passage's spiral and on the negative
|
||||
+ // side of the first connector, so the two bridges cannot cross in plan view.
|
||||
+ // Keep the final approach radial at the passage mouth. This lets the shared
|
||||
+ // passage resolver keep its authored route inside the placement disk.
|
||||
+ primaryXY[7] = localPoint(-0.85 * static_cast<double>(step),
|
||||
+ -0.23 * static_cast<double>(step));
|
||||
+ const double departureRadial = std::hypot(
|
||||
+ static_cast<double>(endpoints.departureSubvoxels[0]),
|
||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
||||
+ const double anchorScale = departureRadial > 1.0
|
||||
+ ? std::max(0.0, (departureRadial - recipe.anchorMouthOffsetSubvoxels) / departureRadial)
|
||||
+ : 0.0;
|
||||
+ primaryXY[8] = {
|
||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[0] * anchorScale),
|
||||
+ floating_island_detail::RoundQ(endpoints.departureSubvoxels[1] * anchorScale)};
|
||||
+
|
||||
+ std::array<double, count> cumulative{};
|
||||
+ for (std::uint32_t i = 1; i < count; ++i)
|
||||
+ {
|
||||
+ const double segmentX = static_cast<double>(primaryXY[i][0] - primaryXY[i - 1][0]);
|
||||
+ const double segmentY = static_cast<double>(primaryXY[i][1] - primaryXY[i - 1][1]);
|
||||
+ const double length = std::hypot(segmentX, segmentY);
|
||||
+ if (!(length > 0.0) || length > static_cast<double>(256 * floating_island_detail::Q))
|
||||
+ { error = "resolved island mouths require an unsupported serpentine segment length"; return std::nullopt; }
|
||||
+ cumulative[i] = cumulative[i - 1] + length;
|
||||
+ }
|
||||
+ const double totalLength = cumulative.back();
|
||||
+ if (!(totalLength > 0.0))
|
||||
+ { error = "resolved floating-island mouths do not define a route"; return std::nullopt; }
|
||||
+ const std::int64_t capsuleHalf = recipe.routeConstraints.capsuleHeightSubvoxels / 2;
|
||||
+ const std::int64_t routeOffset = capsuleHalf
|
||||
+ + recipe.routeConstraints.minimumClearanceSubvoxels
|
||||
+ + floating_island_detail::Q / 8;
|
||||
+ const std::int64_t startTop = endpoints.arrivalSubvoxels[2] - routeOffset;
|
||||
+ const std::int64_t endTop = endpoints.departureSubvoxels[2] - routeOffset;
|
||||
+ const std::uint64_t anchorSite = 0x700000000ull + recipe.anchorIslandOrdinal;
|
||||
+ auto anchorShape = floating_island_detail::MakeIsland(recipe,
|
||||
+ recipe.anchorIslandOrdinal, primaryXY.back()[0], primaryXY.back()[1], endTop, anchorSite);
|
||||
+ anchorShape.radiusSubvoxels = std::max(anchorShape.radiusSubvoxels,
|
||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
||||
+ anchorShape.topHalfSubvoxels = std::max<std::int64_t>(1,
|
||||
+ anchorShape.radiusSubvoxels / 5);
|
||||
+ anchorShape.centerSubvoxels[2] = endTop - anchorShape.topHalfSubvoxels;
|
||||
+ std::int64_t anchorTop = endTop;
|
||||
+ for (int iteration = 0; iteration < 4; ++iteration)
|
||||
+ {
|
||||
+ anchorShape.topZSubvoxels = anchorTop;
|
||||
+ anchorShape.centerSubvoxels[2] = anchorTop - anchorShape.topHalfSubvoxels;
|
||||
+ const double mouthSurface = floating_island_detail::IslandTopSurface(recipe,
|
||||
+ anchorShape, static_cast<double>(endpoints.departureSubvoxels[0]),
|
||||
+ static_cast<double>(endpoints.departureSubvoxels[1]));
|
||||
+ anchorTop = endTop + floating_island_detail::RoundQ(
|
||||
+ static_cast<double>(anchorTop) - mouthSurface);
|
||||
+ }
|
||||
+ std::array<std::int64_t, count> tops{};
|
||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
||||
+ {
|
||||
+ const double t = cumulative[i] / totalLength;
|
||||
+ tops[i] = floating_island_detail::RoundQ(static_cast<double>(startTop)
|
||||
+ + static_cast<double>(anchorTop - startTop) * t);
|
||||
+ }
|
||||
+
|
||||
+ islands.reserve(count);
|
||||
+ for (std::uint32_t i = 0; i < count; ++i)
|
||||
+ {
|
||||
+ const std::uint64_t site = 0x700000000ull + i;
|
||||
+ auto island = floating_island_detail::MakeIsland(recipe, i,
|
||||
+ primaryXY[i][0], primaryXY[i][1], tops[i], site);
|
||||
+ if (i == recipe.anchorIslandOrdinal)
|
||||
+ {
|
||||
+ island.radiusSubvoxels = std::max(island.radiusSubvoxels,
|
||||
+ recipe.anchorIslandMinimumRadiusSubvoxels);
|
||||
+ }
|
||||
+ const std::int64_t interiorBottom = recipe.layoutMinimumZSubvoxels
|
||||
+ + recipe.boundarySealSubvoxels;
|
||||
+ while (island.radiusSubvoxels > recipe.minimumRadiusSubvoxels
|
||||
+ && floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
||||
+ < interiorBottom)
|
||||
+ --island.radiusSubvoxels;
|
||||
+ const std::int64_t requiredRadius = i == recipe.anchorIslandOrdinal
|
||||
+ ? recipe.anchorIslandMinimumRadiusSubvoxels : recipe.minimumRadiusSubvoxels;
|
||||
+ if (island.radiusSubvoxels < requiredRadius
|
||||
+ || floating_island_detail::IslandBottom(recipe, tops[i], island.radiusSubvoxels)
|
||||
+ < interiorBottom)
|
||||
+ {
|
||||
+ error = "resolved-mouth island " + std::to_string(i) + " cannot fit above sealed bedrock"
|
||||
+ + " (radius=" + std::to_string(island.radiusSubvoxels)
|
||||
+ + ", required=" + std::to_string(requiredRadius)
|
||||
+ + ", top=" + std::to_string(tops[i])
|
||||
+ + ", bottom=" + std::to_string(floating_island_detail::IslandBottom(
|
||||
+ recipe, tops[i], island.radiusSubvoxels))
|
||||
+ + ", interior_bottom=" + std::to_string(interiorBottom)
|
||||
+ + ", anchor_top=" + std::to_string(anchorTop)
|
||||
+ + ", mouth_offset=" + std::to_string(recipe.anchorMouthOffsetSubvoxels) + ")";
|
||||
+ return std::nullopt;
|
||||
+ }
|
||||
+ island.topHalfSubvoxels = std::max<std::int64_t>(1, island.radiusSubvoxels / 5);
|
||||
+ island.topZSubvoxels = tops[i];
|
||||
+ island.centerSubvoxels[2] = tops[i] - island.topHalfSubvoxels;
|
||||
+ island.bottomZSubvoxels = floating_island_detail::IslandBottom(recipe,
|
||||
+ island.topZSubvoxels, island.radiusSubvoxels);
|
||||
+ floating_island_detail::SetIslandBounds(recipe, island);
|
||||
+ islands.push_back(std::move(island));
|
||||
+ }
|
||||
+ for (std::uint32_t i = 0; i + 1 < count; ++i)
|
||||
+ edges.push_back(floating_island_detail::MakeConnector(recipe,
|
||||
+ islands[i], islands[i + 1], 0x600000000ull + i));
|
||||
+
|
||||
+ FloatingIslandFieldV1 planningField(recipe, manifest.plan);
|
||||
+ if (!planningField.isValid())
|
||||
+ { error = "resolved-mouth island field did not pass the Core field contract"; return std::nullopt; }
|
||||
+ const std::int64_t routeRadius = 4 * floating_island_detail::Q;
|
||||
+ std::uint32_t ordinal = 0;
|
||||
+ for (std::uint32_t segment = 0; segment + 1 < count; ++segment)
|
||||
+ {
|
||||
+ const double segmentX = static_cast<double>(primaryXY[segment + 1][0]
|
||||
+ - primaryXY[segment][0]);
|
||||
+ const double segmentY = static_cast<double>(primaryXY[segment + 1][1]
|
||||
+ - primaryXY[segment][1]);
|
||||
+ const double length = std::hypot(segmentX, segmentY);
|
||||
+ const std::uint32_t pieces = static_cast<std::uint32_t>(std::max(1.0,
|
||||
+ std::ceil(length / (4.0 * floating_island_detail::Q))));
|
||||
+ for (std::uint32_t piece = segment == 0 ? 0 : 1; piece <= pieces; ++piece)
|
||||
+ {
|
||||
+ const double t = static_cast<double>(piece) / pieces;
|
||||
+ const auto x = floating_island_detail::RoundQ(
|
||||
+ static_cast<double>(primaryXY[segment][0]) + segmentX * t);
|
||||
+ const auto y = floating_island_detail::RoundQ(
|
||||
+ static_cast<double>(primaryXY[segment][1]) + segmentY * t);
|
||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
||||
+ recipe, manifest.plan, x, y);
|
||||
+ if (!surface)
|
||||
+ { error = "resolved-mouth island chain has no capsule-width support along a connector"; return std::nullopt; }
|
||||
+ WorldAssemblyRoutePointV1 point;
|
||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
||||
+ point.radiusSubvoxels = routeRadius;
|
||||
+ if (segment == 0 && piece == 0)
|
||||
+ point.positionSubvoxels = endpoints.arrivalSubvoxels;
|
||||
+ route.push_back(point);
|
||||
+ ++ordinal;
|
||||
+ }
|
||||
+ }
|
||||
+ const double mouthLegX = static_cast<double>(endpoints.departureSubvoxels[0]
|
||||
+ - primaryXY.back()[0]);
|
||||
+ const double mouthLegY = static_cast<double>(endpoints.departureSubvoxels[1]
|
||||
+ - primaryXY.back()[1]);
|
||||
+ const double mouthLegLength = std::hypot(mouthLegX, mouthLegY);
|
||||
+ const std::uint32_t mouthLegPieces = static_cast<std::uint32_t>(std::max(1.0,
|
||||
+ std::ceil(mouthLegLength / (4.0 * floating_island_detail::Q))));
|
||||
+ for (std::uint32_t piece = 1; piece <= mouthLegPieces; ++piece)
|
||||
+ {
|
||||
+ const double t = static_cast<double>(piece) / mouthLegPieces;
|
||||
+ const auto x = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[0])
|
||||
+ + mouthLegX * t);
|
||||
+ const auto y = floating_island_detail::RoundQ(static_cast<double>(primaryXY.back()[1])
|
||||
+ + mouthLegY * t);
|
||||
+ const auto surface = floating_island_detail::FindCapsuleFootingTopZ(
|
||||
+ recipe, manifest.plan, x, y);
|
||||
+ if (!surface)
|
||||
+ { error = "resolved island passage mouth has no capsule-width support along its anchor landing at x="
|
||||
+ + std::to_string(x) + ", y=" + std::to_string(y) + ", piece="
|
||||
+ + std::to_string(piece) + "/" + std::to_string(mouthLegPieces)
|
||||
+ + ", anchor_radius=" + std::to_string(anchorShape.radiusSubvoxels); return std::nullopt; }
|
||||
+ WorldAssemblyRoutePointV1 point;
|
||||
+ point.stableFeatureId = floating_island_detail::StableId(recipe.seed,
|
||||
+ 0x800000000ull + ordinal, 0x524f555445ull);
|
||||
+ point.positionSubvoxels = {x, y, *surface + routeOffset};
|
||||
+ point.radiusSubvoxels = routeRadius;
|
||||
+ if (piece == mouthLegPieces)
|
||||
+ point.positionSubvoxels = endpoints.departureSubvoxels;
|
||||
+ route.push_back(point);
|
||||
+ ++ordinal;
|
||||
+ }
|
||||
+
|
||||
+ const double maximumGrade = floating_island_detail::Q16ToDouble(
|
||||
+ recipe.routeConstraints.maximumGradeQ16) * 0.95;
|
||||
+ std::vector<std::int64_t> baseHeights;
|
||||
+ std::vector<double> routeDistance(route.size(), 0.0);
|
||||
+ baseHeights.reserve(route.size());
|
||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
||||
+ {
|
||||
+ baseHeights.push_back(route[i].positionSubvoxels[2]);
|
||||
+ if (i > 0)
|
||||
+ {
|
||||
+ const double dx = static_cast<double>(route[i].positionSubvoxels[0]
|
||||
+ - route[i - 1].positionSubvoxels[0]);
|
||||
+ const double dy = static_cast<double>(route[i].positionSubvoxels[1]
|
||||
+ - route[i - 1].positionSubvoxels[1]);
|
||||
+ routeDistance[i] = routeDistance[i - 1] + std::hypot(dx, dy);
|
||||
+ }
|
||||
+ }
|
||||
+ for (std::size_t i = 0; i < route.size(); ++i)
|
||||
+ {
|
||||
+ double profile = static_cast<double>(baseHeights[i]);
|
||||
+ std::size_t profileSource = i;
|
||||
+ for (std::size_t j = 0; j < route.size(); ++j)
|
||||
+ {
|
||||
+ const double pathDistance = std::abs(routeDistance[i] - routeDistance[j]);
|
||||
+ const double candidate = static_cast<double>(baseHeights[j])
|
||||
+ - maximumGrade * pathDistance;
|
||||
+ if (candidate > profile)
|
||||
+ {
|
||||
+ profile = candidate;
|
||||
+ profileSource = j;
|
||||
+ }
|
||||
+ }
|
||||
+ const std::int64_t plannedHeight = floating_island_detail::RoundQ(profile);
|
||||
+ const bool fixedEndpoint = i == 0 || i + 1 == route.size();
|
||||
+ if (fixedEndpoint && plannedHeight != baseHeights[i])
|
||||
+ {
|
||||
+ error = "resolved fixed island mouth endpoint " + std::to_string(i)
|
||||
+ + " needs " + std::to_string(plannedHeight - baseHeights[i])
|
||||
+ + " extra subvoxels from route sample " + std::to_string(profileSource)
|
||||
+ + " at z=" + std::to_string(baseHeights[profileSource])
|
||||
+ + " and path distance=" + std::to_string(static_cast<std::int64_t>(
|
||||
+ std::abs(routeDistance[i] - routeDistance[profileSource])))
|
||||
+ + " (arrival_z="
|
||||
+ + std::to_string(endpoints.arrivalSubvoxels[2]) + ", departure_z="
|
||||
+ + std::to_string(endpoints.departureSubvoxels[2]) + ", path_length="
|
||||
+ + std::to_string(static_cast<std::int64_t>(routeDistance.back())) + ")";
|
||||
+ return std::nullopt;
|
||||
+ }
|
||||
+ route[i].positionSubvoxels[2] = plannedHeight;
|
||||
+ }
|
||||
+ if (!manifest.finalize(error)) return std::nullopt;
|
||||
+ const std::string encoded = manifest.ToJson();
|
||||
+ const auto roundTrip = FloatingIslandManifestV1::FromJson(encoded, error);
|
||||
+ if (!roundTrip || roundTrip->ToJson() != encoded
|
||||
+ || roundTrip->contentHash != manifest.contentHash)
|
||||
+ { if (error.empty()) error = "resolved-mouth floating-island plan did not canonical-round-trip"; return std::nullopt; }
|
||||
+ return manifest;
|
||||
+}
|
||||
+
|
||||
FloatingIslandFieldV1::FloatingIslandFieldV1(const FloatingIslandRecipeV1& recipe,
|
||||
const FloatingIslandPlanV1& plan) noexcept : recipe_(&recipe), plan_(&plan)
|
||||
{}
|
||||
diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
||||
index a0cb0a7..468e315 100644
|
||||
--- a/Standalone/Source/StreamBenchmarkHeadless.cpp
|
||||
+++ b/Standalone/Source/StreamBenchmarkHeadless.cpp
|
||||
@@ -1826,12 +1826,59 @@ int main(int argc,char** argv)
|
||||
else if(!StandaloneWorldAssemblyPrototype::AttachRound1WorldAssemblyPrototype(
|
||||
*ownerWorld,ownerJsonRoot,error,{},&worldAssembly))
|
||||
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",error.c_str());return 2;}
|
||||
- if(((!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
||||
- ||(options.NativeOwnerFloatingIslandsRecipe
|
||||
- &&worldAssembly.floatingIslands.empty()))
|
||||
- &&!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||
- *ownerWorld,ownerJsonRoot,worldAssembly,error))
|
||||
- {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||
+ const bool attachFloatingIslandPlan=(!useExportedCorePlan&&options.FloatingIslandsRecipe)
|
||||
+ ||(options.NativeOwnerFloatingIslandsRecipe&&worldAssembly.floatingIslands.empty());
|
||||
+ if(attachFloatingIslandPlan)
|
||||
+ {
|
||||
+ const FStrateSlot* islandSlot=nullptr;
|
||||
+ if(ownerWorld->Manager)
|
||||
+ for(const FStrateSlot& slot:ownerWorld->Manager->GetLayout())
|
||||
+ if(slot.Definition&&slot.Definition->GeneratorType==ECaveGeneratorType::FloatingIslands)
|
||||
+ {islandSlot=&slot;break;}
|
||||
+ if(!islandSlot)
|
||||
+ {std::fprintf(stderr,"Floating-island endpoint resolution found no FloatingIslands strate.\n");return 2;}
|
||||
+ VoxelForgeCore::ResolvedLayoutSnapshotV3 mouthLayout;
|
||||
+ if(!StandaloneWorldAssemblyPrototype::BuildResolvedLayoutSnapshot(
|
||||
+ *ownerWorld,worldAssembly,"floating-islands",mouthLayout,error))
|
||||
+ {std::fprintf(stderr,"Pre-plan passage layout failed: %s\n",error.c_str());return 2;}
|
||||
+ VoxelForgeCore::FloatingIslandRouteEndpointsV1 endpoints;
|
||||
+ bool hasArrival=false,hasDeparture=false;
|
||||
+ for(const auto& passage:mouthLayout.passages)
|
||||
+ {
|
||||
+ const std::size_t lastSlash=passage.stableId.rfind('/');
|
||||
+ if(lastSlash==std::string::npos||passage.stableId.substr(lastSlash+1)!="0")continue;
|
||||
+ if(passage.lowerStrateIndex==islandSlot->StrateIndex&&!hasArrival)
|
||||
+ {
|
||||
+ endpoints.arrivalSubvoxels=passage.lowerMouthSubvoxels;
|
||||
+ hasArrival=true;
|
||||
+ }
|
||||
+ if(passage.upperStrateIndex==islandSlot->StrateIndex&&!hasDeparture)
|
||||
+ {
|
||||
+ endpoints.departureSubvoxels=passage.upperMouthSubvoxels;
|
||||
+ hasDeparture=true;
|
||||
+ }
|
||||
+ }
|
||||
+ if(!hasArrival||!hasDeparture)
|
||||
+ {std::fprintf(stderr,"Resolved passage layout does not provide both FI route mouths (arrival=%s departure=%s).\n",
|
||||
+ hasArrival?"yes":"no",hasDeparture?"yes":"no");return 2;}
|
||||
+ std::printf("[FloatingIslandResolvedMouths] strate=%d arrival_m=(%.3f,%.3f,%.3f) departure_m=(%.3f,%.3f,%.3f) status=PASS\n",
|
||||
+ islandSlot->StrateIndex,
|
||||
+ endpoints.arrivalSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
||||
+ endpoints.arrivalSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
||||
+ endpoints.arrivalSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
||||
+ endpoints.departureSubvoxels[0]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
||||
+ endpoints.departureSubvoxels[1]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel),
|
||||
+ endpoints.departureSubvoxels[2]*ownerWorld->CoreVoxelSizeMicrons
|
||||
+ /(1.0e6*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
||||
+ if(!StandaloneWorldAssemblyPrototype::AttachFloatingIslandWorldAssembly(
|
||||
+ *ownerWorld,ownerJsonRoot,worldAssembly,endpoints,error))
|
||||
+ {std::fprintf(stderr,"Floating-island assembly setup failed: %s\n",error.c_str());return 2;}
|
||||
+ }
|
||||
if(!options.WorldAssemblyManifestOutputPath.empty())
|
||||
{
|
||||
const std::string manifestText=worldAssembly.ToJson();
|
||||
diff --git a/Standalone/Source/WorldAssemblyPrototype.h b/Standalone/Source/WorldAssemblyPrototype.h
|
||||
index dbe2a7c..9790f2f 100644
|
||||
--- a/Standalone/Source/WorldAssemblyPrototype.h
|
||||
+++ b/Standalone/Source/WorldAssemblyPrototype.h
|
||||
@@ -923,7 +923,8 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
}
|
||||
|
||||
inline bool AttachFloatingIslandWorldAssembly(FStandaloneOwnerWorld& owner,
|
||||
- VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,std::string& error)
|
||||
+ VFJson::Value& root,VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
+ const VoxelForgeCore::FloatingIslandRouteEndpointsV1& endpoints,std::string& error)
|
||||
{
|
||||
if(!owner.Manager)
|
||||
{error="floating-island assembly needs the resolved standalone strate manager";return false;}
|
||||
@@ -996,7 +997,7 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
recipe.anchorRootBottomZSubvoxels=recipe.layoutMinimumZSubvoxels
|
||||
+recipe.boundarySealSubvoxels;
|
||||
|
||||
- auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,error);
|
||||
+ auto planned=VoxelForgeCore::PlanFloatingIslandCluster(recipe,endpoints,error);
|
||||
if(!planned)return false;
|
||||
const VoxelForgeCore::FloatingIslandFieldV1 routeField(
|
||||
planned->recipe,planned->plan);
|
||||
@@ -1217,6 +1218,7 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
:it->manifest.plan.route.size()-1;
|
||||
std::size_t resolvedRoutePointCount=it->manifest.plan.route.size();
|
||||
if(it->manifest.recipe.geometryVersion>=5
|
||||
+ &&it->manifest.recipe.geometryVersion<9
|
||||
&&!it->manifest.plan.route.empty()
|
||||
&&it->manifest.recipe.anchorIslandOrdinal<it->manifest.plan.islands.size())
|
||||
{
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build three standalone liminal SurfaceWorld fixtures from the owner input."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l141"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"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"))
|
||||
|
||||
flat = dict(base)
|
||||
flat["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
flat["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [
|
||||
{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = {
|
||||
"stable_id": "liminal/raised-road-v1",
|
||||
"strate_index": 0,
|
||||
"position_xy_m": [240.0, 180.0],
|
||||
"half_extents_m": [32.5, 3.0, 0.75],
|
||||
"deck_height_above_field_m": 22.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
}
|
||||
|
||||
for name, fixture in (("flatField-monolith", flat),
|
||||
("periodicHills", hills),
|
||||
("raised-road", road)):
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L141Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L141 prototype determinism checks and create eye-level/wide captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l141"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(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_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L141Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(f"{name} failed; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
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"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE 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]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= 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"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> 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 point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 45.0, center[1] - ty * 45.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 20.0 - nx * 20.0,
|
||||
center[1] - ty * 20.0 - ny * 20.0, center[2] + 34.0)
|
||||
wide_target = (center[0] - tx * 45.0, center[1] - ty * 45.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 5.0,
|
||||
}
|
||||
road = stamp(layout, "liminal/raised-road-v1")
|
||||
rx, ry, rz = (value / 1.0e6 for value in road["position_um"])
|
||||
half_length, half_width, half_thickness = (
|
||||
value / 1.0e6 for value in road["half_extents_um"])
|
||||
deck = rz + half_thickness
|
||||
eye_camera = (rx - half_length + 14.0, ry, deck + 1.6)
|
||||
eye_target = (rx + half_length, ry, base_height)
|
||||
wide_camera = (rx, ry - 28.0, deck + 24.0)
|
||||
wide_target = (rx + 4.0, ry, base_height + 4.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 24),
|
||||
"wide": (wide_camera, wide_target, 26),
|
||||
"height_center": base_height + 11.0,
|
||||
"height_range": 32.0,
|
||||
"road_deck": deck,
|
||||
"road_width": 2.0 * half_width,
|
||||
}
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0) -> list[object]:
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L141Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
for view_name in ("eye", "wide"):
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,7)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=0)
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
renderer_command = [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=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on","--fog-start-m=30","--fog-end-m=105",f"--fog-color={FOG_COLOR}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name == "eye" else "wide"
|
||||
print(f"[L141Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def measure_near_ready(fixture: Path, views: dict, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
uptime = subprocess.run(["uptime"],cwd=ROOT,capture_output=True,text=True,check=True).stdout.strip()
|
||||
load_match = re.search(r"load average:\s*([0-9.]+)",uptime)
|
||||
if not load_match or float(load_match.group(1)) >= 1.5:
|
||||
raise RuntimeError(f"near-ready timing requires a quiet machine: {uptime}")
|
||||
print(f"[L141TimingEnvironment] window=one W12 run after one warmup uptime='{uptime}' "
|
||||
f"status=PASS",flush=True)
|
||||
camera = views["eye"][0]
|
||||
warm_dir = out / "near-ready" / "warmup"
|
||||
timed_dir = out / "near-ready" / "measured"
|
||||
warm_dir.mkdir(parents=True,exist_ok=True)
|
||||
timed_dir.mkdir(parents=True,exist_ok=True)
|
||||
|
||||
def command(directory: Path) -> list[object]:
|
||||
return [headless,fixture,"--workers=12","--world-assembler-prototype",
|
||||
"--seed=0","--stream-benchmark","--stream-pipeline","--force-generate",
|
||||
"--clip-radius=1","--max-clip-level=2","--max-level=2",
|
||||
"--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera,(camera[0],camera[1]+1.0,camera[2])))}",
|
||||
f"--world-assembler-manifest-output={directory/'manifest.json'}",
|
||||
f"--resolved-layout-output={directory/'layout.json'}"]
|
||||
|
||||
run_step("near-ready-warmup",command(warm_dir),
|
||||
out/"logs"/"near-ready-warmup.log",env=env,stamped=True)
|
||||
output = run_step("near-ready-measured",command(timed_dir),
|
||||
out/"logs"/"near-ready-measured.log",env=env,stamped=True)
|
||||
match = re.search(r"^\[WorldAssemblerNearLODPipelineReady\] lods=0-2 "
|
||||
r"tiles=(\d+)/(\d+) cold_ready_s=([0-9.]+)$",output,re.M)
|
||||
if not match or match.group(1)!=match.group(2):
|
||||
raise RuntimeError("near-ready run did not complete the full LOD 0-2 set")
|
||||
print(f"[L141NearReady] window=one W12 run after one warmup clip_radius=1 "
|
||||
f"tiles={match.group(1)}/{match.group(2)} cold_ready_s={match.group(3)} status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux141/BuildL141Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"raised-road":fixture_dir/"liminal-raised-road.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_road_walk_audit(fixtures["raised-road"],probed["raised-road"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
for name,fixture in fixtures.items():
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
measure_near_ready(fixtures["raised-road"],probed["raised-road"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L141Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,40 @@
|
||||
# L141 — first liminal layer
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
The standalone fixtures and six captures are ready for review. The flat field has one oversized capped-cylinder monolith. The road is a 65 m `fillDais` platform, 6 m wide, with its deck 22 m above a flat field. Fog supplies the road's cloud-sea backdrop in these standalone images.
|
||||
|
||||
The `periodicHills` fixture uses the validated 80 m periods and 2.2 m amplitudes on both axes. Its field repeats exactly, but these captures show one broad rise rather than hills repeating to the horizon. The validated period is longer than the 56 m eye-level capture radius; the wider full-detail capture exceeded Core's 160-million-node interval bound. Smaller periods fail the existing profile validator. A capture extent that trims vertical tiles independently from horizontal tiles is the next useful step; this round leaves the Core profile limits unchanged.
|
||||
|
||||
## Changes
|
||||
|
||||
- Reused Core's `flatField`, `periodicHills`, `fillMonolith`, and `fillDais` support. Added standalone fixture inputs and runners in `BuildL141Fixtures.py` and `CaptureL141.py`.
|
||||
- Added `liminal_road_override` as a standalone adapter to the existing free-placement `fillDais` stamp, plus `--audit-liminal-road-walk`. The walk audit samples the Core field with the existing capsule sampler and checks width and grade.
|
||||
- Added optional distance fog to `LinuxOffscreenCapture` for the screenshots. The fog is capture-only; Unreal cloud rendering remains Unreal-side.
|
||||
- No evaluator change or Unreal `Source/` change. Owner output hashes match their pinned L130/v227 canonical rows in the fast tier.
|
||||
|
||||
## Measurements
|
||||
|
||||
- Determinism: each fixture matched across Clang and GCC at W1, W8, and W12 (six cells per fixture). The matrix used LOD0, clip radius 1; each cell compares recipe/plan hashes, resolved-layout identity, and per-tile hashes. Individual logs are in `Saved/Linux/l141/logs/determinism-*.log`.
|
||||
- Raised-road capsule walk: 127 stations, 8,001 capsule samples, zero blocked samples, zero unsupported stations, 6.000 m width against a 0.680 m capsule diameter, and 0.000° grade. See `Saved/Linux/l141/logs/road-walk-audit.log`.
|
||||
- Near-ready: one W12 LOD0–2 sample after one warm-up, clip radius 1, 139/139 tiles. The measured `cold_ready_s` was **0.173886 s**. The window was one run after one warm-up; `uptime` showed a 1-minute load average of 1.31 and no other build was running. See `Saved/Linux/l141/logs/near-ready-measured.log`.
|
||||
- Each capture is 1600×900 and passed `capture_quality.py`. Eye views place the camera 1.6 m above the field or road deck. Camera coordinates and image hashes are recorded in the capture runner output; export and render logs are under `Saved/Linux/l141/logs/`.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 and GCC standalone builds passed with FP contraction off, four jobs, and source stamp `0a2828c398aa62870690667901d44f2daf5b1a5547b9640da6dbb01df721af8b`.
|
||||
- `python3 Standalone/Tools/verify/verify.py fast --out Saved/Linux/l141 --overall-timeout 1740` passed in 285.7 s. Owner SurfaceWorld and FloatingIslands W1/W12 canonical hashes matched the L130/v227 baselines.
|
||||
- Fixture determinism, dedicated road walk audit, six standalone captures, and all six image-quality checks passed.
|
||||
- Python bytecode compilation and `git diff --check` passed.
|
||||
- An exploratory broad `--audit-resolved-layout-passages` invocation also ran unrelated passage ripple checks and reported 2/3 passage ripple records passing, with `passage/1-2/0` failing. The focused liminal road audit passes; no legacy threshold was changed.
|
||||
- No Unreal build was run because no Unreal `Source/` file changed. No timing matrix was requested or run.
|
||||
|
||||
## Remaining risk
|
||||
|
||||
The hill screenshots do not yet show multiple full repeats to the horizon. The profile itself is exactly periodic; the standalone capture volume is too short at the current full-detail safety bound. The raised road is geometric and walk-audited, while its clouds are represented by fog in the capture renderer.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L142 liminal pictures and walk-audit controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"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"))
|
||||
|
||||
monolith = dict(base)
|
||||
monolith["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
monolith["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
}],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road_override = {
|
||||
"stable_id": "liminal/road-clouds-v2",
|
||||
"strate_index": 0,
|
||||
"start_xy_m": [120.0, 340.0],
|
||||
"path_length_m": 360.0,
|
||||
"curve_control_offset_m": 84.0,
|
||||
"deck_height_above_field_m": 44.0,
|
||||
"rise_control_m": 22.0,
|
||||
"half_width_m": 4.0,
|
||||
"half_thickness_m": 1.0,
|
||||
"sample_spacing_m": 2.0,
|
||||
"support_spacing_m": 24.0,
|
||||
"support_radius_m": 0.55,
|
||||
}
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = dict(road_override)
|
||||
|
||||
short_control = dict(base)
|
||||
short_control["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
short_override = dict(road_override)
|
||||
short_override["path_length_m"] = 180.0
|
||||
short_control["liminal_road_override"] = short_override
|
||||
|
||||
pillars = dict(base)
|
||||
pillars["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
pillars["liminal_pillar_grid_override"] = {
|
||||
"stable_id": "liminal/horizon-pillar-grid-v1",
|
||||
"strate_index": 0,
|
||||
"center_xy_m": [700.0, 800.0],
|
||||
"rows": 15,
|
||||
"columns": 15,
|
||||
"spacing_m": 76.0,
|
||||
"radius_m": 5.5,
|
||||
"height_m": 40.0,
|
||||
}
|
||||
|
||||
fixtures = (
|
||||
("flatField-monolith", monolith),
|
||||
("periodicHills", hills),
|
||||
("road-in-clouds", road),
|
||||
("road-short-control", short_control),
|
||||
("pillar-grid", pillars),
|
||||
)
|
||||
for name, fixture in fixtures:
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L142Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,433 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L142 liminal determinism checks, walk audits and multi-LOD captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l142"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740,
|
||||
expected_exit: int = 0) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(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_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L142Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
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"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE 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]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= 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"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> 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 point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 36.0 - nx * 28.0,
|
||||
center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0)
|
||||
wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 6.0,
|
||||
}
|
||||
if name == "road-in-clouds":
|
||||
deck = sorted((row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/road-clouds-v2/deck/")),
|
||||
key=lambda row: row["stable_id"])
|
||||
if len(deck) < 2:
|
||||
raise RuntimeError("road layout has no resolved swept platform chain")
|
||||
points = [tuple(value / 1.0e6 for value in row["position_um"])
|
||||
for row in deck]
|
||||
tops = [point[2] + row["half_extents_um"][2] / 1.0e6
|
||||
for point, row in zip(points, deck)]
|
||||
index = max(1, len(points) // 12)
|
||||
camera_dx = points[index + 1][0] - points[index - 1][0]
|
||||
camera_dy = points[index + 1][1] - points[index - 1][1]
|
||||
camera_tangent_length = math.hypot(camera_dx, camera_dy)
|
||||
camera_tx, camera_ty = (camera_dx / camera_tangent_length,
|
||||
camera_dy / camera_tangent_length)
|
||||
camera_nx, camera_ny = -camera_ty, camera_tx
|
||||
eye_camera = (points[index][0] + camera_nx * 3.2,
|
||||
points[index][1] + camera_ny * 3.2, tops[index] + 1.6)
|
||||
eye_target = (points[index][0] + camera_tx * 90.0 + camera_nx * 60.0,
|
||||
points[index][1] + camera_ty * 90.0 + camera_ny * 60.0,
|
||||
tops[index] - 44.0)
|
||||
middle = len(points) // 2
|
||||
dx = points[middle + 1][0] - points[middle - 1][0]
|
||||
dy = points[middle + 1][1] - points[middle - 1][1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
nx, ny = -ty, tx
|
||||
wide_target_index = min(len(points) - 1, middle + 30)
|
||||
wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0,
|
||||
points[middle][1] - ty * 35.0 + ny * 30.0,
|
||||
tops[middle] + 25.0)
|
||||
wide_target = (points[wide_target_index][0], points[wide_target_index][1],
|
||||
tops[wide_target_index] - 5.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": base_height + 23.0,
|
||||
"height_range": 60.0,
|
||||
"road_deck": tops[middle],
|
||||
"road_width": 2.0 * deck[middle]["half_extents_um"][1] / 1.0e6,
|
||||
}
|
||||
if name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")]
|
||||
if len(pillars) < 9:
|
||||
raise RuntimeError("pillar-grid layout has too few resolved supports")
|
||||
xs = [row["position_um"][0] / 1.0e6 for row in pillars]
|
||||
ys = [row["position_um"][1] / 1.0e6 for row in pillars]
|
||||
floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6
|
||||
for row in pillars)
|
||||
cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5
|
||||
eye_camera = (min(xs) - 78.0, cy, floor + 1.6)
|
||||
eye_target = (max(xs) + 90.0, cy, floor + 1.6)
|
||||
wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0)
|
||||
wide_target = (cx, cy, floor + 6.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": floor + 12.0,
|
||||
"height_range": 42.0,
|
||||
}
|
||||
raise RuntimeError(f"no capture framing for fixture {name}")
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0,
|
||||
far_lods: bool = False,
|
||||
max_clip_override: int | None = None) -> list[object]:
|
||||
max_clip_level = (max_clip_override if max_clip_override is not None else
|
||||
8 if far_lods else max_level)
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L142Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
for view_name in ("eye", "wide"):
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,3)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=-1,far_lods=True,
|
||||
max_clip_override=(2 if name == "road-in-clouds" and view_name == "wide"
|
||||
else 3 if name == "road-in-clouds" else None))
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
if name == "road-in-clouds":
|
||||
fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95")
|
||||
elif name == "pillar-grid":
|
||||
fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91"
|
||||
elif name == "periodicHills":
|
||||
fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88"
|
||||
else:
|
||||
fog_start,fog_end,fog_color=30,105,FOG_COLOR
|
||||
renderer_command = [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=on","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name == "eye" else "wide"
|
||||
print(f"[L142Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def run_road_short_negative_control(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-short-negative-control"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-short-negative-control",
|
||||
headless_command(headless,fixture,camera,run_dir/"control.vfmesh",
|
||||
run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True),
|
||||
out/"logs"/"road-short-negative-control.log",env=env,stamped=True,
|
||||
expected_exit=14)
|
||||
match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output)
|
||||
if not match or float(match.group(1)) >= 300.0:
|
||||
raise RuntimeError("short-road negative control did not falsify the length gate")
|
||||
print(f"[L142RoadNegativeControl] path_length_m={match.group(1)} "
|
||||
"expected=FAIL status=PASS",flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux142/BuildL142Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"road-in-clouds":fixture_dir/"liminal-road-in-clouds.json",
|
||||
"pillar-grid":fixture_dir/"liminal-pillar-grid.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json",
|
||||
probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env)
|
||||
for name,fixture in fixtures.items():
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L142Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,46 @@
|
||||
# L142 — liminal pass 2
|
||||
|
||||
| Hills: eye height | Hills: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Road: eye height | Road: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Pillar grid: eye height | Pillar grid: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
| Monolith: eye height | Monolith: wide |
|
||||
|---|---|
|
||||
|  |  |
|
||||
|
||||
## Result
|
||||
|
||||
The wide hill capture shows the validated 80 m repeating profile continuing across the horizon. The capture now streams LOD 0–8; the profile period and validator are unchanged.
|
||||
|
||||
The road is a cubic swept path made from overlapping solid deck segments, not a chain of platform stamps. The earlier platform chain produced top steps; the swept deck passes the surface-sampled walk audit. Its wide view is capped at LOD 2 to keep the deck legible, so that frame shows a local section; the full route length is measured by the audit.
|
||||
|
||||
The R47 addition is a 15×15 grid of identical pillars, spaced 76 m apart across a 1,064 m span. The monolith eye and wide views are retained as the first liminal slice.
|
||||
|
||||
## Measurements
|
||||
|
||||
- **Road:** 376.827 m horizontal path length, 8.000 m width, and 12.061° maximum sampled surface grade against the 15° limit. The deck rises and falls along the cubic profile. It has 17 supports at about 24 m spacing; the flat cloud floor is 44 m below the deck centerline (about 45 m below the walking surface), with pale fog in both views.
|
||||
- **Walk audit:** 901 surface stations at no more than 0.5 m spacing; 56,763 capsule samples; 0 blocked samples, 0 unsupported samples across 5,871 support probes, 0 grade failures, and 0 missed surface probes. The 180 m short-road control measured 210.341 m and failed the 300 m gate as expected.
|
||||
- **Capture quality:** all eight fixtures/views passed the image-quality gate at 1600×900. Hills and pillars stream through LOD 8; road eye and wide views use caps 3 and 2 respectively.
|
||||
- **Determinism:** the monolith, hills, road, and pillar fixtures each matched across Clang and GCC at W1, W8, and W12 (six compiler/worker cells per fixture), on source stamp `72f239e9534faa27e3796fe29207403b190558403841aeebc87d8e75e98e34c7`.
|
||||
|
||||
## Checks
|
||||
|
||||
- Clang 19 and GCC builds passed with floating-point contraction off, four build jobs, and the L142 source stamp.
|
||||
- `Standalone/Tools/verify/verify.py fast --out Saved/Linux/l142 --overall-timeout 1740` passed. Both current owner fixtures produced all 12 canonical hash rows matching the pinned L130-v227 baseline; W1 and W12 also matched each other.
|
||||
- The fast summary retains non-gating differences against older L46/L53 diagnostic baselines; the current pinned L130 canonical checks pass.
|
||||
- The L142 capture runner passed the walk audit, the short-road negative control, all four determinism comparisons, and all eight image-quality checks. Final logs are in `Saved/Linux/l142/logs/`.
|
||||
- No Unreal build or performance timing matrix was run. No Unreal `Source/` files changed. The inherited single-run near-ready timing step was removed from this picture-and-geometry runner rather than treated as a timing result.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
The road wide capture frames a local section at LOD 2 to retain a continuous deck; the 376.827 m length is established by the full-route walk audit. Captures use the standalone Linux renderer rather than an Unreal viewport.
|
||||
|
||||
Changed Unreal `Source/` files: none.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 39 KiB |
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the L143 liminal pictures and walk-audit controls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OWNER = Path(os.environ.get("VOXELFORGE_OWNER_DATA",
|
||||
ROOT / "Standalone/data/owner_world.json"))
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
|
||||
FLAT_FIELD = {
|
||||
"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"))
|
||||
|
||||
monolith = dict(base)
|
||||
monolith["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
monolith["motif_family_override"] = {
|
||||
"version": 1,
|
||||
"strate_index": 0,
|
||||
"placement_policy": "seed,strate,stable_id",
|
||||
"motifs": [{
|
||||
"stable_id": "liminal/oversized-familiar-monolith-v1",
|
||||
"kind": "cappedCylinder",
|
||||
"radius_m": 1.6,
|
||||
"height_m": 26.0,
|
||||
"route_side_offset_m": 42.0,
|
||||
"placement_radius_m": 0.0,
|
||||
"fillet_radius_m": 0.6,
|
||||
"blend_width_m": 1.2,
|
||||
"minimum_spacing_m": 0.0,
|
||||
"yaw_quarter_turns": 0,
|
||||
"attachment": "seated_floor",
|
||||
}],
|
||||
}
|
||||
|
||||
hills = dict(base)
|
||||
hills["surface_profile_override"] = {
|
||||
"version": 1,
|
||||
"kind": "periodicHills",
|
||||
"amplitude_x_m": 2.2,
|
||||
"amplitude_y_m": 2.2,
|
||||
"wavelength_x_m": 80.0,
|
||||
"wavelength_y_m": 80.0,
|
||||
"minimum_arena_clearance_m": 0.0,
|
||||
}
|
||||
|
||||
road_override = {
|
||||
"stable_id": "liminal/road-clouds-v2",
|
||||
"strate_index": 0,
|
||||
"start_xy_m": [120.0, 340.0],
|
||||
"path_length_m": 360.0,
|
||||
"curve_control_offset_m": 84.0,
|
||||
"deck_height_above_field_m": 44.0,
|
||||
"rise_control_m": 22.0,
|
||||
"half_width_m": 4.0,
|
||||
"half_thickness_m": 1.0,
|
||||
"sample_spacing_m": 1.0,
|
||||
"support_spacing_m": 24.0,
|
||||
"support_radius_m": 0.55,
|
||||
}
|
||||
road = dict(base)
|
||||
road["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
road["liminal_road_override"] = dict(road_override)
|
||||
|
||||
short_control = dict(base)
|
||||
short_control["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
short_override = dict(road_override)
|
||||
short_override["path_length_m"] = 180.0
|
||||
short_control["liminal_road_override"] = short_override
|
||||
|
||||
pillars = dict(base)
|
||||
pillars["surface_profile_override"] = dict(FLAT_FIELD)
|
||||
pillars["liminal_pillar_grid_override"] = {
|
||||
"stable_id": "liminal/horizon-pillar-grid-v1",
|
||||
"strate_index": 0,
|
||||
"center_xy_m": [700.0, 800.0],
|
||||
"rows": 15,
|
||||
"columns": 15,
|
||||
"spacing_m": 76.0,
|
||||
"radius_m": 5.5,
|
||||
"height_m": 40.0,
|
||||
}
|
||||
|
||||
fixtures = (
|
||||
("flatField-monolith", monolith),
|
||||
("periodicHills", hills),
|
||||
("road-in-clouds", road),
|
||||
("road-short-control", short_control),
|
||||
("pillar-grid", pillars),
|
||||
)
|
||||
for name, fixture in fixtures:
|
||||
path = args.out / f"liminal-{name}.json"
|
||||
path.write_text(json.dumps(fixture, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[L143Fixture] name={name} file={path} status=PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run L143 liminal determinism checks, walk audits and multi-LOD captures."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUT_DEFAULT = Path(os.environ.get("VOXELFORGE_VERIFY_OUT", ROOT / "Saved/Linux/l143"))
|
||||
CAPTURE_DEFAULT = Path(__file__).resolve().parent / "captures"
|
||||
LIMIT = ["taskset", "-c", "0-11", "prlimit", "--as=6442450944", "--"]
|
||||
Q_PER_METRE = 1024.0
|
||||
FOG_COLOR = "0.66,0.73,0.77"
|
||||
|
||||
sys.path.insert(0, str(ROOT / "Standalone/Tools/verify"))
|
||||
from runner import bounded_run, current_stamp, first_stamp
|
||||
|
||||
|
||||
def fmt(values) -> str:
|
||||
return ",".join(f"{value:.6f}" for value in values)
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(block)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_step(name: str, command: list[object], log: Path, *, env: dict[str, str],
|
||||
stamped: bool = False, timeout_seconds: int = 1740,
|
||||
expected_exit: int = 0) -> str:
|
||||
expected_stamp = current_stamp() if stamped else None
|
||||
if stamped:
|
||||
binary = Path(str(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_stamp:
|
||||
raise RuntimeError(f"{name} refused a stale executable: {binary}")
|
||||
started = time.monotonic()
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
with log.open("w", encoding="utf-8") as stream:
|
||||
result = subprocess.run([*LIMIT, f"timeout", f"{timeout_seconds}s",
|
||||
*map(str, command)], cwd=ROOT, env=env,
|
||||
stdout=stream, stderr=subprocess.STDOUT,
|
||||
timeout=timeout_seconds + 5, check=False)
|
||||
elapsed = time.monotonic() - started
|
||||
output = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"[L143Run] step={name} elapsed_s={elapsed:.3f} exit={result.returncode} log={log}",
|
||||
flush=True)
|
||||
if stamped and (first_stamp(output) != expected_stamp or current_stamp() != expected_stamp):
|
||||
raise RuntimeError(f"{name} source stamp mismatch; see {log}")
|
||||
if result.returncode != expected_exit:
|
||||
raise RuntimeError(f"{name} exit={result.returncode}, expected={expected_exit}; see {log}")
|
||||
return output
|
||||
|
||||
|
||||
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"))
|
||||
strata = [entry for entry in manifest["plan"]["strates"]
|
||||
if entry.get("route") and len(entry["route"]) >= 2]
|
||||
if not strata:
|
||||
raise RuntimeError(f"no planned SurfaceWorld route in {manifest_path}")
|
||||
route = strata[0]["route"]
|
||||
points = [tuple(float(value) / Q_PER_METRE 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]:
|
||||
remaining = max(0.0, min(total, distance))
|
||||
for index, (a, b, length) in enumerate(zip(points, points[1:], lengths)):
|
||||
if remaining <= length or index == len(lengths) - 1:
|
||||
if length <= 1.0e-9:
|
||||
return b
|
||||
t = remaining / length
|
||||
return tuple(a[axis] + (b[axis] - a[axis]) * t for axis in range(3))
|
||||
remaining -= 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"degenerate route tangent in {manifest_path}")
|
||||
return center, (dx / length, dy / length, 0.0)
|
||||
|
||||
|
||||
def camera_angles(camera, target) -> 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 point_toward(camera, target) -> tuple[float, ...]:
|
||||
yaw, pitch = camera_angles(camera, target)
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def stamp(layout: dict, stable_id: str) -> dict:
|
||||
for record in layout.get("stamps", []):
|
||||
if record.get("stable_id") == stable_id:
|
||||
return record
|
||||
raise RuntimeError(f"missing resolved stamp {stable_id}")
|
||||
|
||||
|
||||
def make_views(name: str, manifest_path: Path, layout_path: Path):
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
center, tangent = route_frame(manifest_path)
|
||||
tx, ty, _ = tangent
|
||||
nx, ny = -ty, tx
|
||||
base_height = manifest["recipe"]["surface_profile"]["h0_q"] / Q_PER_METRE
|
||||
if name == "flatField-monolith":
|
||||
monolith = stamp(layout, "liminal/oversized-familiar-monolith-v1")
|
||||
sx, sy, _ = (value / 1.0e6 for value in monolith["position_um"])
|
||||
floor = monolith["floor_um"] / 1.0e6
|
||||
eye_camera = (sx - tx * 36.0, sy - ty * 36.0, floor + 1.6)
|
||||
eye_target = (sx, sy, floor + 10.5)
|
||||
wide_camera = (sx - tx * 42.0 - nx * 18.0,
|
||||
sy - ty * 42.0 - ny * 18.0, floor + 42.0)
|
||||
wide_target = (sx, sy, floor + 8.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 24),
|
||||
"height_center": floor,
|
||||
"height_range": 38.0,
|
||||
}
|
||||
if name == "periodicHills":
|
||||
# Route points are capsule centers: +0.72 m places the view at 1.60 m above ground.
|
||||
eye_camera = (center[0], center[1], center[2] + 0.72)
|
||||
eye_target = (center[0] - tx * 360.0, center[1] - ty * 360.0,
|
||||
center[2] + 0.72)
|
||||
wide_camera = (center[0] - tx * 36.0 - nx * 28.0,
|
||||
center[1] - ty * 36.0 - ny * 28.0, center[2] + 30.0)
|
||||
wide_target = (center[0] - tx * 310.0, center[1] - ty * 310.0,
|
||||
center[2] + 0.72)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 20),
|
||||
"wide": (wide_camera, wide_target, 28),
|
||||
"height_center": base_height,
|
||||
"height_range": 6.0,
|
||||
}
|
||||
if name == "road-in-clouds":
|
||||
deck = stamp(layout,"liminal/road-clouds-v2/deck/000")
|
||||
path_um = deck.get("sweep_path_um", [])
|
||||
if len(path_um) < 2:
|
||||
raise RuntimeError("road layout has no resolved continuous swept path")
|
||||
points = [tuple(value / 1.0e6 for value in point) for point in path_um]
|
||||
half_thickness = deck["half_extents_um"][2] / 1.0e6
|
||||
top = lambda point: point[2] + half_thickness
|
||||
index = max(1, len(points) // 12)
|
||||
camera_dx = points[index + 1][0] - points[index - 1][0]
|
||||
camera_dy = points[index + 1][1] - points[index - 1][1]
|
||||
camera_tangent_length = math.hypot(camera_dx, camera_dy)
|
||||
camera_tx, camera_ty = (camera_dx / camera_tangent_length,
|
||||
camera_dy / camera_tangent_length)
|
||||
camera_nx, camera_ny = -camera_ty, camera_tx
|
||||
half_width = deck["half_extents_um"][1] / 1.0e6
|
||||
eye_target_index = min(len(points) - 1, index + 25)
|
||||
eye_camera = (points[index][0] + camera_nx * (half_width + 2.0),
|
||||
points[index][1] + camera_ny * (half_width + 2.0),
|
||||
top(points[index]) + 1.6)
|
||||
eye_target = (points[eye_target_index][0] + camera_nx * (half_width - 0.4),
|
||||
points[eye_target_index][1] + camera_ny * (half_width - 0.4),
|
||||
top(points[eye_target_index]) + 0.3)
|
||||
middle = len(points) // 2
|
||||
dx = points[middle + 1][0] - points[middle - 1][0]
|
||||
dy = points[middle + 1][1] - points[middle - 1][1]
|
||||
tangent_length = math.hypot(dx, dy)
|
||||
tx, ty = dx / tangent_length, dy / tangent_length
|
||||
nx, ny = -ty, tx
|
||||
wide_target_index = min(len(points) - 1, middle + 30)
|
||||
wide_camera = (points[middle][0] - tx * 35.0 + nx * 30.0,
|
||||
points[middle][1] - ty * 35.0 + ny * 30.0,
|
||||
top(points[middle]) + 25.0)
|
||||
wide_target = (points[wide_target_index][0], points[wide_target_index][1],
|
||||
top(points[wide_target_index]) - 5.0)
|
||||
cloud_camera = (points[middle][0] - tx * 18.0 + nx * 12.0,
|
||||
points[middle][1] - ty * 18.0 + ny * 12.0,
|
||||
base_height + 1.6)
|
||||
cloud_target = (points[middle][0] + tx * 8.0,
|
||||
points[middle][1] + ty * 8.0,
|
||||
points[middle][2] - half_thickness * 0.35)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"cloud-up": (cloud_camera, cloud_target, 3),
|
||||
"height_center": base_height + 23.0,
|
||||
"height_range": 60.0,
|
||||
"road_deck": top(points[middle]),
|
||||
"road_width": 2.0 * half_width,
|
||||
}
|
||||
if name == "pillar-grid":
|
||||
pillars = [row for row in layout.get("stamps", [])
|
||||
if row.get("stable_id", "").startswith("liminal/horizon-pillar-grid-v1/pillar/")]
|
||||
if len(pillars) < 9:
|
||||
raise RuntimeError("pillar-grid layout has too few resolved supports")
|
||||
xs = [row["position_um"][0] / 1.0e6 for row in pillars]
|
||||
ys = [row["position_um"][1] / 1.0e6 for row in pillars]
|
||||
floor = min((row["position_um"][2] - row["half_extents_um"][2]) / 1.0e6
|
||||
for row in pillars)
|
||||
cx, cy = (min(xs) + max(xs)) * 0.5, (min(ys) + max(ys)) * 0.5
|
||||
eye_camera = (min(xs) - 78.0, cy, floor + 1.6)
|
||||
eye_target = (max(xs) + 90.0, cy, floor + 1.6)
|
||||
wide_camera = (cx - 280.0, cy - 420.0, floor + 230.0)
|
||||
wide_target = (cx, cy, floor + 6.0)
|
||||
return {
|
||||
"eye": (eye_camera, eye_target, 3),
|
||||
"wide": (wide_camera, wide_target, 3),
|
||||
"height_center": floor + 12.0,
|
||||
"height_range": 42.0,
|
||||
}
|
||||
raise RuntimeError(f"no capture framing for fixture {name}")
|
||||
|
||||
|
||||
def headless_command(binary: Path, fixture: Path, camera, mesh: Path,
|
||||
manifest: Path, layout: Path, workers: int,
|
||||
clip_radius: int, *, hash_tiles: bool = False,
|
||||
audit_road: bool = False, max_level: int = 0,
|
||||
far_lods: bool = False,
|
||||
max_clip_override: int | None = None) -> list[object]:
|
||||
max_clip_level = (max_clip_override if max_clip_override is not None else
|
||||
8 if far_lods else max_level)
|
||||
command: list[object] = [binary, fixture, f"--workers={workers}",
|
||||
"--seed=0", "--world-assembler-prototype", "--stream-benchmark", "--stream-pipeline",
|
||||
"--force-generate", f"--clip-radius={clip_radius}",
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0",
|
||||
f"--camera={fmt(point_toward(camera, (camera[0], camera[1] + 1.0, camera[2])))}",
|
||||
f"--mesh-export={mesh}", f"--world-assembler-manifest-output={manifest}",
|
||||
f"--resolved-layout-output={layout}"]
|
||||
if hash_tiles:
|
||||
command.append("--stream-hash-tiles")
|
||||
if audit_road:
|
||||
command.append("--audit-liminal-road-walk")
|
||||
return command
|
||||
|
||||
|
||||
def determinism_digest(output: str, manifest_path: Path,
|
||||
layout_path: Path) -> tuple[str, str, str]:
|
||||
tile_hashes = sorted(line for line in output.splitlines()
|
||||
if line.startswith("[PipelineTileHash] "))
|
||||
if not manifest_path.is_file() or not layout_path.is_file() or not tile_hashes:
|
||||
raise RuntimeError("determinism run omitted manifest, layout or per-tile hashes")
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
manifest_digest = (manifest["content_hash"],manifest["recipe"]["content_hash"],
|
||||
manifest["plan"]["content_hash"])
|
||||
return ("|".join(manifest_digest),layout["stable_identity"],"\n".join(tile_hashes))
|
||||
|
||||
|
||||
def run_determinism(name: str, fixture: Path, view, *, binaries: dict[str, Path],
|
||||
out: Path, env: dict[str, str], max_level: int = 0,
|
||||
far_lods: bool = False, max_clip_override: int | None = None) -> None:
|
||||
camera, _, _ = view["eye"]
|
||||
clip_radius = 1
|
||||
max_clip = 0
|
||||
baseline = None
|
||||
cells = 0
|
||||
for compiler, workers in ((compiler, workers)
|
||||
for compiler in ("clang", "gcc")
|
||||
for workers in (1, 8, 12)):
|
||||
tag = f"{name}-{compiler}-w{workers}"
|
||||
run_dir = out / "determinism" / tag
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
mesh = run_dir / "feature.vfmesh"
|
||||
manifest = run_dir / "manifest.json"
|
||||
layout = run_dir / "layout.json"
|
||||
command = headless_command(binaries[compiler], fixture,camera,mesh,manifest,
|
||||
layout,workers,max_clip,hash_tiles=True,max_level=max_level,
|
||||
far_lods=far_lods,max_clip_override=max_clip_override)
|
||||
output = run_step(tag,command,out/"logs"/f"determinism-{tag}.log",
|
||||
env=env,stamped=True)
|
||||
digest = determinism_digest(output,manifest,layout)
|
||||
if baseline is None:
|
||||
baseline = digest
|
||||
elif digest != baseline:
|
||||
raise RuntimeError(f"{name} diverged at {compiler} W{workers}; see determinism logs")
|
||||
cells += 1
|
||||
print(f"[L143Determinism] fixture={name} compiler_worker_cells={cells} identical=yes status=PASS",
|
||||
flush=True)
|
||||
|
||||
|
||||
def export_and_capture(name: str, fixture: Path, views: dict, *,
|
||||
headless: Path, renderer: Path, out: Path,
|
||||
artifact_dir: Path, env: dict[str, str]) -> None:
|
||||
view_names = ("eye", "wide", "cloud-up") if name == "road-in-clouds" else ("eye", "wide")
|
||||
for view_name in view_names:
|
||||
camera, target, clip_radius = views[view_name]
|
||||
clip_radius = min(clip_radius,3)
|
||||
stem = f"{name}-{view_name}"
|
||||
mesh = out / "capture-meshes" / f"{stem}.vfmesh"
|
||||
manifest = out / "capture-manifests" / f"{stem}.json"
|
||||
layout = out / "capture-layouts" / f"{stem}.json"
|
||||
png = out / "capture-png" / f"{stem}.png"
|
||||
image = artifact_dir / f"{stem}.jpg"
|
||||
for path in (mesh,manifest,layout,png,image):
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
command = headless_command(headless,fixture,camera,mesh,manifest,layout,12,
|
||||
clip_radius,max_level=-1,far_lods=True,
|
||||
max_clip_override=2 if name == "road-in-clouds" else None)
|
||||
output = run_step(f"capture-{stem}-export",command,
|
||||
out/"logs"/f"capture-{stem}-export.log",env=env,stamped=True)
|
||||
if not mesh.is_file() or mesh.stat().st_size == 0:
|
||||
raise RuntimeError(f"{stem} produced an empty mesh")
|
||||
yaw, pitch = camera_angles(camera,target)
|
||||
height_center = views["height_center"]
|
||||
height_range = views["height_range"]
|
||||
if name == "road-in-clouds":
|
||||
fog_start,fog_end,fog_color=(24,260,"0.88,0.93,0.95")
|
||||
elif name == "pillar-grid":
|
||||
fog_start,fog_end,fog_color=220,1250,"0.82,0.88,0.91"
|
||||
elif name == "periodicHills":
|
||||
fog_start,fog_end,fog_color=210,1000,"0.78,0.84,0.88"
|
||||
else:
|
||||
fog_start,fog_end,fog_color=30,105,FOG_COLOR
|
||||
renderer_command = [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=on","--shade=readable",
|
||||
"--headlamp=off","--height-tint=on",f"--height-center-m={height_center:.6f}",
|
||||
f"--height-range-m={height_range:.6f}","--light-direction=-0.42,-0.58,0.70",
|
||||
"--fog=on",f"--fog-start-m={fog_start}",f"--fog-end-m={fog_end}",
|
||||
f"--fog-color={fog_color}"]
|
||||
run_step(f"capture-{stem}-render",renderer_command,
|
||||
out/"logs"/f"capture-{stem}-render.log",env=env,stamped=True)
|
||||
run_step(f"capture-{stem}-jpeg",["ffmpeg","-nostdin","-hide_banner","-loglevel",
|
||||
"error","-y","-i",png,"-frames:v","1","-q:v","4",image],
|
||||
out/"logs"/f"capture-{stem}-jpeg.log",env=env)
|
||||
quality = run_step(f"capture-{stem}-quality",
|
||||
[sys.executable,ROOT/"Standalone/Tools/verify/capture_quality.py",image],
|
||||
out/"logs"/f"capture-{stem}-quality.log",env=env)
|
||||
if "[CaptureQuality]" not in quality or not image.is_file() \
|
||||
or image.stat().st_size > 300_000:
|
||||
raise RuntimeError(f"{stem} capture failed quality or size checks")
|
||||
eye_height = "1.600m" if view_name in ("eye", "cloud-up") else "wide"
|
||||
print(f"[L143Capture] fixture={name} view={view_name} eye_height={eye_height} "
|
||||
f"camera_m=({fmt(camera)}) target_m=({fmt(target)}) bytes={image.stat().st_size} "
|
||||
f"sha256={sha256(image)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def run_road_walk_audit(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-walk-audit"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-walk-audit",headless_command(headless,fixture,camera,
|
||||
run_dir/"audit.vfmesh",run_dir/"manifest.json",run_dir/"layout.json",12,1,
|
||||
audit_road=True),out/"logs"/"road-walk-audit.log",env=env,stamped=True)
|
||||
if not re.search(r"\[LiminalRoadWalkAudit\].*status=PASS",output):
|
||||
raise RuntimeError("raised road failed the Core capsule walk audit")
|
||||
|
||||
|
||||
def run_road_short_negative_control(fixture: Path, view, *, headless: Path,
|
||||
out: Path, env: dict[str, str]) -> None:
|
||||
camera = view["eye"][0]
|
||||
run_dir = out / "road-short-negative-control"
|
||||
run_dir.mkdir(parents=True,exist_ok=True)
|
||||
output = run_step("road-short-negative-control",
|
||||
headless_command(headless,fixture,camera,run_dir/"control.vfmesh",
|
||||
run_dir/"manifest.json",run_dir/"layout.json",12,1,audit_road=True),
|
||||
out/"logs"/"road-short-negative-control.log",env=env,stamped=True,
|
||||
expected_exit=14)
|
||||
match = re.search(r"\[LiminalRoadWalkAudit\].*path_length_m=([0-9.]+).*status=FAIL",output)
|
||||
if not match or float(match.group(1)) >= 300.0:
|
||||
raise RuntimeError("short-road negative control did not falsify the length gate")
|
||||
print(f"[L143RoadNegativeControl] path_length_m={match.group(1)} "
|
||||
"expected=FAIL status=PASS",flush=True)
|
||||
|
||||
|
||||
def retain_unchanged_captures(artifact_dir: Path) -> None:
|
||||
reference_dir = Path(__file__).resolve().parents[1] / "Linux142" / "captures"
|
||||
for name in ("flatField-monolith", "periodicHills"):
|
||||
for view in ("eye", "wide"):
|
||||
filename = f"{name}-{view}.jpg"
|
||||
source, destination = reference_dir / filename, artifact_dir / filename
|
||||
if not source.is_file():
|
||||
raise RuntimeError(f"missing L142 reference capture {source}")
|
||||
shutil.copy2(source,destination)
|
||||
if sha256(source) != sha256(destination):
|
||||
raise RuntimeError(f"unchanged reference capture copy differs: {filename}")
|
||||
print(f"[L143Capture] fixture={name} view={view} retained_from=Linux142 "
|
||||
f"sha256={sha256(destination)} status=PASS",flush=True)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--out", type=Path, default=OUT_DEFAULT)
|
||||
parser.add_argument("--artifact-dir", type=Path, default=CAPTURE_DEFAULT)
|
||||
parser.add_argument("--headless-clang", type=Path, required=True)
|
||||
parser.add_argument("--headless-gcc", type=Path, required=True)
|
||||
parser.add_argument("--renderer", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
out = args.out.resolve()
|
||||
artifact_dir = args.artifact_dir.resolve()
|
||||
fixture_dir = out / "fixtures"
|
||||
for directory in (out,fixture_dir,artifact_dir):
|
||||
directory.mkdir(parents=True,exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["VOXELFORGE_VERIFY_OUT"] = str(out)
|
||||
env["VOXELFORGE_OWNER_DATA"] = str(ROOT/"Standalone/data/owner_world.json")
|
||||
env["LP_NUM_THREADS"] = "12"
|
||||
env["XDG_CACHE_HOME"] = str(out/"xdg-cache")
|
||||
run_step("build-fixtures",[sys.executable,
|
||||
ROOT/"Standalone/Research/WorldAssemblerM3/Linux143/BuildL143Fixtures.py",
|
||||
f"--out={fixture_dir}"],out/"logs"/"build-fixtures.log",env=env)
|
||||
|
||||
binaries = {"clang":args.headless_clang.resolve(),"gcc":args.headless_gcc.resolve()}
|
||||
fixtures = {
|
||||
"flatField-monolith":fixture_dir/"liminal-flatField-monolith.json",
|
||||
"periodicHills":fixture_dir/"liminal-periodicHills.json",
|
||||
"road-in-clouds":fixture_dir/"liminal-road-in-clouds.json",
|
||||
"pillar-grid":fixture_dir/"liminal-pillar-grid.json",
|
||||
}
|
||||
probed = {}
|
||||
for name,fixture in fixtures.items():
|
||||
probe_dir=out/"plan-probes"/name
|
||||
probe_dir.mkdir(parents=True,exist_ok=True)
|
||||
camera=(0.0,0.0,-500.0)
|
||||
output=run_step(f"plan-probe-{name}",headless_command(binaries["clang"],fixture,
|
||||
camera,probe_dir/"probe.vfmesh",probe_dir/"manifest.json",
|
||||
probe_dir/"layout.json",12,1),out/"logs"/f"plan-probe-{name}.log",
|
||||
env=env,stamped=True)
|
||||
if "[WorldAssemblerFullManifest]" not in output:
|
||||
raise RuntimeError(f"{name} probe omitted its full plan manifest")
|
||||
probed[name]=make_views(name,probe_dir/"manifest.json",probe_dir/"layout.json")
|
||||
|
||||
for name,fixture in fixtures.items():
|
||||
run_determinism(name,fixture,probed[name],binaries=binaries,out=out,env=env)
|
||||
run_determinism("pillar-grid-far-lod",fixtures["pillar-grid"],probed["pillar-grid"],
|
||||
binaries=binaries,out=out,env=env,max_level=-1,far_lods=True,max_clip_override=8)
|
||||
run_road_walk_audit(fixtures["road-in-clouds"],probed["road-in-clouds"],
|
||||
headless=binaries["clang"],out=out,env=env)
|
||||
run_road_short_negative_control(fixture_dir/"liminal-road-short-control.json",
|
||||
probed["road-in-clouds"],headless=binaries["clang"],out=out,env=env)
|
||||
retain_unchanged_captures(artifact_dir)
|
||||
for name in ("road-in-clouds", "pillar-grid"):
|
||||
fixture=fixtures[name]
|
||||
export_and_capture(name,fixture,probed[name],headless=binaries["clang"],
|
||||
renderer=args.renderer.resolve(),out=out,artifact_dir=artifact_dir,env=env)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as error:
|
||||
print(f"[L143Capture] status=FAIL error={error}",file=sys.stderr,flush=True)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,64 @@
|
||||
# L143 — liminal polish
|
||||
|
||||
## Captures
|
||||
|
||||
### Road
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
Looking up from the cloud floor:
|
||||
|
||||

|
||||
|
||||
### Pillar grid
|
||||
|
||||
Eye-height:
|
||||
|
||||

|
||||
|
||||
Wide:
|
||||
|
||||

|
||||
|
||||
### Kept from L142
|
||||
|
||||
These four captures are byte-identical to L142.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Result
|
||||
|
||||
The road is one continuous swept deck, and the distant pillar silhouettes remain complete and consistent. The monolith and periodic hills were retained unchanged.
|
||||
|
||||
## Changes and causes
|
||||
|
||||
- The road edge broke where independently capped deck segments overlapped. The road now resolves to one swept-deck stamp with a shared cubic-Hermite centerline and continuous cross-section; the centerline is sampled at 1 m spacing (361 points). The 8 m deck is 376.829 m long and retains 17 support posts. The sweep path is serialized in stamp-vocabulary V5.
|
||||
- Pillars disappeared or narrowed when the coarse marching-cubes lattice skipped their cross-section. The pillar stamp's interval proof is unsupported, so it conservatively kept tiles but did not protect the mesh from undersampling. At LOD4 the sample spacing is 32 voxels (8 m), already greater than the 5.5 m pillar radius; spacing rises to 64 and 128 voxels at LOD5 and LOD6. The shared meshing path now emits deterministic 24-sided capped-cylinder proxies for regular `fillPillar` stamps once the sample step exceeds their radius or half-height. The same rule applies outside the liminal fixture. The radial approximation error for this fixture is about 4.7 cm, below one 25 cm voxel.
|
||||
|
||||
## Measurements
|
||||
|
||||
- Road walk audit covered the full 376.829 m route at 1,081 surface stations, with 68,103 capsule samples and 7,096 support samples: 0 blocked, 0 unsupported, 0 grade failures, and 0 surface misses. Measured maximum grade was 8.512° against a 15° limit; deck width was 8 m against a 3 m minimum. `road-short-negative-control` failed the walk audit as expected at 210.351 m.
|
||||
- Determinism covered five fixtures—monolith, periodic hills, road, pillar grid, and far-LOD pillar grid—across Clang/GCC and W1/W8/W12 (six compiler/worker cells per fixture). All five reported identical outputs.
|
||||
- Clang and GCC builds passed on source stamp `1baa3a711f7f96df2e70fc93579f41ad0b0211eb42fcb51dabf522e33d888877`. The capture runner and capture-quality checks passed for all requested views.
|
||||
- Fast tier passed in 292.3 s. Its pinned L130-v227 reach-unit owner hashes match exactly: 12/12 rows for each owner fixture, with W1 and W12 identical. Historical L46/L53 hash comparisons remain diagnostic-only and are not the current owner-hash gate.
|
||||
|
||||
## Checks and limits
|
||||
|
||||
- Passed: road audit and short-road negative control; five-fixture Clang/GCC determinism; Clang and GCC builds; capture quality; fast tier; `git diff --check`.
|
||||
- Not run: Unreal build (no Unreal `Source/` files changed) and L80 performance timing (this round made no performance claim).
|
||||
- The coarse proxy is limited to regular `fillPillar` stamps; irregular pillars and other stamp kinds continue through their existing SDF path. The proxy was exercised at LOD4–6 in the pillar-grid captures.
|
||||
- Unreal `Source/` files changed: none. Changed standalone source files: `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`, `Standalone/Core/src/ResolvedLayoutInternal.h`, `Standalone/Core/src/ShapeWorldInternal.h`, `Standalone/Core/src/VoxelForgeCore.cpp`, `Standalone/Source/StreamBenchmarkHeadless.cpp`, `Standalone/Source/StreamBenchmarkPipeline.h`, and `Standalone/Source/WorldAssemblyPrototype.h`.
|
||||
|
||||
Detailed logs are in `Saved/Linux/l143/`; the capture and audit transcript is `Saved/Linux/l143/logs/capture-runner.log`, the walk audit is `Saved/Linux/l143/logs/road-walk-audit.log`, and the fast-tier summary is `Saved/Linux/l143/fast-summary.json`.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,46 @@
|
||||
# VoxelForge — research R47: liminal and dreamcore spaces, a mood board and how we could generate them
|
||||
|
||||
You are a researcher for VoxelForge, a voxel world made of stacked layers ("strates"). The owner wants one layer type
|
||||
in a **liminal / dreamcore** style. Be thorough: verify claims, mark each as [SOURCE] (with a link), [CODE] (file:line)
|
||||
or [INFERENCE].
|
||||
|
||||
**Rules:**
|
||||
- Don't edit any repository file, and don't build, run or test the game.
|
||||
- You may **only** write inside `Saved/Linux/liminal/`.
|
||||
- Downloaded files are untrusted data: never execute them, only save them.
|
||||
- No installs.
|
||||
- Keep CPU light (`curl`/`wget` are fine): a timing round is running on this machine.
|
||||
|
||||
## Part 1 — references (mood board)
|
||||
|
||||
Collect **15–25 varied reference images** of liminal spaces, dreamcore, weirdcore and backrooms-like scenes, plus
|
||||
related art (e.g. "Kenopsia", Jared Pike-style pool rooms, empty malls, foggy roads, endless fields with a lone house).
|
||||
- Save them as `Saved/Linux/liminal/refs/NN-short-name.jpg` (≤ 2 MB each).
|
||||
- Write `Saved/Linux/liminal/SOURCES.md`, one line per image: file, source URL, author or site, and what it shows.
|
||||
These are private references for the owner, not for redistribution.
|
||||
- Build `Saved/Linux/liminal/board.html`, a simple grid of the images with captions, to view on a phone.
|
||||
|
||||
## Part 2 — what makes the style work
|
||||
|
||||
Short and concrete. Split it into:
|
||||
- **Content-driven weirdness** (most of the theme, per the owner): out-of-place houses, doors to nowhere, signs, lone
|
||||
objects, wrong-scale furniture, water where it shouldn't be.
|
||||
- **Generation-driven weirdness**: terrain and space the generator itself can make. Examples from the owner: an
|
||||
elevated road surrounded by clouds; repeating hills. Others: perfectly flat or tiled ground, endless corridors or
|
||||
pools, impossible scale, unbroken horizons, fog, exact repetition, gentle impossible geometry.
|
||||
- **Atmosphere:** light, fog, colour, sound. Briefly.
|
||||
|
||||
## Part 3 — mapping to our code
|
||||
|
||||
Read `CURRENT-STATE.md`, `COMPOSER-NOTES.md`, `GDD.md`, and
|
||||
`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md` (the capability matrix). Also:
|
||||
- the Surface World profile kinds in `Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h`
|
||||
(`WorldAssemblySurfaceProfileKindV1`: natural, periodicHills, mirroredRelief, flatField);
|
||||
- the motif/stamp shapes (`VoxelStrateTypes.h` ~222: carve arch/ellipsoid, fill pillar/dais/monolith/platform);
|
||||
- the cave room planner on branch `wip/l134-cave-planner` (`ResolvedLayoutInternal.h`, `makeCompound`).
|
||||
|
||||
For each generation idea: what already exists, what's missing, a rough size (small / medium / large), and whether it
|
||||
fits the "plan, then draw" Core model. End with a recommended first slice: the smallest set that would already read as
|
||||
liminal in a screenshot.
|
||||
|
||||
Write `Saved/Linux/liminal/REPORT.md` (≤ ~1,200 words) and print a short summary at the end.
|
||||
@@ -0,0 +1,30 @@
|
||||
# VoxelForge R47 — liminal / dreamcore
|
||||
|
||||
## Result
|
||||
|
||||
[INFERENCE] Start with a rare, content-led anomaly on a flat SurfaceWorld: one ordinary, recognizable object placed where it makes no sense, framed by a quiet empty field and mild horizon fog. This can read in a screenshot without a new generator family.
|
||||
|
||||
[CODE] The requested references describe different snapshots: `CURRENT-STATE.md` labels the tree L132/Core v227 (`CURRENT-STATE.md:3,7`), while the live Core header says evaluator v236 (`Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:513`). R46 calls itself a review of an earlier `assembler/world` snapshot (`Standalone/Research/WorldAssemblerM3/Research46/REPORT.md:3`). The mapping below follows the live source; R46 is historical context.
|
||||
|
||||
## 1. What makes it work
|
||||
|
||||
- **Content-driven weirdness (the main signal).** [INFERENCE] Keep most geometry ordinary, then break one semantic expectation: a lone house in an indoor hall; a door standing in a field; a sign that points nowhere; a child-sized chair in a huge room; or still pool water in a dry corridor. Repeat one object exactly or vary its scale to make the wrongness legible. [SOURCE] ETH Zürich describes dreamcore as familiar scenes that are hard to place, including impossible proportions; Jared Pike’s *Dream Pools* frames endless pool environments as eerie nostalgia. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/) · [Jared Pike](https://www.jaredpike.art/projects/dream-pools/)
|
||||
- **Generation-driven weirdness.** [INFERENCE] Use perfectly flat ground, exact repeated hills, a corridor or pool that returns to itself, an elevated road over clouds, oversized rooms, or a horizon with no visible edge. Favor one simple rule repeated cleanly over unrelated noise. [SOURCE] Liminal spaces often use familiar passage places with their expected people or activity absent, making them uncanny. [Liminal space](https://en.wikipedia.org/wiki/Liminal_space) [SOURCE] ETH Zürich describes dreamcore as recognizable but ambiguous scenes. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||
- **Atmosphere.** [INFERENCE] Try cool fluorescent light, a weak warm pool of color, low horizon fog and sparse room tone (fluorescent hum, wind, or a looping distant splash); leave large areas quiet. [SOURCE] ETH Zürich notes that looping sound and ambiguous cues help make dreamcore recognizable but not fully explainable. [ETH Zürich](https://dbt.arch.ethz.ch/latent-spaces/)
|
||||
|
||||
## 2. Mapping to VoxelForge
|
||||
|
||||
| Idea | Already exists / missing | Size and “plan, then draw” fit |
|
||||
|---|---|---|
|
||||
| Wrong-place props and landmarks | [CODE] Shared decorations and rare landmarks can spawn actors or instanced meshes with deterministic placements (`ARCHITECTURE.md:267-349`; `Source/VoxelForge/Public/VoxelStrateTypes.h:2407,2471`). [CODE] Generated schema-2 slots currently carry no content/visual record, and theme draws for materials, creatures and audio remain open (`COMPOSER-NOTES.md:115-117,1484-1485`). | [INFERENCE] **Small** for one authored prop kit; **medium** to make the composer select/store coherent content. [INFERENCE] Fits a resolved placement plan and later Unreal draw; these props are not Core density geometry. |
|
||||
| Flat ground and repeating hills | [CODE] Surface profile kinds include `flatField`, `periodicHills` and `mirroredRelief`; seeded phases are resolved during planning (`VoxelForgeCore.h:560-610`). No profile kind describes tiled ground materials. | [INFERENCE] **Small** to select a flat or periodic profile; **medium** for a dedicated tiled material/pattern pass. [CODE] Yes: profile parameters are recipe/plan data consumed by the Core field. |
|
||||
| Raised road among clouds | [CODE] Floating Islands is an existing family (`VoxelStrateTypes.h:196-197`); persistent ceiling/floor actors can make a cloud sea (`ARCHITECTURE.md:457-464`), and landmarks can carry a mesh bridge. [INFERENCE] There is no demonstrated Core road/path structure that couples its geometry to the island route. | [INFERENCE] **Medium** for a finite authored bridge or platform chain; **large** for a robust generated road network. [INFERENCE] A finite road can be planned as records and drawn as mesh/content; cloud actors stay in Unreal presentation, outside Core density evaluation. |
|
||||
| Endless corridors and pools | [CODE] The cave planner makes a deterministic finite room-and-tunnel graph between passage mouths (`VoxelForgeCore.h:1314-1324`); `makeCompound` emits two to four overlapping room primitives (`ResolvedLayoutInternal.h:3037-3045`). Core exposes water channels (`VoxelForgeCore.h:515-519`). [INFERENCE] The planner is not an endless loop grammar and does not specify a tiled pool basin/waterline. | [INFERENCE] **Medium** for a finite repeated room/pool set-piece; **large** for continuous repeat with reliable traversal and water presentation. [CODE] Finite rooms/edges fit plan-then-draw; truly unbounded repetition needs a new deterministic local-repeat field/streaming rule. |
|
||||
| Impossible scale, horizon and geometry | [CODE] Resolved stamps already include carve ellipsoid/arch and fill pillar/dais/monolith/platform with bounded, stable placement (`VoxelForgeCore.h:912-941`); stamps are composed after boundaries/passages (`ARCHITECTURE.md:2242-2265`). [INFERENCE] They can sell oversized objects or a standalone arch, but not non-Euclidean topology or a genuinely impossible connection. | [INFERENCE] **Small** for bounded silhouette changes; **large** for topology-changing spaces. [CODE] Bounded stamp records fit plan-then-draw; topology changes do not fit the current finite stamp vocabulary. |
|
||||
| Fog, color and sound | [CODE] Per-strate fog, skylight tint/intensity and persistent layer actors exist (`ARCHITECTURE.md:457-470`; `VoxelStrateDefinition.h:283-362`). [CODE] Composer theme draws for audio are still open (`COMPOSER-NOTES.md:1484-1485`). | [INFERENCE] **Small** to reuse fog/light settings; **medium** to author a coherent audio/theme selection. [INFERENCE] Fits a presentation record selected before draw, but remains outside Core density planning. |
|
||||
|
||||
[CODE] The game design requires every strate to keep a findable open-air entrance and exit and says anomalies should be rare and stand apart from grounded neighbours (`GDD.md:132-153`). [CODE] The `motif_family_override` prototype path is restricted to resolved SurfaceWorld/flatField recipes (`Standalone/Source/WorldAssemblyPrototype.h:921-943`); the shared stamp vocabulary itself is broader.
|
||||
|
||||
## Recommended first slice
|
||||
|
||||
[INFERENCE] Author one rare SurfaceWorld `flatField` layer with a single familiar doorway or small house mesh placed alone at an implausible scale, plus low horizon fog and one soft light tint. Keep the rest empty; the viewer should recognize the object before noticing the place has no sensible context. This uses existing profile, placement and atmosphere hooks, so the screenshot slice is **small** and does not require a new density family. [CODE] The mesh placement/presentation remains separate from Core’s density plan, and generated schema-2 slots do not yet carry its visual record (`COMPOSER-NOTES.md:115-117`); a composer-ready version will need that record and theme tags.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Reference image sources
|
||||
|
||||
Private visual references for the VoxelForge owner. This is not a redistribution set; consult each source page and its rights statement before reusing an image. These are local display copies; some were resized to meet the per-image size limit.
|
||||
|
||||
- `01-backrooms-origin.jpg` — [SOURCE] [Bill Magritz (creator) / Bob Mazza (copyright holder)](https://commons.wikimedia.org/wiki/File:HobbyTown_USA_Oshkosh_interior_under_construction_2002_(The_Backrooms).jpg); HobbyTown USA Oshkosh interior under construction 2002, the image associated with “The Backrooms”.
|
||||
- `02-empty-office.jpg` — [SOURCE] [Ged Carroll](https://commons.wikimedia.org/wiki/File:Empty_office.jpg); Empty office interior.
|
||||
- `03-empty-mall-hallway.jpg` — [SOURCE] [Michael Rivera (Mjrmtg)](https://commons.wikimedia.org/wiki/File:Empty_Hallway,_Tallahassee_Mall.JPG); Tallahassee Mall hallway.
|
||||
- `04-mall-source.jpg` — [SOURCE] [Jtalledo](https://commons.wikimedia.org/wiki/File:Mall_at_The_Source_empty.jpg); Mall at The Source, with people present.
|
||||
- `05-ruoholahti-evening.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_Ruoholahti_shopping_centre_empty_of_people_on_an_evening_in_September_2022.jpg); Ruoholahti shopping centre interior, September 2022.
|
||||
- `06-city-center-night.jpg` — [SOURCE] [JIP](https://commons.wikimedia.org/wiki/File:Interior_of_City-Center_empty_of_people_on_a_night_in_January_2025.jpg); City-Center shopping-centre interior at night, January 2025.
|
||||
- `07-school-hallway.jpg` — [SOURCE] [Warren LeMay](https://commons.wikimedia.org/wiki/File:Hallway,_Log_Cabin_Association_Elementary_School,_Barkers_Creek,_NC_(39684209853).jpg); Hallway in Log Cabin Association Elementary School, Barkers Creek, North Carolina.
|
||||
- `08-central-high-hallway.jpg` — [SOURCE] [Thomas J. O’Halloran](https://commons.wikimedia.org/wiki/File:Mostly_empty_hallway_at_Central_High_School_in_Little_Rock.tif); Mostly empty hallway at Central High School, Little Rock.
|
||||
- `09-hearst-pool.jpg` — [SOURCE] [Mike McBey](https://commons.wikimedia.org/wiki/File:Indoor_pool,_Hearst_Castle_(32062000267).jpg); Indoor pool at Hearst Castle.
|
||||
- `10-ymca-pool.jpg` — [SOURCE] [Joe Mabel](https://commons.wikimedia.org/wiki/File:Vancouver_-_Robert_Lee_YMCA_pool_01.jpg); Robert Lee YMCA pool, Vancouver.
|
||||
- `11-jerma-hotel-pool.jpg` — [SOURCE] [Anonymous uploader](https://commons.wikimedia.org/wiki/File:Jerma_Hotel_Indoor_Pool_(first_angle).jpg); Jerma Hotel indoor pool.
|
||||
- `12-dream-pools-01.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||
- `13-dream-pools-02.jpg` — [SOURCE] [Jared Pike (artist’s official site)](https://www.jaredpike.art/projects/dream-pools/); Dream Pools series.
|
||||
- `14-foggy-road.jpg` — [SOURCE] [Dennis Bruijn](https://commons.wikimedia.org/wiki/File:Foggy_Road_(67154193).jpeg); Foggy Road (500px image).
|
||||
- `15-driving-through-mist.jpg` — [SOURCE] [Tomasz Zagórski (Unsplash contributor)](https://commons.wikimedia.org/wiki/File:Driving_through_mist_(Unsplash).jpg); Driving through mist.
|
||||
- `16-foggy-campus.jpg` — [SOURCE] [Ramesh Lalwani](https://commons.wikimedia.org/wiki/File:Foggy_Morning_in_IIT.jpg); Foggy Morning in IIT.
|
||||
- `17-empty-playground.jpg` — [SOURCE] [Ian Paterson](https://commons.wikimedia.org/wiki/File:Empty_playground_-_geograph.org.uk_-_4118218.jpg); Empty playground, Geograph image 4118218.
|
||||
- `18-playground-park.jpg` — [SOURCE] [mycurrency.com (uploader credit)](https://commons.wikimedia.org/wiki/File:Empty_Playground_in_a_park.jpg); Empty playground in a park.
|
||||
- `19-lone-house.jpg` — [SOURCE] [Mayqel](https://commons.wikimedia.org/wiki/File:Lone_House.jpg); Lone House, Blender render.
|
||||
- `20-door-to-nowhere.jpg` — [SOURCE] [Benmckune](https://commons.wikimedia.org/wiki/File:Door_to_Nowhere_Winchester_House_6-5-06.jpg); “Door to Nowhere” at Winchester Mystery House.
|
||||
- `21-abandoned-theater.jpg` — [SOURCE] [Noki Va](https://commons.wikimedia.org/wiki/File:Abandoned_Theater_(97702985).jpeg); Abandoned Theater.
|
||||
- `22-empty-retail-space.jpg` — [SOURCE] [Wikimedia Commons (uploader credit on source page)](https://commons.wikimedia.org/wiki/File:Empty_Retail_Space.jpg); Empty retail space.
|
||||
- `23-historic-rail-platform.jpg` — [SOURCE] [Ben Brooksbank](https://commons.wikimedia.org/wiki/File:Euston_railway_station_geograph-2197358-by-Ben-Brooksbank.jpg); Euston railway station, historic view.
|
||||
- `24-empty-airport-terminal.jpg` — [SOURCE] [Matt @ PEK](https://commons.wikimedia.org/wiki/File:Empty_terminal_in_TPE_airport_(4287773502).jpg); Empty terminal in TPE airport.
|
||||
@@ -0,0 +1,36 @@
|
||||
# VoxelForge — research R48 (read-only): who owns the tunnel mouth?
|
||||
|
||||
You are a researcher. **Read only:** don't edit, create or delete any file, and don't build, run or test anything.
|
||||
Read code, reports and design docs. Be thorough, verify by hand, and mark each claim [CODE] file:line, [MEASURED]
|
||||
report, or [INFERENCE]. This checkout (`Saved/Linux/r48-tree`) is branch `wip/l134-cave-planner` at `4f93f3f`.
|
||||
|
||||
## The problem
|
||||
|
||||
Inter-strate passages (tunnels between layers) are planned first, and each layer's content is planned afterwards
|
||||
around their fixed mouths and tubes.
|
||||
- **Tunnel Network** (L134–L138) works, because caves are carved into rock and can bend to the mouths.
|
||||
- **Vertical Shafts** (L139): 15/16 seeds pass; seed 2 fails the shell and seams at the arrival mouth. Three local
|
||||
geometry fixes failed.
|
||||
- **Floating Islands** (L133 twice, then L140): 0/16 seeds pass.
|
||||
- In 14 of 16 seeds, no island arrangement clears the fixed tubes and reaches both mouth heights.
|
||||
- One seed has a composed collision.
|
||||
- Wall ripple is 6–7 cm against the 3.5 cm limit on the passages.
|
||||
|
||||
Read the L133, L139 and L140 reports and patches in `Standalone/Research/WorldAssemblerM3/`.
|
||||
|
||||
## Questions
|
||||
|
||||
1. Where and in what order are passages and layer contents planned (adapter, `ResolvedLayoutInternal.h`, the
|
||||
passage planner, the island and cave planners)? What does each stage fix that later stages must obey?
|
||||
2. Which designs would make mouths solvable by construction for every layer type? For example:
|
||||
- **content first:** the layer plans its landing spots, and passages are routed to target them;
|
||||
- **negotiated:** passages reserve a mouth landing region, and every layer planner must provide support there,
|
||||
a "landing pad" contract;
|
||||
- **a joint plan:** a corridor and the island anchors are solved together.
|
||||
For each: what changes, the risks (determinism, the existing audits, the passage smoothness/ripple work), and the
|
||||
size.
|
||||
3. Why does ripple fail at 6–7 cm on the upper passages with native islands (and in L67)? Is it the passage, or the
|
||||
host field under it?
|
||||
4. Recommend one design, with steps and the evidence that would prove or kill it.
|
||||
|
||||
Write plain Markdown, ≤ ~1,200 words.
|
||||
@@ -0,0 +1,43 @@
|
||||
# R48 read only — who owns the tunnel mouth?
|
||||
|
||||
**The passage planner currently chooses the mouth.** Floating Islands then has to fit its islands and walk route around fixed tubes; caves can instead carve toward them. L140’s fixed tube layout left no valid island plan for most seeds. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:3699–3792`; [MEASURED] `Standalone/Research/WorldAssemblerM3/Linux140/REPORT.md:15–19`
|
||||
|
||||
## 1. Planning order and constraints
|
||||
|
||||
There are two adapter paths, so there isn’t one universal order today:
|
||||
|
||||
- The geometry only adapter builds strata and passage configs, plans passages, then resolves authored Core geometry. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1455–1585`
|
||||
- The validated assembly path can build a manifest, including an imported Floating Islands plan, before creating the resolved layout. It copies plan anchors into that layout, then calls the passage planner. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1621–1685, 1950–2019, 2021–2041`
|
||||
- L140’s kept patch changes the Floating Islands path: first resolve the fixed passage layout, then plan islands around those passages and export the resulting anchors. The patch is not applied in the R48 source: the current adapter still calls `PlanFloatingIslandCluster` without a resolved layout. [CODE] `Standalone/Research/WorldAssemblerM3/Linux140/l140-fi-around-tunnels.patch:102–219`; `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:893–987`
|
||||
|
||||
`PlanResolvedLayoutPassagesV1` sorts the strata, uses existing route anchors where present, otherwise chooses seeded mouths, and builds passage control points with radii. Once resolved, those mouths and tubes are inputs to later planners. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:3699–3792, 3820–3874, 4713–4819`
|
||||
|
||||
The generic island planner lays out seeded island centers and a route without seeing the passage layout. L133’s alternate planner bound endpoints, but did not plan against both full passage volumes. Its composed walk still had collisions and unsupported route points. [CODE] `Standalone/Core/src/FloatingIslandsInternal.h:1991–2050, 2166–2233`; historical patch `Linux133/l133-native-fi-v9.patch` in Git commit `f8cba7c:79–134`; [MEASURED] `Linux133/REPORT.md` in that commit:3–7
|
||||
|
||||
The cavern planner reads incoming and outgoing passages and requires them to exist, except for a Vertical Shafts basal terminal. Caves can carve through host rock to reach those mouths; L134’s default composed route passed, while L139 still failed the shell and seam checks at seed 2’s incoming mouth after three local geometry attempts. [CODE] `Standalone/Core/src/ResolvedLayoutInternal.h:2344–2359`; [MEASURED] `Linux134/REPORT.md:3–7`; `Linux139/REPORT.md:5–8, 43–52, 89–91`
|
||||
|
||||
## 2. Design choices
|
||||
|
||||
- **Content first:** each layer produces supported landing candidates and a connected approach; the passage planner chooses compatible candidates and routes to them. This can solve the fixed-height problem by letting the tunnel target viable terrain. It requires every layer type to provide reliable candidates, stable ordering and tie breaks, and passage rerouting; changed routes must still pass the existing grade, shell, seam, and ripple audits. **Size: medium to large.** [INFERENCE]
|
||||
|
||||
- **Negotiated landing pad:** the passage reserves a landing region and the layer guarantees support there. A point or pad contract alone is too weak: L140 checked five support probes, yet 14 seeds had no arrangement clearing the fixed tubes or could not reach both mouth heights. A useful contract must cover the capsule approach and passage exclusion envelope too. That grows toward a joint plan. **Size: medium for a pad; large for a full approach and clearance contract.** [MEASURED] `Linux140/REPORT.md:7, 15–17`; [INFERENCE]
|
||||
|
||||
- **Joint plan:** choose passage geometry and layer anchors together. This gives the solver room to avoid the fixed-tube infeasibility seen in L140, but costs the most: bounded deterministic search, new planner interfaces, and careful preservation of passage smoothness and audit behavior. A bounded search only guarantees feasibility within its candidate space. **Size: large.** [MEASURED] `Linux140/REPORT.md:17`; [INFERENCE]
|
||||
|
||||
## 3. Why ripple reaches 6–7 cm
|
||||
|
||||
The ripple audit samples the **composed field** passed as `Shape`, at `pose.radius − 0.18 m`, then estimates wall radius as that sample radius plus the average signed density. It cubic-detrends each 32 m window, preserving authored radius variation. Thus the result includes both the passage’s radius profile and any host-field effect on the sampled wall; it is not a passage-only measurement. [CODE] `Standalone/Source/StreamBenchmarkPipeline.h:4186–4239, 4414–4432, 4941–4944`
|
||||
|
||||
L67 traced a prior gauge error: omitting `pose.radius` hid an authored-radius beat. The corrected gauge found a 4.77 cm beat at 7.06 m and measured 7.27 cm on passage 1–2. L140 measured 6.22–7.19 cm on its upper passages with native islands. [MEASURED] `Linux67/REPORT.md:64–79`; `Linux140/REPORT.md:15–19`
|
||||
|
||||
The evidence shows that passage-radius variation alone is enough to explain a failure at this scale; it does not isolate how much native island density adds or masks. L140’s shell-preserving composition change still failed the unchanged ripple gate. [INFERENCE] `Linux140/l140-fi-around-tunnels.patch:912–924`; [MEASURED] `Linux140/REPORT.md:15–19`
|
||||
|
||||
## 4. Recommendation
|
||||
|
||||
**Use a bounded pairwise joint plan.** Have each layer planner publish certified landing candidates with capsule support and approach constraints. Let the passage planner choose compatible ports and route geometry together, then check the complete tube and composed layer before finalizing the resolved layout. Start with Floating Islands; preserve cave planners’ ability to carve to selected ports. [INFERENCE]
|
||||
|
||||
Keep candidate generation and tie breaking seed-stable, and finalize the layout identity only after the selected mouths, tubes, and content anchors are known. Keep the existing thresholds unchanged. [CODE] `Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:2021–2041`; [INFERENCE]
|
||||
|
||||
The design passes if all 16 seeds clear the existing route, support, shell, seam, grade, mouth-profile, and 3.5 cm ripple gates, with matching Clang/GCC W1/W8/W12 hashes. Separately compare ripple on the same tube in a smooth host field and in the composed field; that isolates passage-profile ripple from host-field contribution. Kill the design if joint movement still leaves seeds infeasible, or if the passage-only profile fails ripple. [INFERENCE]
|
||||
|
||||
L133’s report and patch were absent from the R48 working tree; I read them from Git commit `f8cba7c`. No files were changed, and I did not build or run tests. [MEASURED]
|
||||
@@ -592,6 +592,11 @@ uniform float uHeightCenter;
|
||||
uniform float uHeightRange;
|
||||
uniform float uTileEdgeWidth;
|
||||
uniform vec3 uLightDirection;
|
||||
uniform vec3 uCameraWorld;
|
||||
uniform vec3 uFogColor;
|
||||
uniform float uFogStart;
|
||||
uniform float uFogEnd;
|
||||
uniform int uFogEnabled;
|
||||
uniform int uHeadlamp;
|
||||
uniform int uGrazingLight;
|
||||
uniform int uClipEnabled;
|
||||
@@ -643,6 +648,11 @@ void main()
|
||||
color=mix(color,resourceColor/max(resourceWeight,0.001),
|
||||
clamp(resourceWeight,0.0,0.95))*illumination;
|
||||
}
|
||||
if(uFogEnabled!=0)
|
||||
{
|
||||
float fogT=smoothstep(uFogStart,uFogEnd,length(vWorldPosition-uCameraWorld));
|
||||
color=mix(color,uFogColor,fogT);
|
||||
}
|
||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
||||
{
|
||||
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
|
||||
@@ -707,7 +717,8 @@ void main()
|
||||
bool HeightTint,bool Headlamp,bool TileEdges,
|
||||
double TileEdgeWidthMeters,bool HasLightDirection,
|
||||
const std::array<double,3>& LightDirection,bool ClipPlaneEnabled,
|
||||
const std::array<double,4>& ClipPlane)
|
||||
const std::array<double,4>& ClipPlane,bool FogEnabled,
|
||||
double FogStartMeters,double FogEndMeters,const std::array<double,3>& FogColor)
|
||||
{
|
||||
EGLDisplay Display=MakeDisplay();
|
||||
EGLint Major=0,Minor=0;
|
||||
@@ -796,7 +807,10 @@ void main()
|
||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||
// A covered empty ray represents open air. Keep it visibly blue so it cannot
|
||||
// be mistaken for the old near-black result from a scene that lacked tiles.
|
||||
glDepthFunc(GL_LEQUAL);glClearColor(0.30f,0.48f,0.62f,1.0f);
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glClearColor(FogEnabled?static_cast<float>(FogColor[0]):0.30f,
|
||||
FogEnabled?static_cast<float>(FogColor[1]):0.48f,
|
||||
FogEnabled?static_cast<float>(FogColor[2]):0.62f,1.0f);
|
||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||
glUseProgram(Program);
|
||||
glUniform1f(glGetUniformLocation(Program,"uTanHalfHorizontal"),static_cast<float>(TanHalfHorizontal));
|
||||
@@ -825,6 +839,18 @@ void main()
|
||||
static_cast<float>(ActiveLightDirection[0]),
|
||||
static_cast<float>(ActiveLightDirection[1]),
|
||||
static_cast<float>(ActiveLightDirection[2]));
|
||||
glUniform3f(glGetUniformLocation(Program,"uCameraWorld"),
|
||||
static_cast<float>(Scene.CameraMeters[0]),
|
||||
static_cast<float>(Scene.CameraMeters[1]),
|
||||
static_cast<float>(Scene.CameraMeters[2]));
|
||||
glUniform3f(glGetUniformLocation(Program,"uFogColor"),
|
||||
static_cast<float>(FogColor[0]),static_cast<float>(FogColor[1]),
|
||||
static_cast<float>(FogColor[2]));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFogStart"),
|
||||
static_cast<float>(FogStartMeters));
|
||||
glUniform1f(glGetUniformLocation(Program,"uFogEnd"),
|
||||
static_cast<float>(FogEndMeters));
|
||||
glUniform1i(glGetUniformLocation(Program,"uFogEnabled"),FogEnabled?1:0);
|
||||
glDrawElements(GL_TRIANGLES,static_cast<GLsizei>(Scene.Indices.size()),GL_UNSIGNED_INT,nullptr);
|
||||
glFinish();
|
||||
std::vector<std::uint8_t> Pixels(static_cast<std::size_t>(Width)*Height*4);
|
||||
@@ -863,7 +889,7 @@ int main(int argc,char** argv)
|
||||
if(argc==2&&std::string_view(argv[1])=="--print-source-stamp")return 0;
|
||||
if(argc<3)
|
||||
{
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]);
|
||||
std::fprintf(stderr,"Usage: %s scene.vfmesh output.png [--size=WIDTHxHEIGHT] [--fov-deg=90] [--height-range-m=N] [--height-center-m=N] [--camera-m=X,Y,Z] [--yaw-deg=N] [--pitch-deg=N] [--shade=readable|normal] [--height-tint=on|off] [--headlamp=on|off] [--light-direction=X,Y,Z] [--fog=on|off] [--fog-start-m=N] [--fog-end-m=N] [--fog-color=R,G,B] [--tile-edges=on|off] [--tile-skirts=on|off] [--tile-edge-width-m=N] [--clip-plane=NX,NY,NZ,D] [--focus-region=X,Y,Z,R]\n",argv[0]);
|
||||
return 2;
|
||||
}
|
||||
int Width=2560,Height=1440;double Fov=90.0,HeightRangeMeters=64.0;
|
||||
@@ -871,6 +897,8 @@ int main(int argc,char** argv)
|
||||
bool HasYaw=false,HasPitch=false;double Yaw=0.0,Pitch=0.0;
|
||||
bool HasCamera=false;std::array<double,3> Camera{};
|
||||
bool HasLightDirection=false;std::array<double,3> LightDirection{};
|
||||
bool FogEnabled=false;double FogStartMeters=50.0,FogEndMeters=200.0;
|
||||
std::array<double,3> FogColor={0.62,0.70,0.75};
|
||||
bool ReadableMode=true,HeightTint=true,TileEdges=false,TileSkirts=true,Headlamp=false;
|
||||
double TileEdgeWidthMeters=0.15;
|
||||
bool ClipPlaneEnabled=false;std::array<double,4> ClipPlane{};
|
||||
@@ -967,6 +995,38 @@ int main(int argc,char** argv)
|
||||
{std::fprintf(stderr,"Invalid light direction; use X,Y,Z with non-zero length\n");return 2;}
|
||||
HasLightDirection=true;
|
||||
}
|
||||
else if(Arg.starts_with("--fog="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(6);
|
||||
if(Mode=="on")FogEnabled=true;
|
||||
else if(Mode=="off")FogEnabled=false;
|
||||
else {std::fprintf(stderr,"Invalid fog mode\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-start-m="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(14));
|
||||
FogStartMeters=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(FogStartMeters)
|
||||
||FogStartMeters<0.0||FogStartMeters>20000.0)
|
||||
{std::fprintf(stderr,"Invalid fog start distance\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-end-m="))
|
||||
{
|
||||
char* End=nullptr;const std::string Value(Arg.substr(12));
|
||||
FogEndMeters=std::strtod(Value.c_str(),&End);
|
||||
if(End==Value.c_str()||*End!='\0'||!std::isfinite(FogEndMeters)
|
||||
||FogEndMeters<=0.0||FogEndMeters>20000.0)
|
||||
{std::fprintf(stderr,"Invalid fog end distance\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--fog-color="))
|
||||
{
|
||||
const std::string Value(Arg.substr(12));
|
||||
if(std::sscanf(Value.c_str(),"%lf,%lf,%lf",&FogColor[0],&FogColor[1],
|
||||
&FogColor[2])!=3
|
||||
||std::any_of(FogColor.begin(),FogColor.end(),[](double Channel)
|
||||
{return !std::isfinite(Channel)||Channel<0.0||Channel>1.0;}))
|
||||
{std::fprintf(stderr,"Invalid fog color; use R,G,B in 0..1\n");return 2;}
|
||||
}
|
||||
else if(Arg.starts_with("--tile-edges="))
|
||||
{
|
||||
const std::string_view Mode=Arg.substr(13);
|
||||
@@ -1002,6 +1062,8 @@ int main(int argc,char** argv)
|
||||
}
|
||||
else {std::fprintf(stderr,"Unknown option: %.*s\n",static_cast<int>(Arg.size()),Arg.data());return 2;}
|
||||
}
|
||||
if(FogEnabled&&FogEndMeters<=FogStartMeters)
|
||||
{std::fprintf(stderr,"Fog end distance must be greater than fog start distance\n");return 2;}
|
||||
FScene Scene;std::string Error;
|
||||
if(!LoadScene(argv[1],Scene,Error))
|
||||
{std::fprintf(stderr,"[OffscreenCapture] FAIL: %s\n",Error.c_str());return 2;}
|
||||
@@ -1077,14 +1139,16 @@ int main(int argc,char** argv)
|
||||
FocusRadius,static_cast<unsigned long long>(Scene.DrawnTriangles));
|
||||
}
|
||||
}
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s vertices=%zu triangles=%llu\n",
|
||||
std::printf("[OffscreenScene] voxels_per_meter=%.6f camera_m=(%.3f,%.3f,%.3f) yaw=%.2f pitch=%.2f height_m=%.3f..%.3f height_center_m=%.3f shade=%s height_tint=%s tile_edges=%s fog=%s fog_distance_m=%.1f..%.1f fog_color=(%.2f,%.2f,%.2f) vertices=%zu triangles=%llu\n",
|
||||
1000000.0/Scene.VoxelSizeMicrons,Scene.CameraMeters[0],Scene.CameraMeters[1],Scene.CameraMeters[2],
|
||||
Scene.YawDegrees,Scene.PitchDegrees,Scene.MinimumHeight,Scene.MaximumHeight,
|
||||
HeightCenterMeters,
|
||||
ReadableMode?"readable":"normal",HeightTint?"on":"off",
|
||||
TileEdges?"on":"off",Scene.Vertices.size(),
|
||||
TileEdges?"on":"off",FogEnabled?"on":"off",FogStartMeters,FogEndMeters,
|
||||
FogColor[0],FogColor[1],FogColor[2],Scene.Vertices.size(),
|
||||
static_cast<unsigned long long>(Scene.Triangles));
|
||||
return Render(Scene,argv[2],Width,Height,Fov,HeightRangeMeters,HeightCenterMeters,ReadableMode,
|
||||
HeightTint,Headlamp,TileEdges,
|
||||
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane)?0:3;
|
||||
TileEdgeWidthMeters,HasLightDirection,LightDirection,ClipPlaneEnabled,ClipPlane,
|
||||
FogEnabled,FogStartMeters,FogEndMeters,FogColor)?0:3;
|
||||
}
|
||||
@@ -108,6 +108,7 @@ namespace
|
||||
bool ResolvedLayoutOnly=false;
|
||||
bool AuditResolvedLayoutBoundaries=false;
|
||||
bool AuditResolvedLayoutPassages=false;
|
||||
bool AuditLiminalRoadWalk=false;
|
||||
bool AuditSurfaceClearance=false;
|
||||
bool AuditStrateTransitions=false;
|
||||
int TransitionModeOverride=-1;
|
||||
@@ -1447,6 +1448,7 @@ namespace
|
||||
" --audit-strate-transitions (measure interface crossing, band thickness and holes)\n"
|
||||
" --transition-mode=hard|gradient|interleaved --transition-width-chunks=N (fixture override)\n"
|
||||
" --audit-resolved-layout-passages (capsule, support, route-binding and blocked-control audit)\n"
|
||||
" --audit-liminal-road-walk (capsule, support, width and grade audit for the liminal road)\n"
|
||||
" --audit-heightfield-profile (brute-force the profiled field intervals and route)\n"
|
||||
" --audit-motif-intervals (brute-force motif bounds, BVH and shape-free guards)\n"
|
||||
" --audit-landmark-reachability (capsule, support, grade and buried-L43 control)\n"
|
||||
@@ -1494,6 +1496,7 @@ namespace
|
||||
else if(arg=="--resolved-layout-only")options.ResolvedLayoutOnly=true;
|
||||
else if(arg=="--audit-resolved-layout-boundaries")options.AuditResolvedLayoutBoundaries=true;
|
||||
else if(arg=="--audit-resolved-layout-passages")options.AuditResolvedLayoutPassages=true;
|
||||
else if(arg=="--audit-liminal-road-walk")options.AuditLiminalRoadWalk=true;
|
||||
else if(arg=="--native-owner-fi")options.NativeOwnerFloatingIslandsRecipe=true;
|
||||
else if(arg=="--density-dispatch-report")
|
||||
{options.DensityDispatchReport=true;options.StreamHash=true;options.StreamHashTiles=true;}
|
||||
@@ -1875,7 +1878,16 @@ int main(int argc,char** argv)
|
||||
&&!StandaloneWorldAssemblyPrototype::ValidateExportedCorePlanMatchesOwner(
|
||||
*ownerWorld,worldAssembly,resolvedLayout,error))
|
||||
{std::fprintf(stderr,"Unreal-resolved Core plan/owner slot validation failed: %s\n",error.c_str());return 2;}
|
||||
if(!std::holds_alternative<std::nullptr_t>(ownerJsonRoot.Get("motif_family_override").Data))
|
||||
const bool hasMotifFamily=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("motif_family_override").Data);
|
||||
const bool hasLiminalRoad=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_road_override").Data);
|
||||
const bool hasLiminalPillarGrid=!std::holds_alternative<std::nullptr_t>(
|
||||
ownerJsonRoot.Get("liminal_pillar_grid_override").Data);
|
||||
if((hasMotifFamily&&hasLiminalRoad)||(hasMotifFamily&&hasLiminalPillarGrid)
|
||||
||(hasLiminalRoad&&hasLiminalPillarGrid))
|
||||
{std::fprintf(stderr,"liminal content overrides must be used in separate fixtures\n");return 2;}
|
||||
if(hasMotifFamily)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildMotifFamilyRequests(ownerJsonRoot,
|
||||
@@ -1903,6 +1915,76 @@ int main(int argc,char** argv)
|
||||
static_cast<unsigned long long>(motif.irregularitySeed));
|
||||
}
|
||||
}
|
||||
if(hasLiminalRoad)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalRoadRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal road resolution failed: %s\n",error.c_str());return 2;}
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
std::size_t supportCount=0;
|
||||
const std::string deckPrefix="liminal/road-clouds-v2/deck/";
|
||||
const std::string supportPrefix="liminal/road-clouds-v2/support/";
|
||||
for(const auto& stamp:resolvedLayout.stamps)
|
||||
{
|
||||
if(stamp.stableId.starts_with(deckPrefix))deck.push_back(&stamp);
|
||||
else if(stamp.stableId.starts_with(supportPrefix))++supportCount;
|
||||
}
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
double pathLength=0.0,maxGradeDegrees=0.0;
|
||||
std::vector<std::array<std::int64_t,3>> centerline;
|
||||
if(deck.size()==1&&!deck.front()->sweepPathMicrometres.empty())
|
||||
centerline=deck.front()->sweepPathMicrometres;
|
||||
else
|
||||
{
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(i==0)centerline.push_back(segment->sweepEndpointsMicrometres[0]);
|
||||
centerline.push_back(segment->sweepEndpointsMicrometres[1]);
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<centerline.size();++i)
|
||||
{
|
||||
const auto& a=centerline[i-1];const auto& b=centerline[i];
|
||||
const double dx=(b[0]-a[0])/1.0e6;
|
||||
const double dy=(b[1]-a[1])/1.0e6;
|
||||
const double dz=std::abs(b[2]-a[2])/1.0e6;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLength+=horizontal;
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const double width=deck.empty()?0.0:
|
||||
2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
std::printf("[LiminalRoadResolved] vocabulary=%u id=liminal/road-clouds-v2 "
|
||||
"strate=%d shape=continuous_swept_deck deck_stamps=%zu centerline_points=%zu supports=%zu "
|
||||
"path_length_m=%.3f width_m=%.3f grade_max_deg=%.3f "
|
||||
"placement=free route_keepout=PASS band_clearance=PASS status=%s\n",
|
||||
resolvedLayout.stampVocabularyVersion,
|
||||
deck.empty()?-1:deck.front()->strateIndex,deck.size(),centerline.size(),
|
||||
supportCount,pathLength,width,maxGradeDegrees,
|
||||
deck.size()==1&¢erline.size()>=2&&pathLength>=300.0&&width>=3.0
|
||||
&&maxGradeDegrees<=15.0?"PASS":"FAIL");
|
||||
if(deck.size()!=1||centerline.size()<2||pathLength<80.0
|
||||
||width<3.0||maxGradeDegrees>15.0)
|
||||
{std::fprintf(stderr,"Liminal road resolved outside the walkable road bounds\n");return 2;}
|
||||
}
|
||||
if(hasLiminalPillarGrid)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
if(!StandaloneWorldAssemblyPrototype::BuildLiminalPillarGridRequests(ownerJsonRoot,
|
||||
*ownerWorld,worldAssembly,resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutStampsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Liminal pillar-grid resolution failed: %s\n",error.c_str());return 2;}
|
||||
std::printf("[LiminalPillarGridResolved] vocabulary=%u pillars=%zu shape=identical_vertical_cylinders "
|
||||
"placement=regular_grid route_keepout=PASS band_clearance=PASS status=PASS\n",
|
||||
resolvedLayout.stampVocabularyVersion,resolvedLayout.stamps.size());
|
||||
}
|
||||
if(options.LandmarkChamberDemo)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1> requests;
|
||||
@@ -2404,5 +2486,5 @@ int main(int argc,char** argv)
|
||||
options.MovingCameraSpeedMetersPerSecond,options.MovingCameraDurationSeconds,
|
||||
options.MovingCameraDwellSeconds,options.ColdWorkerTimeline,
|
||||
options.AuditPassageSourceClamp,options.AuditPassageCarveReach,
|
||||
options.AuditPassageTileSeams);
|
||||
options.AuditPassageTileSeams,options.AuditLiminalRoadWalk);
|
||||
}
|
||||
@@ -2384,7 +2384,8 @@ namespace StandaloneStreamBenchmark
|
||||
bool bMovingCameraTrace=false,double MovingCameraSpeedMetersPerSecond=0.0,
|
||||
double MovingCameraDurationSeconds=12.0,double MovingCameraDwellSeconds=5.0,
|
||||
bool bColdWorkerTimeline=false,bool bAuditPassageSourceClamp=false,
|
||||
bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false)
|
||||
bool bAuditPassageCarveReach=false,bool bAuditPassageTileSeams=false,
|
||||
bool bAuditLiminalRoadWalk=false)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const std::clock_t ColdCpuOrigin=std::clock();
|
||||
@@ -2554,7 +2555,7 @@ namespace StandaloneStreamBenchmark
|
||||
||bAuditOreVeinYield||bAuditLandmarkReachability||bAuditStrateTransitions
|
||||
||bAuditResolvedLayoutPassages||bAuditResolvedLayoutBoundaries
|
||||
||bAuditTileShapeReadiness||bAuditBasePageEquivalence||bAuditRouteParity
|
||||
||bAuditPassageSourceClamp||bAuditPassageCarveReach;
|
||||
||bAuditPassageSourceClamp||bAuditPassageCarveReach||bAuditLiminalRoadWalk;
|
||||
const bool bDeferFarWorld=bIncludeFarWorld&&!bMovingCameraTrace&&!bFarWorldAudit;
|
||||
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
||||
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
||||
@@ -2820,10 +2821,10 @@ namespace StandaloneStreamBenchmark
|
||||
Streamer.Stop();
|
||||
return passed?0:11;
|
||||
}
|
||||
if(bAuditResolvedLayoutPassages)
|
||||
if(bAuditResolvedLayoutPassages||bAuditLiminalRoadWalk)
|
||||
{
|
||||
if(ResolvedLayout==nullptr||!CoreSnapshot.resolvedLayout())
|
||||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedPassageAudit] missing resolved-layout snapshot\n");return 2;}
|
||||
{Streamer.Stop();std::fprintf(stderr,"[ResolvedWalkAudit] missing resolved-layout snapshot\n");return 2;}
|
||||
struct FWalkAudit
|
||||
{
|
||||
std::uint64_t segments=0,centers=0,capsuleSamples=0,blocked=0;
|
||||
@@ -2961,6 +2962,162 @@ namespace StandaloneStreamBenchmark
|
||||
&&audit.gradeFailures==0;
|
||||
return audit;
|
||||
};
|
||||
const auto auditLiminalRoadWalk=[&](bool required)
|
||||
{
|
||||
bool found=false;
|
||||
bool passed=true;
|
||||
std::vector<const VoxelForgeCore::ResolvedLayoutStampV1*> deck;
|
||||
constexpr std::string_view DeckPrefix="liminal/road-clouds-v2/deck/";
|
||||
for(const auto& road:ResolvedLayout->stamps)
|
||||
{
|
||||
if(road.stableId.starts_with(DeckPrefix))deck.push_back(&road);
|
||||
}
|
||||
if(!deck.empty())
|
||||
{
|
||||
found=true;
|
||||
std::sort(deck.begin(),deck.end(),[](const auto* a,const auto* b)
|
||||
{return a->stableId<b->stableId;});
|
||||
constexpr double CapsuleHeightMetres=1.76;
|
||||
constexpr double HalfCapsuleHeightMetres=CapsuleHeightMetres*0.5;
|
||||
constexpr double SlopeCapsuleClearanceMetres=0.12;
|
||||
std::vector<std::array<double,3>> roadPath;
|
||||
std::vector<std::array<double,3>> centerline;
|
||||
const bool continuousDeck=deck.size()==1
|
||||
&&deck.front()->kind
|
||||
==VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment
|
||||
&&deck.front()->sweepPathMicrometres.size()>=2;
|
||||
bool surfacePathValid=continuousDeck;
|
||||
if(continuousDeck)
|
||||
{
|
||||
centerline.reserve(deck.front()->sweepPathMicrometres.size());
|
||||
for(const auto& point:deck.front()->sweepPathMicrometres)
|
||||
centerline.push_back({point[0]/1.0e6,point[1]/1.0e6,point[2]/1.0e6});
|
||||
surfacePathValid=surfacePathValid
|
||||
&¢erline.front()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[0][0]/1.0e6
|
||||
&¢erline.back()[0]
|
||||
==deck.front()->sweepEndpointsMicrometres[1][0]/1.0e6;
|
||||
}
|
||||
else
|
||||
{
|
||||
centerline.reserve(deck.size()+1);
|
||||
surfacePathValid=true;
|
||||
for(std::size_t i=0;i<deck.size();++i)
|
||||
{
|
||||
const auto* segment=deck[i];
|
||||
if(segment->kind
|
||||
!=VoxelForgeCore::ResolvedSdfStampKindV1::fillSweptDeckSegment)
|
||||
surfacePathValid=false;
|
||||
const auto point=[&](std::size_t endpoint)
|
||||
{return std::array<double,3>{
|
||||
segment->sweepEndpointsMicrometres[endpoint][0]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][1]/1.0e6,
|
||||
segment->sweepEndpointsMicrometres[endpoint][2]/1.0e6};};
|
||||
const auto a=point(0),b=point(1);
|
||||
if(i==0)centerline.push_back(a);
|
||||
else if(std::abs(centerline.back()[0]-a[0])>1.0e-6
|
||||
||std::abs(centerline.back()[1]-a[1])>1.0e-6
|
||||
||std::abs(centerline.back()[2]-a[2])>1.0e-6)
|
||||
surfacePathValid=false;
|
||||
centerline.push_back(b);
|
||||
}
|
||||
}
|
||||
roadPath.reserve(centerline.size()*4);
|
||||
double pathLengthMetres=0.0,maxGradeDegrees=0.0;
|
||||
std::uint64_t surfaceProbes=0,surfaceMisses=0;
|
||||
const auto surfaceTopAt=[&](double xMetres,double yMetres,
|
||||
double nominalTop,double& surfaceTop)
|
||||
{
|
||||
++surfaceProbes;
|
||||
double low=nominalTop-0.25,high=nominalTop+1.0;
|
||||
const float lowDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,low/coreMetres});
|
||||
const float highDensity=Shape.densityAt({xMetres/coreMetres,
|
||||
yMetres/coreMetres,high/coreMetres});
|
||||
if(lowDensity>=0.0f||highDensity<0.0f)
|
||||
{
|
||||
++surfaceMisses;
|
||||
std::printf("[LiminalRoadSurfaceProbeMiss] x_m=%.6f y_m=%.6f "
|
||||
"nominal_top_m=%.6f low_density=%.6f high_density=%.6f\n",
|
||||
xMetres,yMetres,nominalTop,lowDensity,highDensity);
|
||||
return false;
|
||||
}
|
||||
for(int iteration=0;iteration<20;++iteration)
|
||||
{
|
||||
const double middle=(low+high)*0.5;
|
||||
if(Shape.densityAt({xMetres/coreMetres,yMetres/coreMetres,
|
||||
middle/coreMetres})<0.0f)low=middle;
|
||||
else high=middle;
|
||||
}
|
||||
surfaceTop=high;
|
||||
return true;
|
||||
};
|
||||
const double halfThickness=deck.front()->halfExtentsMicrometres[2]/1.0e6;
|
||||
for(std::size_t segment=1;segment<centerline.size();++segment)
|
||||
{
|
||||
const auto& a=centerline[segment-1];const auto& b=centerline[segment];
|
||||
const double dx=b[0]-a[0],dy=b[1]-a[1];
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
pathLengthMetres+=horizontal;
|
||||
const std::uint64_t intervals=std::max<std::uint64_t>(1,
|
||||
static_cast<std::uint64_t>(std::ceil(horizontal/0.5)));
|
||||
for(std::uint64_t step=0;step<=intervals;++step)
|
||||
{
|
||||
if(segment>1&&step==0)continue;
|
||||
const double t=static_cast<double>(step)/intervals;
|
||||
const double x=a[0]+dx*t,y=a[1]+dy*t;
|
||||
const double nominalTop=a[2]+(b[2]-a[2])*t+halfThickness;
|
||||
double actualTop=nominalTop;
|
||||
if(!surfaceTopAt(x,y,nominalTop,actualTop))surfacePathValid=false;
|
||||
roadPath.push_back({x/coreMetres,y/coreMetres,
|
||||
(actualTop+HalfCapsuleHeightMetres
|
||||
+SlopeCapsuleClearanceMetres)/coreMetres});
|
||||
}
|
||||
}
|
||||
for(std::size_t i=1;i<roadPath.size();++i)
|
||||
{
|
||||
const double dx=(roadPath[i][0]-roadPath[i-1][0])*coreMetres;
|
||||
const double dy=(roadPath[i][1]-roadPath[i-1][1])*coreMetres;
|
||||
const double dz=std::abs(roadPath[i][2]-roadPath[i-1][2])*coreMetres;
|
||||
const double horizontal=std::hypot(dx,dy);
|
||||
maxGradeDegrees=std::max(maxGradeDegrees,
|
||||
std::atan2(dz,horizontal)*57.29577951308232);
|
||||
}
|
||||
const FWalkAudit roadAudit=auditPlayerPath(Shape,roadPath,
|
||||
0.125,0.125,1.125,3,0.45);
|
||||
const double widthMetres=2.0*deck.front()->halfExtentsMicrometres[1]/1.0e6;
|
||||
passed=continuousDeck
|
||||
&&pathLengthMetres>=300.0&&widthMetres>=3.0
|
||||
&&maxGradeDegrees<=15.0&&surfacePathValid&&roadAudit.passed;
|
||||
std::printf("[LiminalRoadWalkAudit] id=liminal/road-clouds-v2 "
|
||||
"deck_stamps=%zu path_length_m=%.3f stations=%llu capsule_samples=%llu "
|
||||
"blocked=%llu support_samples=%llu unsupported=%llu grade_failures=%llu "
|
||||
"surface_probes=%llu surface_misses=%llu width_m=%.3f minimum_width_m=3.000 "
|
||||
"max_grade_deg=%.3f grade_limit_deg=15.000 status=%s\n",
|
||||
deck.size(),pathLengthMetres,
|
||||
static_cast<unsigned long long>(roadAudit.centers),
|
||||
static_cast<unsigned long long>(roadAudit.capsuleSamples),
|
||||
static_cast<unsigned long long>(roadAudit.blocked),
|
||||
static_cast<unsigned long long>(roadAudit.supportSamples),
|
||||
static_cast<unsigned long long>(roadAudit.unsupported),
|
||||
static_cast<unsigned long long>(roadAudit.gradeFailures),
|
||||
static_cast<unsigned long long>(surfaceProbes),
|
||||
static_cast<unsigned long long>(surfaceMisses),widthMetres,
|
||||
maxGradeDegrees,passed?"PASS":"FAIL");
|
||||
}
|
||||
if(!found&&required)
|
||||
{
|
||||
std::printf("[LiminalRoadWalkAudit] status=FAIL reason=road_path_missing\n");
|
||||
return false;
|
||||
}
|
||||
return passed;
|
||||
};
|
||||
if(bAuditLiminalRoadWalk&&!bAuditResolvedLayoutPassages)
|
||||
{
|
||||
const bool passed=auditLiminalRoadWalk(true);
|
||||
Streamer.Stop();
|
||||
return passed?0:14;
|
||||
}
|
||||
const auto qToMetres=[&](std::int64_t q)
|
||||
{return static_cast<double>(q)*World.CoreVoxelSizeMicrons
|
||||
/(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel*1000000.0);};
|
||||
@@ -4919,6 +5076,7 @@ namespace StandaloneStreamBenchmark
|
||||
routeAudit.firstUnsupported[i][1],routeAudit.firstUnsupported[i][2],
|
||||
routeAudit.firstUnsupported[i][3]);
|
||||
}
|
||||
const bool liminalRoadAuditPassed=auditLiminalRoadWalk(false);
|
||||
for(std::size_t passageIndex=0;
|
||||
passageIndex<ResolvedLayout->passages.size();++passageIndex)
|
||||
{
|
||||
@@ -4984,6 +5142,7 @@ namespace StandaloneStreamBenchmark
|
||||
&&shellAuditPassed
|
||||
&&routePasses>0&&routePasses==routeWalkPasses&&routePasses==routeBindings
|
||||
&&routeBindingFailures==0
|
||||
&&liminalRoadAuditPassed
|
||||
&&blockedCompiled&&blockedControls==passageCount;
|
||||
std::printf("[ResolvedPassageAuditSummary] interfaces=%llu passed=%llu natural=%llu/%llu strate_routes=%llu route_walkable=%llu route_points=%llu route_bindings=%llu route_binding_failures=%llu blocked_controls=%llu/%llu status=%s\n",
|
||||
static_cast<unsigned long long>(passageCount),static_cast<unsigned long long>(passagePasses),
|
||||
|
||||
@@ -587,6 +587,241 @@ namespace StandaloneWorldAssemblyPrototype
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalRoadRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_road_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_road_override must be an object";return false;}
|
||||
if(assembly.recipe.surfaceProfile.version!=1
|
||||
||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField
|
||||
||owner.CoreVoxelSizeMicrons==0)
|
||||
{error="the liminal road requires a valid flatField SurfaceWorld recipe";return false;}
|
||||
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const auto& position=overrideValue.Get("start_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double pathLength=0.0,curveControl=0.0,deckHeight=0.0,riseControl=0.0;
|
||||
double halfWidth=0.0,halfThickness=0.0,sampleSpacing=0.0;
|
||||
double supportSpacing=0.0,supportRadius=0.0;
|
||||
if(!strateValue.IsNumber()||!id||id->empty()||position.size()!=2
|
||||
||!readNumber("path_length_m",360.0,pathLength)
|
||||
||!readNumber("curve_control_offset_m",84.0,curveControl)
|
||||
||!readNumber("deck_height_above_field_m",44.0,deckHeight)
|
||||
||!readNumber("rise_control_m",22.0,riseControl)
|
||||
||!readNumber("half_width_m",3.2,halfWidth)
|
||||
||!readNumber("half_thickness_m",0.65,halfThickness)
|
||||
||!readNumber("sample_spacing_m",1.0,sampleSpacing)
|
||||
||!readNumber("support_spacing_m",24.0,supportSpacing)
|
||||
||!readNumber("support_radius_m",0.55,supportRadius))
|
||||
{error="liminal road requires a valid identity, start, and finite path parameters";return false;}
|
||||
for(const auto& value:position)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal road start_xy_m must contain finite metre values";return false;}
|
||||
if(std::abs(position[0].Number())>5000.0||std::abs(position[1].Number())>5000.0
|
||||
||pathLength<80.0||pathLength>1000.0||curveControl<0.0||curveControl>200.0
|
||||
||deckHeight<30.0||deckHeight>60.0||riseControl<0.0||riseControl>30.0
|
||||
||halfWidth<1.5||halfWidth>10.0||halfThickness<0.5||halfThickness>3.0
|
||||
||sampleSpacing<1.0||sampleSpacing>6.0
|
||||
||supportSpacing<8.0||supportSpacing>60.0
|
||||
||supportRadius<0.5||supportRadius>1.5)
|
||||
{error="liminal road dimensions are outside the bounded walkable-road ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_road_override must select a resolved SurfaceWorld strate";return false;}
|
||||
|
||||
const auto metresToMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t fieldFloorMicrometres=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t startX=metresToMicrometres(position[0].Number());
|
||||
const std::int64_t startY=metresToMicrometres(position[1].Number());
|
||||
const std::int64_t lengthUm=metresToMicrometres(pathLength);
|
||||
const std::int64_t lateralUm=metresToMicrometres(curveControl);
|
||||
const std::int64_t deckBaseUm=fieldFloorMicrometres+metresToMicrometres(deckHeight);
|
||||
const std::int64_t riseUm=metresToMicrometres(riseControl);
|
||||
const std::int64_t halfWidthUm=metresToMicrometres(halfWidth);
|
||||
const std::int64_t halfThicknessUm=metresToMicrometres(halfThickness);
|
||||
const std::uint32_t steps=static_cast<std::uint32_t>(std::ceil(pathLength/sampleSpacing));
|
||||
const auto lerpInteger=[steps](std::int64_t a,std::int64_t b,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t numerator=(b-a)*static_cast<std::int64_t>(i);
|
||||
const std::int64_t rounded=numerator>=0
|
||||
?numerator+static_cast<std::int64_t>(steps/2)
|
||||
:numerator-static_cast<std::int64_t>(steps/2);
|
||||
return a+rounded/static_cast<std::int64_t>(steps);
|
||||
};
|
||||
const auto bezier=[&](const std::array<std::int64_t,4>& controls,std::uint32_t i)
|
||||
{
|
||||
const std::int64_t a=lerpInteger(controls[0],controls[1],i);
|
||||
const std::int64_t b=lerpInteger(controls[1],controls[2],i);
|
||||
const std::int64_t c=lerpInteger(controls[2],controls[3],i);
|
||||
const std::int64_t d=lerpInteger(a,b,i);
|
||||
const std::int64_t e=lerpInteger(b,c,i);
|
||||
return lerpInteger(d,e,i);
|
||||
};
|
||||
const std::array<std::int64_t,4> xControls={startX,
|
||||
startX+lengthUm/3,startX+(2*lengthUm)/3,startX+lengthUm};
|
||||
const std::array<std::int64_t,4> yControls={startY,startY+lateralUm,
|
||||
startY-lateralUm,startY};
|
||||
const std::array<std::int64_t,4> zControls={deckBaseUm,deckBaseUm+riseUm,
|
||||
deckBaseUm-riseUm,deckBaseUm};
|
||||
std::vector<std::array<std::int64_t,3>> path;
|
||||
path.reserve(static_cast<std::size_t>(steps)+1);
|
||||
const auto halfUp=[](std::int64_t numerator,std::int64_t denominator)
|
||||
{return (numerator>=0?numerator+denominator/2:numerator-denominator/2)/denominator;};
|
||||
for(std::uint32_t i=0;i<=steps;++i)
|
||||
{
|
||||
const auto t=static_cast<std::int64_t>(i);
|
||||
const auto n=static_cast<std::int64_t>(steps);
|
||||
path.push_back({xControls[0]+halfUp(lengthUm*t,n),
|
||||
bezier(yControls,i),bezier(zControls,i)});
|
||||
}
|
||||
const auto makeStamp=[&](std::string stableId,ResolvedSdfStampKindV1 kind,
|
||||
const std::array<std::int64_t,3>& positionUm,
|
||||
const std::array<std::int64_t,3>& halfExtentsUm,std::int64_t smoothWidthUm)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=std::move(stableId);request.strateIndex=strateIndex;
|
||||
request.kind=kind;request.anchorPositionMicrometres=positionUm;
|
||||
request.halfExtentsMicrometres=halfExtentsUm;
|
||||
request.smoothWidthMicrometres=smoothWidthUm;
|
||||
requests.push_back(std::move(request));
|
||||
};
|
||||
const std::int64_t deckSmoothUm=250000;
|
||||
ResolvedLayoutStampRequestV1 deck;
|
||||
deck.stableId=*id+"/deck/000";
|
||||
deck.strateIndex=strateIndex;
|
||||
deck.kind=ResolvedSdfStampKindV1::fillSweptDeckSegment;
|
||||
deck.anchorPositionMicrometres=path[path.size()/2];
|
||||
deck.sweepEndpointsMicrometres={path.front(),path.back()};
|
||||
deck.sweepPathMicrometres=path;
|
||||
deck.halfExtentsMicrometres={500000,halfWidthUm,halfThicknessUm};
|
||||
deck.smoothWidthMicrometres=deckSmoothUm;
|
||||
requests.push_back(std::move(deck));
|
||||
const std::int64_t supportBaseUm=fieldFloorMicrometres-2000000;
|
||||
const std::int64_t supportRadiusUm=metresToMicrometres(supportRadius);
|
||||
double distanceSinceSupport=supportSpacing;
|
||||
std::uint32_t supportIndex=0;
|
||||
for(std::uint32_t i=0;i<path.size();++i)
|
||||
{
|
||||
if(i>0)
|
||||
{
|
||||
const auto& a=path[i-1];const auto& b=path[i];
|
||||
const double dx=static_cast<double>(b[0]-a[0]);
|
||||
const double dy=static_cast<double>(b[1]-a[1]);
|
||||
distanceSinceSupport+=std::hypot(dx,dy)/1000000.0;
|
||||
}
|
||||
if(i!=0&&i+1!=path.size()&&distanceSinceSupport<supportSpacing)continue;
|
||||
const std::int64_t supportTopUm=path[i][2]-halfThicknessUm+50000;
|
||||
const std::int64_t supportHalfHeightUm=(supportTopUm-supportBaseUm)/2;
|
||||
const std::int64_t supportCenterUm=(supportTopUm+supportBaseUm)/2;
|
||||
if(supportHalfHeightUm<500000)
|
||||
{error="liminal road support is too short to resolve safely";requests.clear();return false;}
|
||||
makeStamp(*id+"/support/"+std::to_string(10000+supportIndex).substr(1),
|
||||
ResolvedSdfStampKindV1::fillPillar,
|
||||
{path[i][0],path[i][1],supportCenterUm},
|
||||
{supportRadiusUm,supportRadiusUm,supportHalfHeightUm},100000);
|
||||
++supportIndex;distanceSinceSupport=0.0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool BuildLiminalPillarGridRequests(const VFJson::Value& root,
|
||||
const FStandaloneOwnerWorld& owner,
|
||||
const VoxelForgeCore::WorldAssemblyManifest& assembly,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutStampRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const VFJson::Value& overrideValue=root.Get("liminal_pillar_grid_override");
|
||||
if(!overrideValue.IsObject())
|
||||
{error="liminal_pillar_grid_override must be an object";return false;}
|
||||
if(assembly.recipe.surfaceProfile.version!=1
|
||||
||assembly.recipe.surfaceProfile.kind!=WorldAssemblySurfaceProfileKindV1::flatField
|
||||
||owner.CoreVoxelSizeMicrons==0)
|
||||
{error="the liminal pillar grid requires a valid flatField SurfaceWorld recipe";return false;}
|
||||
const auto* id=std::get_if<std::string>(&overrideValue.Get("stable_id").Data);
|
||||
const VFJson::Value& strateValue=overrideValue.Get("strate_index");
|
||||
const auto& center=overrideValue.Get("center_xy_m").AsArray();
|
||||
const auto readNumber=[&](const char* key,double fallback,double& result)
|
||||
{
|
||||
const VFJson::Value& value=overrideValue.Get(key);
|
||||
if(std::holds_alternative<std::nullptr_t>(value.Data))
|
||||
{result=fallback;return true;}
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))return false;
|
||||
result=value.Number();return true;
|
||||
};
|
||||
double spacing=0.0,radius=0.0,height=0.0;
|
||||
const VFJson::Value& rowsValue=overrideValue.Get("rows");
|
||||
const VFJson::Value& columnsValue=overrideValue.Get("columns");
|
||||
if(!id||id->empty()||!strateValue.IsNumber()||center.size()!=2
|
||||
||!readNumber("spacing_m",76.0,spacing)
|
||||
||!readNumber("radius_m",4.0,radius)
|
||||
||!readNumber("height_m",28.0,height)
|
||||
||!rowsValue.IsNumber()||!columnsValue.IsNumber())
|
||||
{error="liminal pillar grid requires an identity, center, dimensions and counts";return false;}
|
||||
for(const auto& value:center)
|
||||
if(!value.IsNumber()||!std::isfinite(value.Number()))
|
||||
{error="liminal pillar grid center_xy_m must contain finite metre values";return false;}
|
||||
const int rows=static_cast<int>(rowsValue.Integer());
|
||||
const int columns=static_cast<int>(columnsValue.Integer());
|
||||
if(rows<3||rows>15||columns<3||columns>15||spacing<32.0||spacing>120.0
|
||||
||radius<2.0||radius>12.0||height<12.0||height>60.0
|
||||
||std::abs(center[0].Number())>5000.0||std::abs(center[1].Number())>5000)
|
||||
{error="liminal pillar grid dimensions are outside their bounded ranges";return false;}
|
||||
const auto strateIndex=static_cast<std::int32_t>(strateValue.Integer());
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[strateIndex](const auto& value){return value.strateIndex==strateIndex
|
||||
&&value.generatorKind=="SurfaceWorld";});
|
||||
if(strate==layout.strates.end())
|
||||
{error="liminal_pillar_grid_override must select a resolved SurfaceWorld strate";return false;}
|
||||
const auto toMicrometres=[](double value)
|
||||
{return static_cast<std::int64_t>(std::llround(value*1000000.0));};
|
||||
const std::int64_t floorUm=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(assembly.recipe.surfaceProfile.h0Subvoxels)
|
||||
*owner.CoreVoxelSizeMicrons/WorldAssemblySubvoxelsPerVoxel));
|
||||
const std::int64_t spacingUm=toMicrometres(spacing);
|
||||
const std::int64_t radiusUm=toMicrometres(radius);
|
||||
const std::int64_t halfHeightUm=toMicrometres(height*0.5);
|
||||
const int rowStart=-(rows/2),columnStart=-(columns/2);
|
||||
for(int row=0;row<rows;++row)
|
||||
for(int column=0;column<columns;++column)
|
||||
{
|
||||
ResolvedLayoutStampRequestV1 request;
|
||||
request.stableId=*id+"/pillar/"+std::to_string(1000+row*columns+column).substr(1);
|
||||
request.strateIndex=strateIndex;
|
||||
request.kind=ResolvedSdfStampKindV1::fillPillar;
|
||||
request.anchorPositionMicrometres={
|
||||
toMicrometres(center[0].Number())+(columnStart+column)*spacingUm,
|
||||
toMicrometres(center[1].Number())+(rowStart+row)*spacingUm,
|
||||
floorUm+halfHeightUm};
|
||||
request.halfExtentsMicrometres={radiusUm,radiusUm,halfHeightUm};
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
|
||||
std::string& error,const std::string& manifestOutputPath,
|
||||
VoxelForgeCore::WorldAssemblyManifest* outManifest=nullptr)
|
||||
|
||||