feat(core): ore vein records and a materialAt query, off by default (L50); research R18, R19
Versioned capsule vein records (stable ID, resource ID, endpoints, radius, seeded irregularity) resolved into the snapshot only when requested, placed beside the route and away from passages, landings and landmarks (forced-passage control rejected). The bound field table gains materialAt (host + two resource IDs + Q16 weights) behind a per-tile BVH; mesh export v2 carries per-vertex materials; a measurement-only yield computes removed resource volume (0 on already-air space). With veins on, every density, mesh, payload and topology hash is unchanged; demo off: owner hashes identical to L47. fast/medium/full PASS. Research R18 (cavern-graph planner) and R19 (why FloatingIslands costs ~10x SurfaceWorld). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
b9ccff4ad6
commit
ca85e876ad
15 files changed
+1573
-19
No files matched your search
@@ -81,6 +81,9 @@ namespace VoxelForgeCore
|
||||
Vec3d normal;
|
||||
Vec2f uv;
|
||||
ColorRgba8 color;
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct MeshGroups
|
||||
@@ -163,6 +166,10 @@ namespace VoxelForgeCore
|
||||
double capRefinementExclusiveMilliseconds = 0.0;
|
||||
double meshAssemblyMilliseconds = 0.0;
|
||||
double meshingMilliseconds = 0.0;
|
||||
double materialQueryMilliseconds = 0.0;
|
||||
std::uint64_t materialQueries = 0;
|
||||
std::uint64_t materialCandidateChecks = 0;
|
||||
bool materialTileOutsideEveryVein = true;
|
||||
double featureListMilliseconds = 0.0;
|
||||
double farShellMilliseconds = 0.0;
|
||||
double farStratumShellMilliseconds = 0.0;
|
||||
@@ -216,7 +223,9 @@ namespace VoxelForgeCore
|
||||
std::uint64_t densitySampleCount = 0;
|
||||
std::uint64_t meshPayloadAllocatedBytes = 0;
|
||||
std::uint64_t densityHash = 0, meshHash = 0, payloadHash = 0, topologyHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false, hasTopologyHash = false;
|
||||
std::uint64_t materialHash = 0;
|
||||
bool hasDensityHash = false, hasMeshHash = false, hasPayloadHash = false,
|
||||
hasTopologyHash = false, hasMaterialHash = false;
|
||||
};
|
||||
|
||||
struct WorldConfiguration
|
||||
@@ -708,6 +717,7 @@ namespace VoxelForgeCore
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutStampVocabularyVersionV2 = 2;
|
||||
inline constexpr std::uint32_t ResolvedLayoutCavernGraphVocabularyVersionV1 = 1;
|
||||
inline constexpr std::uint32_t ResolvedLayoutOreVeinVocabularyVersionV1 = 1;
|
||||
enum class ResolvedLayoutValidationModeV3 : std::uint8_t { compatibility, strict };
|
||||
|
||||
// Stable, bounded geometry edits composed into every Core field after the shared
|
||||
@@ -787,6 +797,36 @@ namespace VoxelForgeCore
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
};
|
||||
|
||||
// Immutable material-only deposits. The capsule and its conservative bounds are in
|
||||
// micrometres; bounds include irregularity and the blend envelope.
|
||||
struct ResolvedLayoutOreVeinRequestV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> anchorEndpointsMicrometres{};
|
||||
std::int64_t placementRadiusMicrometres = 0;
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutOreVeinV1
|
||||
{
|
||||
std::string stableId;
|
||||
std::int32_t strateIndex = -1;
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::array<std::array<std::int64_t, 3>, 2> endpointsMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMinimumMicrometres{};
|
||||
std::array<std::int64_t, 3> boundsMaximumMicrometres{};
|
||||
std::int64_t radiusMicrometres = 0;
|
||||
std::int64_t irregularityAmplitudeMicrometres = 0;
|
||||
std::int64_t irregularityWavelengthMicrometres = 0;
|
||||
std::int64_t blendWidthMicrometres = 0;
|
||||
std::uint64_t irregularitySeed = 0;
|
||||
};
|
||||
|
||||
struct ResolvedLayoutAnchorV2
|
||||
{
|
||||
std::string kind;
|
||||
@@ -959,6 +999,9 @@ namespace VoxelForgeCore
|
||||
std::uint32_t cavernGraphVocabularyVersion = 0;
|
||||
std::vector<ResolvedCavernRoomV1> cavernRooms;
|
||||
std::vector<ResolvedCavernEdgeV1> cavernEdges;
|
||||
// Omitted from canonical identity when no records are present.
|
||||
std::uint32_t oreVeinVocabularyVersion = 0;
|
||||
std::vector<ResolvedLayoutOreVeinV1> oreVeins;
|
||||
std::string stableIdentity;
|
||||
|
||||
bool finalize(std::string& error);
|
||||
@@ -1016,6 +1059,40 @@ namespace VoxelForgeCore
|
||||
std::span<const ResolvedCavernRoomV1> rooms,
|
||||
std::span<const ResolvedCavernEdgeV1> edges,
|
||||
std::string& error);
|
||||
VFCORE_API bool ResolveResolvedLayoutOreVeinsV1(
|
||||
ResolvedLayoutSnapshotV3& layout, std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,
|
||||
std::string& error);
|
||||
|
||||
struct ResolvedLayoutOreVeinPlacementAuditV1
|
||||
{
|
||||
std::uint32_t negativeControlsRun = 0;
|
||||
std::uint32_t negativeControlsRejected = 0;
|
||||
bool passed = false;
|
||||
};
|
||||
VFCORE_API ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout);
|
||||
|
||||
struct MaterialSampleV1
|
||||
{
|
||||
std::uint16_t hostMaterialId = 1;
|
||||
std::array<std::uint16_t, 2> resourceMaterialIds{};
|
||||
std::array<std::uint16_t, 2> resourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct ResourceVolumeV1
|
||||
{
|
||||
std::uint16_t resourceMaterialId = 0;
|
||||
std::uint64_t weightedSamplesQ16 = 0;
|
||||
double volumeCubicMetres = 0.0;
|
||||
};
|
||||
|
||||
struct OreMiningYieldV1
|
||||
{
|
||||
std::uint64_t solidSamplesRemoved = 0;
|
||||
double sampleSpacingMetres = 0.0;
|
||||
std::vector<ResourceVolumeV1> resources;
|
||||
};
|
||||
|
||||
struct FloatingIslandBoundsAuditResult
|
||||
{
|
||||
@@ -1611,6 +1688,12 @@ namespace VoxelForgeCore
|
||||
ShapeBuildStats buildStats() const;
|
||||
|
||||
float densityAt(Vec3d voxelPosition) const;
|
||||
// Companion material query for the bound strate evaluator; coordinates use Core voxels.
|
||||
MaterialSampleV1 materialAt(Vec3d voxelPosition) const;
|
||||
// Measure-only carve estimate. This does not apply an edit or grant inventory.
|
||||
OreMiningYieldV1 measureOreMiningYieldForSphere(
|
||||
Vec3d centerVoxel, double radiusMetres,
|
||||
double sampleSpacingMetres = 0.25) const;
|
||||
// Audit-only view of density-field columns and short spans supporting passage voids.
|
||||
std::vector<PassageVoidCrossingAuditV1> passageVoidCrossings() const;
|
||||
bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex,
|
||||
|
||||
@@ -283,6 +283,187 @@ namespace
|
||||
}
|
||||
return true;
|
||||
}
|
||||
double LayoutSegmentDistance(const std::array<double,3>& p0,
|
||||
const std::array<double,3>& p1,const std::array<double,3>& q0,
|
||||
const std::array<double,3>& q1)
|
||||
{
|
||||
std::array<double,3> u{},v{},w{};
|
||||
double a=0.0,b=0.0,c=0.0,d=0.0,e=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
u[axis]=p1[axis]-p0[axis];v[axis]=q1[axis]-q0[axis];
|
||||
w[axis]=p0[axis]-q0[axis];
|
||||
a+=u[axis]*u[axis];b+=u[axis]*v[axis];
|
||||
c+=v[axis]*v[axis];d+=u[axis]*w[axis];e+=v[axis]*w[axis];
|
||||
}
|
||||
const double denominator=a*c-b*b;
|
||||
double sN=0.0,sD=denominator,tN=0.0,tD=denominator;
|
||||
if(denominator<1.0e-18){sN=0.0;sD=1.0;tN=e;tD=c;}
|
||||
else
|
||||
{
|
||||
sN=b*e-c*d;tN=a*e-b*d;
|
||||
if(sN<0.0){sN=0.0;tN=e;tD=c;}
|
||||
else if(sN>sD){sN=sD;tN=e+b;tD=c;}
|
||||
}
|
||||
if(tN<0.0)
|
||||
{
|
||||
tN=0.0;
|
||||
if(-d<0.0)sN=0.0;
|
||||
else if(-d>a)sN=sD;
|
||||
else{sN=-d;sD=a;}
|
||||
}
|
||||
else if(tN>tD)
|
||||
{
|
||||
tN=tD;
|
||||
if(-d+b<0.0)sN=0.0;
|
||||
else if(-d+b>a)sN=sD;
|
||||
else{sN=-d+b;sD=a;}
|
||||
}
|
||||
const double s=std::abs(sN)<1.0e-18?0.0:sN/sD;
|
||||
const double t=std::abs(tN)<1.0e-18?0.0:tN/tD;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double delta=w[axis]+s*u[axis]-t*v[axis];
|
||||
distanceSquared+=delta*delta;
|
||||
}
|
||||
return std::sqrt(std::max(0.0,distanceSquared));
|
||||
}
|
||||
std::array<double,3> LayoutOrePointUm(const std::array<std::int64_t,3>& point)
|
||||
{return {static_cast<double>(point[0]),static_cast<double>(point[1]),
|
||||
static_cast<double>(point[2])};}
|
||||
bool LayoutOreVeinSafe(const ResolvedLayoutSnapshotV3& layout,
|
||||
const ResolvedLayoutOreVeinV1& vein,std::string* reason=nullptr)
|
||||
{
|
||||
const auto fail=[&](const char* text)
|
||||
{if(reason)*reason=text;return false;};
|
||||
const auto strate=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==vein.strateIndex;});
|
||||
if(strate==layout.strates.end()||!strate->content.presentInCurrentField)
|
||||
return fail("vein strate has no current content band");
|
||||
if(vein.boundsMinimumMicrometres[2]<=strate->content.bottomMicrometres
|
||||
||vein.boundsMaximumMicrometres[2]>=strate->content.topMicrometres)
|
||||
return fail("vein bounds touch or cross the ceiling/bedrock content faces");
|
||||
for(const auto* band:{&strate->ceiling,&strate->bedrock})
|
||||
if(band->presentInCurrentField
|
||||
&&vein.boundsMinimumMicrometres[2]<band->topMicrometres
|
||||
&&vein.boundsMaximumMicrometres[2]>band->bottomMicrometres)
|
||||
return fail("vein bounds overlap a ceiling or bedrock band");
|
||||
const auto veinA=LayoutOrePointUm(vein.endpointsMicrometres[0]);
|
||||
const auto veinB=LayoutOrePointUm(vein.endpointsMicrometres[1]);
|
||||
const double coreRadius=static_cast<double>(vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres);
|
||||
const double coreMicrometres=std::max<std::uint32_t>(1,layout.coreVoxelSizeMicrometres);
|
||||
constexpr double RouteClearanceMicrometres=100000.0;
|
||||
const auto qToUm=[&](const std::array<std::int64_t,3>& q)
|
||||
{return std::array<double,3>{q[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
q[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
q[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
||||
for(const auto& passage:layout.passages)
|
||||
{
|
||||
if(passage.upperStrateIndex!=vein.strateIndex
|
||||
&&passage.lowerStrateIndex!=vein.strateIndex)continue;
|
||||
const auto upper=qToUm(passage.upperMouthSubvoxels);
|
||||
const auto lower=qToUm(passage.lowerMouthSubvoxels);
|
||||
const double mouthRadius=passage.mouthRadiusSubvoxels*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,upper,upper)
|
||||
<=coreRadius+mouthRadius+RouteClearanceMicrometres
|
||||
||LayoutSegmentDistance(veinA,veinB,lower,lower)
|
||||
<=coreRadius+mouthRadius+RouteClearanceMicrometres)
|
||||
return fail("vein overlaps a clear passage landing");
|
||||
for(std::size_t i=0;i+1<passage.controlPoints.size();++i)
|
||||
{
|
||||
const auto a=qToUm(passage.controlPoints[i].positionSubvoxels);
|
||||
const auto b=qToUm(passage.controlPoints[i+1].positionSubvoxels);
|
||||
const double radius=std::max(passage.controlPoints[i].radiusSubvoxels,
|
||||
passage.controlPoints[i+1].radiusSubvoxels)*coreMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
||||
<=coreRadius+radius+RouteClearanceMicrometres)
|
||||
return fail("vein enters a clear passage corridor");
|
||||
}
|
||||
}
|
||||
std::vector<const ResolvedLayoutAnchorV2*> route;
|
||||
for(const auto& anchor:strate->anchors)
|
||||
{
|
||||
if(anchor.values.size()<3)continue;
|
||||
if(anchor.kind=="route_point")route.push_back(&anchor);
|
||||
if(anchor.kind.find("landing")!=std::string::npos)
|
||||
{
|
||||
const std::array<double,3> landing={
|
||||
anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};
|
||||
const double radius=anchor.values.size()>=4
|
||||
?anchor.values[3]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel:3000000.0;
|
||||
if(LayoutSegmentDistance(veinA,veinB,landing,landing)
|
||||
<=coreRadius+radius+RouteClearanceMicrometres)
|
||||
return fail("vein overlaps a resolved landing footprint");
|
||||
}
|
||||
}
|
||||
for(std::size_t i=0;i+1<route.size();++i)
|
||||
{
|
||||
const auto qPoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{return std::array<double,3>{anchor.values[0]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[1]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel,
|
||||
anchor.values[2]*coreMicrometres/WorldAssemblySubvoxelsPerVoxel};};
|
||||
const auto a=qPoint(*route[i]),b=qPoint(*route[i+1]);
|
||||
const double routeRadius=std::max(route[i]->values.size()>=4?route[i]->values[3]:0,
|
||||
route[i+1]->values.size()>=4?route[i+1]->values[3]:0)
|
||||
*coreMicrometres/WorldAssemblySubvoxelsPerVoxel;
|
||||
if(LayoutSegmentDistance(veinA,veinB,a,b)
|
||||
<=coreRadius+routeRadius+RouteClearanceMicrometres)
|
||||
return fail("vein enters a clear strate route corridor");
|
||||
}
|
||||
for(const auto& room:layout.cavernRooms)
|
||||
if(room.strateIndex==vein.strateIndex)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(vein.boundsMinimumMicrometres[axis]
|
||||
<=room.boundsMaximumMicrometres[axis]
|
||||
&&vein.boundsMaximumMicrometres[axis]
|
||||
>=room.boundsMinimumMicrometres[axis])
|
||||
return fail("vein overlaps a cavern landmark footprint");
|
||||
for(const auto& edge:layout.cavernEdges)
|
||||
{
|
||||
if(edge.strateIndex!=vein.strateIndex)continue;
|
||||
std::array<double,3> previous{};
|
||||
const auto bezier=[&](double t)
|
||||
{
|
||||
const double one=1.0-t;
|
||||
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
||||
3.0*one*t*t,t*t*t};
|
||||
std::array<double,3> point{};
|
||||
for(std::size_t i=0;i<4;++i)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
point[axis]+=weights[i]*edge.controlPointsMicrometres[i][axis];
|
||||
return point;
|
||||
};
|
||||
previous=bezier(0.0);
|
||||
for(int step=1;step<=256;++step)
|
||||
{
|
||||
const auto next=bezier(static_cast<double>(step)/256.0);
|
||||
if(LayoutSegmentDistance(veinA,veinB,previous,next)
|
||||
<=coreRadius+edge.boreRadiusMicrometres+RouteClearanceMicrometres)
|
||||
return fail("vein enters a landmark passage corridor");
|
||||
previous=next;
|
||||
}
|
||||
}
|
||||
for(const auto& stamp:layout.stamps)
|
||||
{
|
||||
if(stamp.strateIndex!=vein.strateIndex
|
||||
||(stamp.kind!=ResolvedSdfStampKindV1::fillDais
|
||||
&&stamp.kind!=ResolvedSdfStampKindV1::fillPillar))continue;
|
||||
bool overlaps=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlaps=overlaps&&vein.boundsMinimumMicrometres[axis]
|
||||
<=stamp.boundsMaximumMicrometres[axis]
|
||||
&&vein.boundsMaximumMicrometres[axis]
|
||||
>=stamp.boundsMinimumMicrometres[axis];
|
||||
if(overlaps)return fail("vein overlaps a resolved landmark solid footprint");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
std::string LayoutHex(std::uint64_t value)
|
||||
{std::ostringstream out;out<<std::hex<<std::setfill('0')<<std::setw(16)<<value;return out.str();}
|
||||
std::string LayoutBandJson(const ResolvedLayoutBandV2& band)
|
||||
@@ -454,6 +635,32 @@ namespace
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
if(!s.oreVeins.empty())
|
||||
{
|
||||
out<<",\"ore_vein_vocabulary_version\":"<<s.oreVeinVocabularyVersion
|
||||
<<",\"ore_veins\":[";
|
||||
for(std::size_t i=0;i<s.oreVeins.size();++i)
|
||||
{
|
||||
if(i)out<<',';const auto& vein=s.oreVeins[i];
|
||||
out<<"{\"stable_id\":"<<LayoutEscape(vein.stableId)
|
||||
<<",\"strate_index\":"<<vein.strateIndex
|
||||
<<",\"resource_material_id\":"<<vein.resourceMaterialId
|
||||
<<",\"endpoints_um\":[["<<vein.endpointsMicrometres[0][0]<<','
|
||||
<<vein.endpointsMicrometres[0][1]<<','<<vein.endpointsMicrometres[0][2]
|
||||
<<"],["<<vein.endpointsMicrometres[1][0]<<','
|
||||
<<vein.endpointsMicrometres[1][1]<<','<<vein.endpointsMicrometres[1][2]
|
||||
<<"]],\"bounds_min_um\":["<<vein.boundsMinimumMicrometres[0]<<','
|
||||
<<vein.boundsMinimumMicrometres[1]<<','<<vein.boundsMinimumMicrometres[2]
|
||||
<<"],\"bounds_max_um\":["<<vein.boundsMaximumMicrometres[0]<<','
|
||||
<<vein.boundsMaximumMicrometres[1]<<','<<vein.boundsMaximumMicrometres[2]
|
||||
<<"],\"radius_um\":"<<vein.radiusMicrometres
|
||||
<<",\"irregularity_amplitude_um\":"<<vein.irregularityAmplitudeMicrometres
|
||||
<<",\"irregularity_wavelength_um\":"<<vein.irregularityWavelengthMicrometres
|
||||
<<",\"blend_width_um\":"<<vein.blendWidthMicrometres
|
||||
<<",\"irregularity_seed\":"<<vein.irregularitySeed<<'}';
|
||||
}
|
||||
out<<']';
|
||||
}
|
||||
out<<'}';return out.str();
|
||||
}
|
||||
std::int64_t LayoutInt(const VFJson::Value& obj,std::string_view key)
|
||||
@@ -620,6 +827,8 @@ bool ResolvedLayoutSnapshotV3::finalize(std::string& error)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::sort(cavernEdges.begin(),cavernEdges.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::sort(oreVeins.begin(),oreVeins.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
for(std::size_t i=0;i<strates.size();++i)
|
||||
{
|
||||
auto& x=strates[i];x.order=static_cast<std::int32_t>(i);
|
||||
@@ -1028,6 +1237,55 @@ bool ResolvedLayoutSnapshotV3::validate(ResolvedLayoutValidationModeV3 mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
if(oreVeins.empty())
|
||||
{
|
||||
if(oreVeinVocabularyVersion!=0)
|
||||
add("ore_vein_vocabulary_version",
|
||||
"ore-vein vocabulary version is set without records");
|
||||
}
|
||||
else if(oreVeinVocabularyVersion!=ResolvedLayoutOreVeinVocabularyVersionV1)
|
||||
add("ore_vein_vocabulary_version",
|
||||
"ore-vein records require vocabulary version 1");
|
||||
std::vector<std::string> oreVeinIds;
|
||||
for(const auto& vein:oreVeins)
|
||||
{
|
||||
if(vein.stableId.empty()
|
||||
||std::find(oreVeinIds.begin(),oreVeinIds.end(),vein.stableId)!=oreVeinIds.end())
|
||||
add("ore_vein_identity","ore vein has an empty or duplicate stable identity");
|
||||
else oreVeinIds.push_back(vein.stableId);
|
||||
if(vein.resourceMaterialId==0||vein.radiusMicrometres<250000
|
||||
||vein.radiusMicrometres>20000000
|
||||
||vein.irregularityAmplitudeMicrometres<0
|
||||
||vein.irregularityAmplitudeMicrometres>10000000
|
||||
||vein.irregularityWavelengthMicrometres<0
|
||||
||vein.irregularityWavelengthMicrometres>100000000
|
||||
||(vein.irregularityAmplitudeMicrometres>0
|
||||
&&vein.irregularityWavelengthMicrometres<=0)
|
||||
||vein.blendWidthMicrometres<50000
|
||||
||vein.blendWidthMicrometres>2000000
|
||||
||vein.irregularitySeed>0x001fffffffffffffull)
|
||||
add("ore_vein_parameters",vein.stableId+" has invalid or unbounded parameters");
|
||||
const std::int64_t pad=vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
if(vein.endpointsMicrometres[0][axis]<-1000000000000ll
|
||||
||vein.endpointsMicrometres[0][axis]>1000000000000ll
|
||||
||vein.endpointsMicrometres[1][axis]<-1000000000000ll
|
||||
||vein.endpointsMicrometres[1][axis]>1000000000000ll)
|
||||
add("ore_vein_bounds",vein.stableId+" endpoint is outside the bounded world envelope");
|
||||
if(vein.boundsMinimumMicrometres[axis]
|
||||
!=std::min(vein.endpointsMicrometres[0][axis],
|
||||
vein.endpointsMicrometres[1][axis])-pad
|
||||
||vein.boundsMaximumMicrometres[axis]
|
||||
!=std::max(vein.endpointsMicrometres[0][axis],
|
||||
vein.endpointsMicrometres[1][axis])+pad)
|
||||
add("ore_vein_bounds",vein.stableId+" conservative bounds do not match its parameters");
|
||||
}
|
||||
std::string safetyReason;
|
||||
if(!LayoutOreVeinSafe(*this,vein,&safetyReason))
|
||||
add("ore_vein_placement",vein.stableId+" is unsafe: "+safetyReason);
|
||||
}
|
||||
if(strict&&!issues.empty()){error="strict resolved-layout validation rejected "+issues.front().code;return false;}
|
||||
return true;
|
||||
}
|
||||
@@ -1046,6 +1304,8 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
s.stampVocabularyVersion=static_cast<std::uint32_t>(LayoutInt(root,"stamp_vocabulary_version"));
|
||||
s.cavernGraphVocabularyVersion=static_cast<std::uint32_t>(
|
||||
LayoutInt(root,"cavern_graph_vocabulary_version"));
|
||||
s.oreVeinVocabularyVersion=static_cast<std::uint32_t>(
|
||||
LayoutInt(root,"ore_vein_vocabulary_version"));
|
||||
for(const auto& v:root.Get("strates").AsArray())
|
||||
{
|
||||
ResolvedLayoutStratumV2 x;x.stableId=LayoutString(v,"stable_id");
|
||||
@@ -1166,6 +1426,33 @@ std::optional<ResolvedLayoutSnapshotV3> ResolvedLayoutSnapshotV3::FromJson(
|
||||
edge.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.cavernEdges.push_back(std::move(edge));
|
||||
}
|
||||
for(const auto& v:root.Get("ore_veins").AsArray())
|
||||
{
|
||||
ResolvedLayoutOreVeinV1 vein;vein.stableId=LayoutString(v,"stable_id");
|
||||
vein.strateIndex=static_cast<std::int32_t>(LayoutInt(v,"strate_index"));
|
||||
vein.resourceMaterialId=static_cast<std::uint16_t>(LayoutInt(v,"resource_material_id"));
|
||||
const auto& endpoints=v.Get("endpoints_um").AsArray();
|
||||
for(std::size_t endpoint=0;endpoint<2&&endpoint<endpoints.size();++endpoint)
|
||||
{
|
||||
const auto& values=endpoints[endpoint].AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
vein.endpointsMicrometres[endpoint][axis]=values[axis].Integer();
|
||||
}
|
||||
const auto readTriplet=[&](std::string_view key,std::array<std::int64_t,3>& target)
|
||||
{
|
||||
const auto& values=v.Get(key).AsArray();
|
||||
for(std::size_t axis=0;axis<3&&axis<values.size();++axis)
|
||||
target[axis]=values[axis].Integer();
|
||||
};
|
||||
readTriplet("bounds_min_um",vein.boundsMinimumMicrometres);
|
||||
readTriplet("bounds_max_um",vein.boundsMaximumMicrometres);
|
||||
vein.radiusMicrometres=LayoutInt(v,"radius_um");
|
||||
vein.irregularityAmplitudeMicrometres=LayoutInt(v,"irregularity_amplitude_um");
|
||||
vein.irregularityWavelengthMicrometres=LayoutInt(v,"irregularity_wavelength_um");
|
||||
vein.blendWidthMicrometres=LayoutInt(v,"blend_width_um");
|
||||
vein.irregularitySeed=static_cast<std::uint64_t>(LayoutInt(v,"irregularity_seed"));
|
||||
s.oreVeins.push_back(std::move(vein));
|
||||
}
|
||||
const std::string serializedIdentity=LayoutString(root,"stable_identity");
|
||||
if(!s.finalize(error))return std::nullopt;
|
||||
if(s.stableIdentity!=serializedIdentity){error="resolved layout stable identity does not match its canonical payload";return std::nullopt;}
|
||||
@@ -1352,6 +1639,148 @@ bool ResolveResolvedCavernGraphV1(ResolvedLayoutSnapshotV3& layout,std::int32_t
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
bool ResolveResolvedLayoutOreVeinsV1(ResolvedLayoutSnapshotV3& layout,std::int32_t seed,
|
||||
std::span<const ResolvedLayoutOreVeinRequestV1> requests,std::string& error)
|
||||
{
|
||||
error.clear();
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.oreVeins.clear();candidate.oreVeinVocabularyVersion=0;
|
||||
if(requests.empty())
|
||||
{
|
||||
if(!candidate.finalize(error))return false;
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
||||
std::vector<ResolvedLayoutOreVeinRequestV1> ordered(requests.begin(),requests.end());
|
||||
std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b)
|
||||
{return a.stableId<b.stableId;});
|
||||
std::vector<std::string> requestIds;
|
||||
candidate.oreVeins.reserve(ordered.size());
|
||||
for(const auto& request:ordered)
|
||||
{
|
||||
if(request.stableId.empty()
|
||||
||std::find(requestIds.begin(),requestIds.end(),request.stableId)!=requestIds.end())
|
||||
{error="ore-vein requests require non-empty unique stable IDs";return false;}
|
||||
requestIds.push_back(request.stableId);
|
||||
if(request.resourceMaterialId==0||request.placementRadiusMicrometres<0
|
||||
||request.placementRadiusMicrometres>10000000
|
||||
||request.radiusMicrometres<250000||request.radiusMicrometres>20000000
|
||||
||request.irregularityAmplitudeMicrometres<0
|
||||
||request.irregularityAmplitudeMicrometres>10000000
|
||||
||request.irregularityWavelengthMicrometres<0
|
||||
||request.irregularityWavelengthMicrometres>100000000
|
||||
||(request.irregularityAmplitudeMicrometres>0
|
||||
&&request.irregularityWavelengthMicrometres<=0)
|
||||
||request.blendWidthMicrometres<50000
|
||||
||request.blendWidthMicrometres>2000000)
|
||||
{error="ore-vein request "+request.stableId+" has invalid or unbounded parameters";return false;}
|
||||
const auto strate=std::find_if(candidate.strates.begin(),candidate.strates.end(),
|
||||
[&](const auto& value){return value.strateIndex==request.strateIndex;});
|
||||
if(strate==candidate.strates.end())
|
||||
{error="ore-vein request "+request.stableId+" names an unknown strate";return false;}
|
||||
|
||||
const std::uint64_t baseSeed=LayoutStampSeed(seed,request.strateIndex,
|
||||
std::string("ore-vein/")+request.stableId)&0x001fffffffffffffull;
|
||||
std::uint64_t state=baseSeed;
|
||||
const auto randomOffset=[&](std::uint64_t bits)
|
||||
{
|
||||
const std::int64_t radius=request.placementRadiusMicrometres;
|
||||
if(radius==0)return std::int64_t{0};
|
||||
const std::uint64_t span=static_cast<std::uint64_t>(radius)*2+1;
|
||||
return static_cast<std::int64_t>(bits%span)-radius;
|
||||
};
|
||||
bool placed=false;
|
||||
std::string lastUnsafeReason="all candidates were outside the bounded content volume";
|
||||
const std::uint32_t attempts=request.placementRadiusMicrometres==0?1u:512u;
|
||||
for(std::uint32_t attempt=0;attempt<attempts;++attempt)
|
||||
{
|
||||
std::array<std::int64_t,3> offset{};
|
||||
if(request.placementRadiusMicrometres>0)
|
||||
{
|
||||
offset={randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state)),
|
||||
randomOffset(LayoutStampNext(state))/3};
|
||||
const long double radius=request.placementRadiusMicrometres;
|
||||
const long double distanceSquared=static_cast<long double>(offset[0])*offset[0]
|
||||
+static_cast<long double>(offset[1])*offset[1]
|
||||
+static_cast<long double>(offset[2])*offset[2];
|
||||
if(distanceSquared>radius*radius)continue;
|
||||
}
|
||||
ResolvedLayoutOreVeinV1 vein;vein.stableId=request.stableId;
|
||||
vein.strateIndex=request.strateIndex;
|
||||
vein.resourceMaterialId=request.resourceMaterialId;
|
||||
vein.radiusMicrometres=request.radiusMicrometres;
|
||||
vein.irregularityAmplitudeMicrometres=request.irregularityAmplitudeMicrometres;
|
||||
vein.irregularityWavelengthMicrometres=request.irregularityWavelengthMicrometres;
|
||||
vein.blendWidthMicrometres=request.blendWidthMicrometres;
|
||||
vein.irregularitySeed=baseSeed;
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
vein.endpointsMicrometres[endpoint][axis]
|
||||
=request.anchorEndpointsMicrometres[endpoint][axis]+offset[axis];
|
||||
const std::int64_t pad=vein.radiusMicrometres
|
||||
+vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
vein.boundsMinimumMicrometres[axis]=std::min(
|
||||
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])-pad;
|
||||
vein.boundsMaximumMicrometres[axis]=std::max(
|
||||
vein.endpointsMicrometres[0][axis],vein.endpointsMicrometres[1][axis])+pad;
|
||||
}
|
||||
if(!LayoutOreVeinSafe(candidate,vein,&lastUnsafeReason))continue;
|
||||
candidate.oreVeins.push_back(std::move(vein));placed=true;break;
|
||||
}
|
||||
if(!placed)
|
||||
{error="ore-vein request "+request.stableId
|
||||
+" has no deterministic passage-safe content placement: "+lastUnsafeReason;return false;}
|
||||
}
|
||||
if(!candidate.finalize(error))return false;
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string strictError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,strictError))
|
||||
{error="strict ore-vein placement validation failed: "+strictError;return false;}
|
||||
layout=std::move(candidate);return true;
|
||||
}
|
||||
|
||||
ResolvedLayoutOreVeinPlacementAuditV1 AuditResolvedLayoutOreVeinPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
ResolvedLayoutOreVeinPlacementAuditV1 result;
|
||||
if(layout.passages.empty()||layout.passages.front().controlPoints.empty())return result;
|
||||
const auto& passage=layout.passages.front();
|
||||
const auto& control=passage.controlPoints.front().positionSubvoxels;
|
||||
ResolvedLayoutOreVeinV1 forced;forced.stableId="negative-control/forced-clear-passage";
|
||||
forced.strateIndex=passage.upperStrateIndex;forced.resourceMaterialId=1;
|
||||
forced.radiusMicrometres=500000;forced.blendWidthMicrometres=500000;
|
||||
forced.irregularitySeed=1;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const std::int64_t point=static_cast<std::int64_t>(std::llround(
|
||||
static_cast<double>(control[axis])*layout.coreVoxelSizeMicrometres
|
||||
/WorldAssemblySubvoxelsPerVoxel));
|
||||
forced.endpointsMicrometres[0][axis]=point;
|
||||
forced.endpointsMicrometres[1][axis]=point;
|
||||
forced.boundsMinimumMicrometres[axis]=point-1000000;
|
||||
forced.boundsMaximumMicrometres[axis]=point+1000000;
|
||||
}
|
||||
ResolvedLayoutSnapshotV3 candidate=layout;
|
||||
candidate.oreVeinVocabularyVersion=ResolvedLayoutOreVeinVocabularyVersionV1;
|
||||
candidate.oreVeins.push_back(forced);
|
||||
std::string finalizeError;
|
||||
++result.negativeControlsRun;
|
||||
if(candidate.finalize(finalizeError))
|
||||
{
|
||||
std::vector<ResolvedLayoutIssueV2> issues;std::string validationError;
|
||||
if(!candidate.validate(ResolvedLayoutValidationModeV3::strict,issues,validationError)
|
||||
&&std::any_of(issues.begin(),issues.end(),[](const auto& issue)
|
||||
{return issue.code=="ore_vein_placement"
|
||||
&&issue.detail.find("clear passage")!=std::string::npos;}))
|
||||
++result.negativeControlsRejected;
|
||||
}
|
||||
result.passed=result.negativeControlsRun>0
|
||||
&&result.negativeControlsRejected==result.negativeControlsRun;
|
||||
return result;
|
||||
}
|
||||
|
||||
ResolvedLayoutStampPlacementAuditV1 AuditResolvedLayoutStampPlacementV1(
|
||||
const ResolvedLayoutSnapshotV3& layout)
|
||||
{
|
||||
|
||||
@@ -369,6 +369,7 @@ public:
|
||||
}
|
||||
std::sort(BoundStratumFields.begin(),BoundStratumFields.end(),
|
||||
[](const auto& A,const auto& B){return A.BottomChunkZ<B.BottomChunkZ;});
|
||||
BuildOreVeinSpatialIndex();
|
||||
}
|
||||
|
||||
const FBoundStratumField* FindBoundStratumForChunk(int32 ChunkZ) const
|
||||
@@ -2972,6 +2973,27 @@ public:
|
||||
return AnalyticDensityAt(X,Y,Z);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> OreVeinTileCandidates(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{return BuildOreVeinTileList(minimum,maximum);}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 EvaluateMaterialAt(float X,float Y,float Z,
|
||||
std::span<const std::uint32_t> candidates,
|
||||
std::uint64_t* candidateChecks=nullptr) const
|
||||
{
|
||||
if(SharedDensitySource)
|
||||
return SharedDensitySource->MaterialAt(X,Y,Z,candidates,candidateChecks);
|
||||
return MaterialAt(X,Y,Z,candidates,candidateChecks);
|
||||
}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->MaterialAt(X,Y,Z);
|
||||
const auto candidates=BuildOreVeinPointList(X,Y,Z);
|
||||
return EvaluateMaterialAt(X,Y,Z,candidates);
|
||||
}
|
||||
|
||||
std::string DensityDispatchPathAt(float X,float Y,float Z) const
|
||||
{
|
||||
if(SharedDensitySource)return SharedDensitySource->DensityDispatchPathAt(X,Y,Z);
|
||||
@@ -3208,6 +3230,25 @@ private:
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint8_t quarterTurns=0;
|
||||
};
|
||||
struct FOreVeinSpatialBound
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
};
|
||||
struct FOreVeinSpatialNode
|
||||
{
|
||||
std::array<std::int64_t,3> minimumSubvoxels{},maximumSubvoxels{};
|
||||
std::size_t first=0,end=0;
|
||||
int left=-1,right=-1;
|
||||
};
|
||||
struct FOreVeinRuntime
|
||||
{
|
||||
const VoxelForgeCore::ResolvedLayoutOreVeinV1* record=nullptr;
|
||||
std::array<std::array<double,3>,2> endpointsVoxels{};
|
||||
double radiusVoxels=0.0,irregularityAmplitudeVoxels=0.0;
|
||||
double irregularityWavelengthVoxels=0.0,blendWidthVoxels=0.0;
|
||||
std::uint64_t irregularitySeed=0;
|
||||
std::uint16_t resourceMaterialId=0;
|
||||
};
|
||||
struct FCavernRuntimeFeature
|
||||
{
|
||||
const VoxelForgeCore::ResolvedCavernRoomV1* room=nullptr;
|
||||
@@ -3349,6 +3390,258 @@ private:
|
||||
if(!StampSpatialOrder.empty())BuildStampSpatialNode(0,StampSpatialOrder.size());
|
||||
}
|
||||
|
||||
int BuildOreVeinSpatialNode(std::size_t first,std::size_t end)
|
||||
{
|
||||
FOreVeinSpatialNode node;node.first=first;node.end=end;
|
||||
node.minimumSubvoxels.fill(std::numeric_limits<std::int64_t>::max());
|
||||
node.maximumSubvoxels.fill(std::numeric_limits<std::int64_t>::min());
|
||||
for(std::size_t offset=first;offset<end;++offset)
|
||||
{
|
||||
const auto& bound=OreVeinSpatialBounds[OreVeinSpatialOrder[offset]];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
node.minimumSubvoxels[axis]=std::min(node.minimumSubvoxels[axis],
|
||||
bound.minimumSubvoxels[axis]);
|
||||
node.maximumSubvoxels[axis]=std::max(node.maximumSubvoxels[axis],
|
||||
bound.maximumSubvoxels[axis]);
|
||||
}
|
||||
}
|
||||
const int nodeIndex=static_cast<int>(OreVeinSpatialNodes.size());
|
||||
OreVeinSpatialNodes.push_back(node);
|
||||
if(end-first>8)
|
||||
{
|
||||
std::size_t splitAxis=0;
|
||||
for(std::size_t axis=1;axis<3;++axis)
|
||||
if(static_cast<long double>(node.maximumSubvoxels[axis])
|
||||
-node.minimumSubvoxels[axis]
|
||||
>static_cast<long double>(node.maximumSubvoxels[splitAxis])
|
||||
-node.minimumSubvoxels[splitAxis])splitAxis=axis;
|
||||
const std::size_t middle=first+(end-first)/2;
|
||||
std::nth_element(OreVeinSpatialOrder.begin()+first,
|
||||
OreVeinSpatialOrder.begin()+middle,OreVeinSpatialOrder.begin()+end,
|
||||
[this,splitAxis](std::uint32_t a,std::uint32_t b)
|
||||
{
|
||||
const auto& left=OreVeinSpatialBounds[a];
|
||||
const auto& right=OreVeinSpatialBounds[b];
|
||||
const long double leftCenter=static_cast<long double>(
|
||||
left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis];
|
||||
const long double rightCenter=static_cast<long double>(
|
||||
right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis];
|
||||
return leftCenter==rightCenter?a<b:leftCenter<rightCenter;
|
||||
});
|
||||
OreVeinSpatialNodes[nodeIndex].left=BuildOreVeinSpatialNode(first,middle);
|
||||
OreVeinSpatialNodes[nodeIndex].right=BuildOreVeinSpatialNode(middle,end);
|
||||
}
|
||||
return nodeIndex;
|
||||
}
|
||||
|
||||
void QueryOreVeinSpatialNode(int nodeIndex,
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum,
|
||||
std::vector<std::uint32_t>& result) const
|
||||
{
|
||||
if(nodeIndex<0)return;
|
||||
const auto& node=OreVeinSpatialNodes[static_cast<std::size_t>(nodeIndex)];
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(node.maximumSubvoxels[axis]<minimum[axis]
|
||||
||node.minimumSubvoxels[axis]>maximum[axis])return;
|
||||
if(node.left<0)
|
||||
{
|
||||
for(std::size_t offset=node.first;offset<node.end;++offset)
|
||||
{
|
||||
const std::uint32_t index=OreVeinSpatialOrder[offset];
|
||||
const auto& bound=OreVeinSpatialBounds[index];bool overlap=true;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
overlap=overlap&&bound.minimumSubvoxels[axis]<=maximum[axis]
|
||||
&&bound.maximumSubvoxels[axis]>=minimum[axis];
|
||||
if(overlap)result.push_back(index);
|
||||
}
|
||||
return;
|
||||
}
|
||||
QueryOreVeinSpatialNode(node.left,minimum,maximum,result);
|
||||
QueryOreVeinSpatialNode(node.right,minimum,maximum,result);
|
||||
}
|
||||
|
||||
void BuildOreVeinSpatialIndex()
|
||||
{
|
||||
OreVeinSpatialBounds.clear();OreVeinSpatialOrder.clear();OreVeinSpatialNodes.clear();
|
||||
OreVeinRuntime.clear();
|
||||
if(!ResolvedLayout||ResolvedLayout->oreVeins.empty()||!Owner
|
||||
||Owner->CoreVoxelSizeMicrons==0)return;
|
||||
constexpr long double Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const long double scale=Q/Owner->CoreVoxelSizeMicrons;
|
||||
OreVeinSpatialBounds.reserve(ResolvedLayout->oreVeins.size());
|
||||
OreVeinRuntime.reserve(ResolvedLayout->oreVeins.size());
|
||||
for(const auto& vein:ResolvedLayout->oreVeins)
|
||||
{
|
||||
FOreVeinSpatialBound bound;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
bound.minimumSubvoxels[axis]=static_cast<std::int64_t>(std::floor(
|
||||
static_cast<long double>(vein.boundsMinimumMicrometres[axis])*scale));
|
||||
bound.maximumSubvoxels[axis]=static_cast<std::int64_t>(std::ceil(
|
||||
static_cast<long double>(vein.boundsMaximumMicrometres[axis])*scale));
|
||||
}
|
||||
OreVeinSpatialBounds.push_back(bound);
|
||||
FOreVeinRuntime runtime;runtime.record=&vein;
|
||||
for(std::size_t endpoint=0;endpoint<2;++endpoint)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
runtime.endpointsVoxels[endpoint][axis]=
|
||||
static_cast<double>(vein.endpointsMicrometres[endpoint][axis])
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.radiusVoxels=static_cast<double>(vein.radiusMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularityAmplitudeVoxels=
|
||||
static_cast<double>(vein.irregularityAmplitudeMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularityWavelengthVoxels=
|
||||
static_cast<double>(vein.irregularityWavelengthMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.blendWidthVoxels=static_cast<double>(vein.blendWidthMicrometres)
|
||||
/Owner->CoreVoxelSizeMicrons;
|
||||
runtime.irregularitySeed=vein.irregularitySeed;
|
||||
runtime.resourceMaterialId=vein.resourceMaterialId;
|
||||
OreVeinRuntime.push_back(runtime);
|
||||
}
|
||||
OreVeinSpatialOrder.resize(OreVeinSpatialBounds.size());
|
||||
for(std::size_t i=0;i<OreVeinSpatialOrder.size();++i)
|
||||
OreVeinSpatialOrder[i]=static_cast<std::uint32_t>(i);
|
||||
if(!OreVeinSpatialOrder.empty())BuildOreVeinSpatialNode(0,OreVeinSpatialOrder.size());
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildOreVeinTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
{
|
||||
std::vector<std::uint32_t> result;
|
||||
if(OreVeinSpatialBounds.empty())return result;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
if(minimum[axis]>maximum[axis])
|
||||
{
|
||||
result.resize(OreVeinSpatialBounds.size());
|
||||
for(std::size_t i=0;i<result.size();++i)
|
||||
result[i]=static_cast<std::uint32_t>(i);
|
||||
return result;
|
||||
}
|
||||
QueryOreVeinSpatialNode(0,minimum,maximum,result);
|
||||
std::sort(result.begin(),result.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildOreVeinPointList(float X,float Y,float Z) const
|
||||
{
|
||||
constexpr std::int64_t Q=VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
||||
const auto toQ=[](float value)
|
||||
{
|
||||
const long double scaled=static_cast<long double>(value)*Q;
|
||||
if(!std::isfinite(value)||scaled<std::numeric_limits<std::int64_t>::min()
|
||||
||scaled>std::numeric_limits<std::int64_t>::max())return std::int64_t{0};
|
||||
return static_cast<std::int64_t>(std::llround(scaled));
|
||||
};
|
||||
const std::array<std::int64_t,3> point={toQ(X),toQ(Y),toQ(Z)};
|
||||
return BuildOreVeinTileList(point,point);
|
||||
}
|
||||
|
||||
VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z,
|
||||
std::span<const std::uint32_t> candidates,
|
||||
std::uint64_t* candidateChecks=nullptr) const
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
MaterialSampleV1 result;
|
||||
if(!ResolvedLayout||OreVeinRuntime.empty()||candidates.empty()
|
||||
||!std::isfinite(X)||!std::isfinite(Y)||!std::isfinite(Z))return result;
|
||||
const FBoundStratumField* bound=FindBoundStratumForZ(Z);
|
||||
if(!bound)return result;
|
||||
const std::array<double,3> point={X,Y,Z};
|
||||
const auto noiseAt=[](std::uint64_t seed,std::int64_t x,std::int64_t y,std::int64_t z)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
for(const std::uint64_t word:{seed,static_cast<std::uint64_t>(x),
|
||||
static_cast<std::uint64_t>(y),static_cast<std::uint64_t>(z)})
|
||||
for(int byte=0;byte<8;++byte)
|
||||
{hash^=static_cast<std::uint8_t>(word>>(byte*8));hash*=1099511628211ull;}
|
||||
hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull;
|
||||
hash^=hash>>27;hash*=0x94d049bb133111ebull;hash^=hash>>31;
|
||||
return static_cast<double>(static_cast<std::uint32_t>(hash>>32))/2147483647.5-1.0;
|
||||
};
|
||||
const auto fade=[](double value){return value*value*(3.0-2.0*value);};
|
||||
for(const std::uint32_t candidate:candidates)
|
||||
{
|
||||
if(candidateChecks)++*candidateChecks;
|
||||
if(candidate>=OreVeinRuntime.size())continue;
|
||||
const auto& vein=OreVeinRuntime[candidate];
|
||||
if(!vein.record||vein.record->strateIndex!=bound->StrateIndex)continue;
|
||||
const double wavelength=vein.irregularityWavelengthVoxels;
|
||||
double irregularity=0.0;
|
||||
if(vein.irregularityAmplitudeVoxels>0.0&&wavelength>0.0)
|
||||
{
|
||||
std::array<std::int64_t,3> cell{};std::array<double,3> t{};
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double coordinate=point[axis]/wavelength;
|
||||
cell[axis]=static_cast<std::int64_t>(std::floor(coordinate));
|
||||
t[axis]=fade(coordinate-cell[axis]);
|
||||
}
|
||||
double layer[2][2]{};
|
||||
for(int z=0;z<2;++z)for(int y=0;y<2;++y)
|
||||
{
|
||||
const double a=noiseAt(vein.irregularitySeed,cell[0],cell[1]+y,cell[2]+z);
|
||||
const double b=noiseAt(vein.irregularitySeed,cell[0]+1,cell[1]+y,cell[2]+z);
|
||||
layer[z][y]=std::lerp(a,b,t[0]);
|
||||
}
|
||||
irregularity=std::lerp(std::lerp(layer[0][0],layer[0][1],t[1]),
|
||||
std::lerp(layer[1][0],layer[1][1],t[1]),t[2]);
|
||||
}
|
||||
const auto& a=vein.endpointsVoxels[0];const auto& b=vein.endpointsVoxels[1];
|
||||
double lengthSquared=0.0,dot=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{const double delta=b[axis]-a[axis];lengthSquared+=delta*delta;dot+=(point[axis]-a[axis])*delta;}
|
||||
const double t=lengthSquared>0.0?std::clamp(dot/lengthSquared,0.0,1.0):0.0;
|
||||
double distanceSquared=0.0;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{const double delta=point[axis]-(a[axis]+t*(b[axis]-a[axis]));distanceSquared+=delta*delta;}
|
||||
const double distance=std::sqrt(distanceSquared);
|
||||
const double radius=std::max(0.0,vein.radiusVoxels
|
||||
+irregularity*vein.irregularityAmplitudeVoxels);
|
||||
if(distance>=radius+vein.blendWidthVoxels)continue;
|
||||
double weight=1.0;
|
||||
if(distance>radius)
|
||||
{
|
||||
const double u=std::clamp((radius+vein.blendWidthVoxels-distance)
|
||||
/vein.blendWidthVoxels,0.0,1.0);
|
||||
weight=u*u*(3.0-2.0*u);
|
||||
}
|
||||
const auto weightQ16=static_cast<std::uint16_t>(std::clamp(
|
||||
std::llround(weight*65535.0),0ll,65535ll));
|
||||
if(weightQ16==0)continue;
|
||||
std::size_t slot=2;
|
||||
for(std::size_t index=0;index<2;++index)
|
||||
if(result.resourceMaterialIds[index]==vein.resourceMaterialId)
|
||||
{slot=index;break;}
|
||||
if(slot==2)
|
||||
for(std::size_t index=0;index<2;++index)
|
||||
if(result.resourceMaterialIds[index]==0){slot=index;break;}
|
||||
if(slot<2)
|
||||
{
|
||||
result.resourceMaterialIds[slot]=vein.resourceMaterialId;
|
||||
const std::uint32_t sum=static_cast<std::uint32_t>(
|
||||
result.resourceWeightsQ16[slot])+weightQ16;
|
||||
result.resourceWeightsQ16[slot]=static_cast<std::uint16_t>(
|
||||
std::min<std::uint32_t>(65535,sum));
|
||||
}
|
||||
}
|
||||
const std::uint32_t total=static_cast<std::uint32_t>(result.resourceWeightsQ16[0])
|
||||
+result.resourceWeightsQ16[1];
|
||||
if(total>65535)
|
||||
{
|
||||
result.resourceWeightsQ16[0]=static_cast<std::uint16_t>(
|
||||
static_cast<std::uint64_t>(result.resourceWeightsQ16[0])*65535/total);
|
||||
result.resourceWeightsQ16[1]=static_cast<std::uint16_t>(
|
||||
65535-result.resourceWeightsQ16[0]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> BuildStampTileList(
|
||||
const std::array<std::int64_t,3>& minimum,
|
||||
const std::array<std::int64_t,3>& maximum) const
|
||||
@@ -9412,6 +9705,10 @@ private:
|
||||
std::vector<FStampSpatialNode> StampSpatialNodes;
|
||||
std::vector<std::uint32_t> StampSpatialOrder;
|
||||
std::vector<FStampRuntime> StampRuntime;
|
||||
std::vector<FOreVeinSpatialBound> OreVeinSpatialBounds;
|
||||
std::vector<FOreVeinSpatialNode> OreVeinSpatialNodes;
|
||||
std::vector<std::uint32_t> OreVeinSpatialOrder;
|
||||
std::vector<FOreVeinRuntime> OreVeinRuntime;
|
||||
std::vector<FCavernRuntimeFeature> CavernRuntime;
|
||||
std::vector<VoxelForgeCore::WorldAssemblyFeatureBoundsV1> PassageFeatureBounds;
|
||||
std::vector<FPassageTileFeature> PassageTileFeatures;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
#include <new>
|
||||
@@ -216,6 +217,19 @@ namespace VoxelForgeCore
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashMaterialSamples(std::span<const MeshVertex> vertices)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
AddWord(hash,vertices.size(),8);
|
||||
for(const MeshVertex& vertex:vertices)
|
||||
{
|
||||
AddWord(hash,vertex.hostMaterialId,2);
|
||||
for(const std::uint16_t id:vertex.resourceMaterialIds)AddWord(hash,id,2);
|
||||
for(const std::uint16_t weight:vertex.resourceWeightsQ16)AddWord(hash,weight,2);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
std::uint64_t HashBrushTransaction(const EditTransaction& transaction)
|
||||
{
|
||||
std::uint64_t hash=14695981039346656037ull;
|
||||
@@ -1706,6 +1720,62 @@ namespace VoxelForgeCore
|
||||
|
||||
float World::densityAt(Vec3d position) const
|
||||
{ return impl_&&impl_->shapeReady.load(std::memory_order_acquire)&&impl_->shape?impl_->shape->DensityAt(static_cast<float>(position.x),static_cast<float>(position.y),static_cast<float>(position.z)):0.0f; }
|
||||
MaterialSampleV1 World::materialAt(Vec3d position) const
|
||||
{
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape)
|
||||
return {};
|
||||
return impl_->shape->MaterialAt(static_cast<float>(position.x),
|
||||
static_cast<float>(position.y),static_cast<float>(position.z));
|
||||
}
|
||||
OreMiningYieldV1 World::measureOreMiningYieldForSphere(Vec3d centerVoxel,
|
||||
double radiusMetres,double sampleSpacingMetres) const
|
||||
{
|
||||
OreMiningYieldV1 result;result.sampleSpacingMetres=sampleSpacingMetres;
|
||||
if(!impl_||!impl_->shapeReady.load(std::memory_order_acquire)||!impl_->shape
|
||||
||!std::isfinite(radiusMetres)||!std::isfinite(sampleSpacingMetres)
|
||||
||radiusMetres<=0.0||sampleSpacingMetres<0.05||sampleSpacingMetres>2.0)
|
||||
return result;
|
||||
const std::uint32_t voxelMicrometres=impl_->configuration.voxelSizeMicrons;
|
||||
if(voxelMicrometres==0)return result;
|
||||
const double voxelMetres=static_cast<double>(voxelMicrometres)/1000000.0;
|
||||
const double radiusVoxels=radiusMetres/voxelMetres;
|
||||
const double spacingVoxels=sampleSpacingMetres/voxelMetres;
|
||||
const int steps=static_cast<int>(std::ceil(radiusMetres/sampleSpacingMetres));
|
||||
const double radiusSquared=radiusMetres*radiusMetres;
|
||||
std::map<std::uint16_t,std::uint64_t> weightedSamples;
|
||||
for(int z=-steps;z<=steps;++z)
|
||||
for(int y=-steps;y<=steps;++y)
|
||||
for(int x=-steps;x<=steps;++x)
|
||||
{
|
||||
const double dx=x*sampleSpacingMetres;
|
||||
const double dy=y*sampleSpacingMetres;
|
||||
const double dz=z*sampleSpacingMetres;
|
||||
const double distanceSquared=dx*dx+dy*dy+dz*dz;
|
||||
if(distanceSquared>radiusSquared)continue;
|
||||
const Vec3d point={centerVoxel.x+x*spacingVoxels,
|
||||
centerVoxel.y+y*spacingVoxels,centerVoxel.z+z*spacingVoxels};
|
||||
const float before=impl_->shape->DensityAt(static_cast<float>(point.x),
|
||||
static_cast<float>(point.y),static_cast<float>(point.z));
|
||||
const double carveDensity=radiusVoxels
|
||||
-std::sqrt(distanceSquared)/voxelMetres;
|
||||
const double after=std::max(static_cast<double>(before),carveDensity);
|
||||
if(!(before<0.0f&&after>=0.0))continue;
|
||||
++result.solidSamplesRemoved;
|
||||
const MaterialSampleV1 material=materialAt(point);
|
||||
for(std::size_t slot=0;slot<2;++slot)
|
||||
if(material.resourceMaterialIds[slot]
|
||||
&&material.resourceWeightsQ16[slot])
|
||||
weightedSamples[material.resourceMaterialIds[slot]]
|
||||
+=material.resourceWeightsQ16[slot];
|
||||
}
|
||||
const double sampleVolume=sampleSpacingMetres*sampleSpacingMetres
|
||||
*sampleSpacingMetres;
|
||||
result.resources.reserve(weightedSamples.size());
|
||||
for(const auto& [id,weighted]:weightedSamples)
|
||||
result.resources.push_back({id,weighted,
|
||||
static_cast<double>(weighted)/65535.0*sampleVolume});
|
||||
return result;
|
||||
}
|
||||
std::vector<PassageVoidCrossingAuditV1> World::passageVoidCrossings() const
|
||||
{
|
||||
std::vector<PassageVoidCrossingAuditV1> result;
|
||||
@@ -2636,6 +2706,21 @@ namespace VoxelForgeCore
|
||||
out.skirtFaceMasks.assign(mesh.TriangleSkirtFaceMasks.begin(),
|
||||
mesh.TriangleSkirtFaceMasks.end());
|
||||
out.skirtFaceMasks.resize(mesh.Triangles.Num()/3,0);
|
||||
constexpr std::int64_t MaterialSubvoxelsPerVoxel=WorldAssemblySubvoxelsPerVoxel;
|
||||
const std::array<std::int64_t,3> materialTileMinimum={
|
||||
(static_cast<std::int64_t>(origin.X)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)-step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)-step)*MaterialSubvoxelsPerVoxel};
|
||||
const std::array<std::int64_t,3> materialTileMaximum={
|
||||
(static_cast<std::int64_t>(origin.X)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Y)+extent+step)*MaterialSubvoxelsPerVoxel,
|
||||
(static_cast<std::int64_t>(origin.Z)+extent+step)*MaterialSubvoxelsPerVoxel};
|
||||
const auto materialCandidates=tileShape.OreVeinTileCandidates(
|
||||
materialTileMinimum,materialTileMaximum);
|
||||
out.diagnostics.materialTileOutsideEveryVein=materialCandidates.empty();
|
||||
const double geometryVoxelSizeCm=static_cast<double>(world.CoreVoxelSizeMicrons)/10000.0;
|
||||
std::uint64_t materialQueries=0,materialCandidateChecks=0;
|
||||
const auto materialQueryStart=std::chrono::steady_clock::now();
|
||||
out.vertices.reserve(static_cast<std::size_t>(mesh.Vertices.Num()));
|
||||
for(int32 i=0;i<mesh.Vertices.Num();++i)
|
||||
{
|
||||
@@ -2643,9 +2728,25 @@ namespace VoxelForgeCore
|
||||
const FVector normal=i<mesh.Normals.Num()?mesh.Normals[i]:FVector::ZeroVector;
|
||||
const FVector2f uv=i<mesh.UVs.Num()?mesh.UVs[i]:FVector2f(0.0f,0.0f);
|
||||
const FColor color=i<mesh.Colors.Num()?mesh.Colors[i]:FColor(0,0,0,0);
|
||||
MaterialSampleV1 material;
|
||||
if(!materialCandidates.empty())
|
||||
{
|
||||
++materialQueries;
|
||||
material=tileShape.EvaluateMaterialAt(
|
||||
static_cast<float>(p.X/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Y/geometryVoxelSizeCm),
|
||||
static_cast<float>(p.Z/geometryVoxelSizeCm),materialCandidates,
|
||||
&materialCandidateChecks);
|
||||
}
|
||||
out.vertices.push_back({{p.X,p.Y,p.Z},{normal.X,normal.Y,normal.Z},{uv.X,uv.Y},
|
||||
{color.R,color.G,color.B,color.A}});
|
||||
{color.R,color.G,color.B,color.A},material.hostMaterialId,
|
||||
material.resourceMaterialIds,material.resourceWeightsQ16});
|
||||
}
|
||||
out.diagnostics.materialQueries=materialQueries;
|
||||
out.diagnostics.materialCandidateChecks=materialCandidateChecks;
|
||||
out.diagnostics.materialQueryMilliseconds=materialQueries
|
||||
?std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-materialQueryStart).count():0.0;
|
||||
out.indices.reserve(static_cast<std::size_t>(mesh.Triangles.Num()));
|
||||
for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast<std::uint32_t>(index));
|
||||
if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues);
|
||||
@@ -2657,6 +2758,7 @@ namespace VoxelForgeCore
|
||||
{ densityHash^=std::bit_cast<std::uint32_t>(value);densityHash*=1099511628211ull; }
|
||||
out.densityHash=densityHash;out.meshHash=HashViewerMesh(mesh);
|
||||
out.payloadHash=HashPayload(mesh);out.topologyHash=HashTopology(mesh);
|
||||
out.materialHash=HashMaterialSamples(out.vertices);
|
||||
if(key.lod>=4)
|
||||
{
|
||||
out.meshHash=SaltFarHorizonHash(out.meshHash,options.farHorizonVersion);
|
||||
@@ -2664,6 +2766,7 @@ namespace VoxelForgeCore
|
||||
out.topologyHash=SaltFarHorizonHash(out.topologyHash,options.farHorizonVersion);
|
||||
}
|
||||
out.hasDensityHash=out.hasMeshHash=out.hasPayloadHash=out.hasTopologyHash=true;
|
||||
out.hasMaterialHash=true;
|
||||
}
|
||||
out.diagnostics.meshAssemblyMilliseconds=std::chrono::duration<double,std::milli>(
|
||||
std::chrono::steady_clock::now()-MeshAssemblyStart).count();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# VoxelForge L50: ore veins in Core, slice 1
|
||||
|
||||
Scope: Core only on `assembler/world`. The owner demo is opt-in with `--ore-veins-demo`; no Unreal integration or mining gameplay is included.
|
||||
|
||||
## Representation and placement
|
||||
|
||||
Vocabulary v1 currently has one shape, `capsule`. A resolved record carries its stable ID, strate index, resource material ID, two endpoints, radius, irregularity amplitude and wavelength, blend width, seed, and conservative micrometre bounds. The `ore_veins` canonical field is emitted only when records exist. Stable IDs sort canonically; irregularity seeds derive from world seed, strate, and stable ID.
|
||||
|
||||
The seed-0 SW demo resolves three records in strate 1. Bounds include radius, irregularity, and blend:
|
||||
|
||||
| Stable ID | Resource | Endpoints (m) | Radius | Bounds (µm, half-open x × y × z) | Irregularity seed |
|
||||
|---|---:|---|---:|---|---:|
|
||||
| `ore/sw-arrival-route-copper-v1` | 1 copper | (16.510, 21.173, -502.909) → (18.277, 22.940, -502.950) | 0.65 m | `[15159830,19627354) × [19822674,24290198) × [-504300299,-501558872)` | 6689110868732322 |
|
||||
| `ore/sw-arrival-route-cobalt-v1` | 2 cobalt | (25.092, 21.192, -503.626) → (26.852, 22.952, -503.394) | 0.75 m | `[23641575,28301747) × [19741941,24402113) × [-505075523,-501944042)` | 2641701047127768 |
|
||||
| `ore/sw-chamber-approach-quartz-v1` | 3 quartz | (1.402, 27.019, -502.525) → (1.481, 29.459, -503.062) | 0.80 m | `[-98238,2981467) × [25518936,30959266) × [-504561729,-501024663)` | 882664590774173 |
|
||||
|
||||
Amplitude is 0.10 m; wavelengths are 1.4 m for copper/cobalt and 1.8 m for quartz; blend width is 0.60 m. A bounded, seeded candidate search biases capsules beside the route and toward side pockets/chamber approach. It rejects clear passage and mouth clearance, landing regions, and landmark/cavern footprints, using the resolved bounds. The forced-clear-passage control was rejected (1/1).
|
||||
|
||||
## Query, mesh, yield
|
||||
|
||||
The bound field table exposes deterministic `materialAt(x,y,z)`: host material, up to two resource IDs, and Q16 blend weights. A per-tile BVH narrows candidate veins; tiles with no vein bounds skip the query. Density and its sign are untouched. L44 shape-free and culling paths do not inspect veins: they depend only on density/geometry, and material lookup is a separate surface attribute query.
|
||||
|
||||
Mesh export v2 stores host ID plus two resource-ID/weight pairs per surface vertex, sampled at the vertex. The standalone renderer colors copper orange, cobalt blue, and quartz turquoise; v1 export remains available without ore.
|
||||
|
||||
Mining yield is measurement-only: a fixed 0.25 m grid evaluates the edit sphere before/after solid state and accumulates weighted removed resource volume by ID. The audited copper edit used a 1.25 m sphere and measured 0.000071 m³ (one removed solid sample, Q16 weight sum 296). This is a coarse-grid measurement, not a tuned reward amount. Repeating it on already-air space returned 0.000000 m³.
|
||||
|
||||
## Proofs and cost
|
||||
|
||||
With the demo off, owner SurfaceWorld W1/W12 hashes match L47: density `0x7dc75e2c09302258`, mesh `0x0cb34c40ceec4ce3`, payload `0x9d23e8c46b515cf1`, topology `0x2962c9464991eedd`.
|
||||
|
||||
At the 125-tile ore audit camera `(19.725, 19.725, -501.566)`, enabling veins leaves all geometry hashes unchanged:
|
||||
|
||||
| State | Density | Mesh | Payload | Topology | Material |
|
||||
|---|---|---|---|---|---|
|
||||
| Demo off | `0x5f9f1fdc0aabc8f0` | `0xed9e30865c98c2af` | `0xb8e3715300641fed` | `0x48584cc4e11c330c` | `0x58803ee275e9ba0a` |
|
||||
| Demo on | `0x5f9f1fdc0aabc8f0` | `0xed9e30865c98c2af` | `0xb8e3715300641fed` | `0x48584cc4e11c330c` | `0x5bfc71cbd5615b02` |
|
||||
|
||||
The random-camera and cavern sweeps ran with the demo enabled; material hash rows agreed across W1/W8/W12. Broad sweep positions outside all vein bounds recorded zero material queries. The ore audit changed six tile material hashes while preserving every density, mesh, payload, and topology tile hash.
|
||||
|
||||
At this audit, the on path queried 19,396 vertices and checked 24,871 candidates in 4.562 ms. 111/125 tiles had no vein candidate and recorded 0 ms query time; demo off queried zero vertices (also 0 ms). The off audit material hash above is the new empty-material result; the four owner geometry hashes remain the L47 values.
|
||||
|
||||
## Verification, captures, failures
|
||||
|
||||
- Fast: PASS, 211.2 s. Medium: PASS, 589.7 s. Full: PASS, 1,639.0 s; 1,084 checks, 0 failures. All tiers used source stamp `5f0f388366a4a6e7fb842459f035679e88da6b2a20af44e90e419c35f7c2fc06`.
|
||||
- Clean source stamp: `5f0f388366a4a6e7fb842459f035679e88da6b2a20af44e90e419c35f7c2fc06`; Clang 19 and GCC 14 builds passed.
|
||||
- Eye-height approach-wall capture: [`tunnel-quartz-eye.png`](captures/tunnel-quartz-eye.png). Wide arrival/approach capture: [`tunnel-approach-wide.png`](captures/tunnel-approach-wide.png). Mining-yield log: `Saved/Linux/l50/ore-tunnel-export.log`.
|
||||
- Failed runs: early seeded placements intersected the lower passage-mouth clearance and were rejected before moving the accepted candidate; the final demo passes the forced-clear-passage control. The first medium run caught a renderer shader-scope error, fixed before the clean rebuild and passing medium run. An initial off-angle surface capture was replaced after inspecting the exported material vertices.
|
||||
|
||||
## Orchestrator review (Claude)
|
||||
|
||||
Accepted, off by default. The stamp matches the final Clang and GCC builds (`5f0f3883…`). With veins on, all four geometry hashes are unchanged; only the material hash moves. Demo-off owner hashes are identical to L47. **Paused as a feature** after this slice: the owner made all-generators-at-about-1-s the priority (2026-10-24).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,49 @@
|
||||
# Research R18 — Core cavern graph planner
|
||||
|
||||
**Read-only report.** The R17 question is settled: authors provide motifs, size ranges and rules; Core places the rooms and routes from the seed, strate and stable IDs. This fits the composer’s generated-strate vision in [COMPOSER-NOTES.md](/home/jahni/VoxelM/Plugins/VoxelForge/COMPOSER-NOTES.md:18).
|
||||
|
||||
## Planner algorithm
|
||||
|
||||
1. **Freeze the planning inputs.** Use integer micrometres for the resolved strate content bounds, arrival and departure mouths, existing passages, stamps, and motif limits. Leave clearance inside the resolved ceiling and bedrock bands. Key each random choice by `(world seed, strate ID, motif stable ID, feature role/ordinal)` so one choice does not shift when another motif is added. Sort inputs and final records by stable ID.
|
||||
|
||||
2. **Build the guaranteed spine first.** Plan a grade-limited route between the actual resolved mouths. Use a bounded waypoint search through the usable volume, allowing contact with existing passages only at named mouths. Place at least one room on the route: current strict graph validation requires both a room and an edge. Split the route at room portals into edges, with matched floor heights and enough overlap for the player capsule.
|
||||
|
||||
3. **Place additional rooms.** Use bounded 3D Poisson-disk sampling for candidate centers, then draw motif and dimensions from their authored ranges with feature-keyed randomness. Reject candidates whose expanded bounds leave the content band, collide with a fill stamp or passage, overlap another room, or lack clearance. Treat carve stamps as usable space only where a deliberate connection is allowed. Bridson’s sampler is relevant for its minimum-spacing samples and spatial search grid; variable room sizes require a variable-radius spacing check. [Bridson, *Fast Poisson Disk Sampling in Arbitrary Dimensions*](https://www.cs.ubc.ca/~rbridson/docs/bridson-siggraph07-poissondisk.pdf)
|
||||
|
||||
4. **Attach branches and form loops.** Make 3D Delaunay neighbors candidate connections; reject edges that fail grade, clearance, protected-band or portal checks. Connect accepted off-spine rooms to the spine with a constrained, deterministic spanning tree. Add a bounded number of extra feasible links afterward to create loops; leaves naturally provide dead ends. Space colonisation can bias branch growth toward unfilled parts of the strate, but the spine and clearance checks remain authoritative. This combines the POI/Delaunay/A* ideas in the cave-generation literature with a required path and a spanning-tree connection policy; that combination is a VoxelForge design inference. [Santamaría-Ibirika et al., *Procedural Playable Cave Systems*](https://santosgrueiro.com/papers/2014/2014-Santam-Cave.pdf) · [Runions et al., *Modeling Trees with a Space Colonization Algorithm*](https://www.algorithmicbotany.org/papers/colonization.egwnp2007.html)
|
||||
|
||||
5. **Emit and check the records.** Convert accepted features into the existing room and edge records, derive conservative bounds, then validate graph endpoints, connectivity, grade, floor continuity, clearance and protected-band separation. Give each record a stable semantic ID; sort records and all equal-cost choices deterministically.
|
||||
|
||||
This order follows Spelunky’s documented practice of generating a safe exit path before adding randomized interest. Deep Rock Galactic offers a useful room-and-tunnel model: room templates have exits that connect through winding tunnels. Minecraft’s cheese, spaghetti and noodle caves suggest useful scale families for chambers and passages. [Spelunky 2 interview](https://blog.playstation.com/archive/2018/08/29/first-look-at-spelunky-2-gameplay-plus-new-derek-yu-interview/) · [Deep Rock Galactic cave-generation article](https://store.steampowered.com/news/posts/?appgroupname=Deep+Rock+Galactic&appids=548430&enddate=1730296516&feed=steam_community_announcements) · [Minecraft, *Caves & Cliffs: Part II*](https://www.minecraft.net/en-us/article/caves---cliffs--part-ii-out-today-java)
|
||||
|
||||
## Grade routing in 3D
|
||||
|
||||
A 15° walk grade allows at most `tan(15°) ≈ 0.268 m` of vertical change per metre of horizontal run. Check the maximum local grade along each smoothed edge, including its joins—not just endpoint heights or average grade. A 10 m elevation change therefore needs at least 37.3 m of horizontal run.
|
||||
|
||||
Place backbone-room floors along a grade-feasible route, and constrain their heights to that profile. When the direct route lacks enough run, use broad switchbacks whose legs progress monotonically in height. Keep legs far enough apart to avoid overlapping XY projections and ambiguous floor ownership; use separate smooth edges for separate legs.
|
||||
|
||||
Avoid tight helical coils in the first planner. R3 links L38’s 0.939 m floor-ripple period to the coil’s segment spacing and explains how a nearby turn can win floor ownership. Its proposed floor-near ownership rule improved a synthetic case, but did not prove a fix on the real route. [R3](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research3/REPORT.md:5>) · [R3 coil analysis](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research3/REPORT.md:9>)
|
||||
|
||||
If the mouths, bounds and grade limit admit no route, reject that candidate; keep the 15° limit fixed.
|
||||
|
||||
## Fit with Core and the artifact
|
||||
|
||||
The resolved snapshot already carries cavern rooms and edges, and includes them in canonical layout identity. The current `ResolveResolvedCavernGraphV1` copies supplied positions, derives irregularity seeds and bounds, then validates records; it does not choose room positions or routes. A separate Core planning entry point can produce those records before snapshot finalization. [Snapshot records](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h:945) · [Current resolver](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ResolvedLayoutInternal.h:1294) · [R17 finding](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research17/REPORT.md:13)
|
||||
|
||||
Run planning after passages and stamps are resolved, before `Layout.finalize` and JSON export. `isTileShapeFree` already rejects tiles whose padded bounds touch graph bounds. Keep those bounds conservative: include supports, blends, relief and junctions. The graph tile-list builder currently scans graph feature bounds per tile; a graph BVH would reduce candidate work as networks grow. Keep candidate order stable after the spatial query. [Adapter order](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1259) · [Shape-free proof](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1396) · [Graph tile list](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3431)
|
||||
|
||||
The floor/wall artifact remains unresolved. L47 found sawtooth teeth at the wall foot beyond its floor rails. L48’s rounded-cap trial still failed the whole-surface gate and increased flagged area. L48 describes L49’s next idea—build the bore and floor as one filleted SDF before stamps—but no L49 report or artifact is present in this checkout, so that remains unverified. [L47](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux47/REPORT.md:31) · [L48 results and L49 proposal](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux48/REPORT.md:13)
|
||||
|
||||
For planned rooms and edges, preserve native solid under the walking floor wherever possible; make the floor part of each shape’s profile, with narrow support fallback. Build the floor, bore and rounded wall-foot as one shape. Avoid a hard post-pass floor cap, tangent-only room/edge contact, and wall relief near the walk plane. R16 gives a practical geometric fillet target of at least `2h`, preferably `3h`: at the 25 cm base spacing, 0.5–0.75 m. These are sampling targets, not guarantees; coarse LODs need separate filtering or feature-aware handling. [R16 composition and sizing](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research16/REPORT.md:3) · [Current composition order](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1787)
|
||||
|
||||
## Verification plan and implementation slices
|
||||
|
||||
- **Graph:** Check endpoint IDs, connectedness, spine reachability, loop counts, room/edge clearance, and mouth joins. Treat links as undirected for reachability; current validation requires an edge’s end ID to name a same-strate room, while its start ID only has to be nonempty.
|
||||
- **Walkability:** Flood-fill the final composed field from arrival to departure using the project’s player-fit capsule (0.34 m radius × 1.76 m height), support and headroom checks. Graph connectivity alone does not prove a traversable route. [R12 verification guidance](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research12/REPORT.md:51) · [Capsule dimensions](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research1/REPORT.md:10)
|
||||
- **Geometry and determinism:** Sweep seeds, including tight bounds and maximum mouth-height differences. Check edge grades, ceiling/bedrock clearance, wall-foot defects and floor profiles. Extend the L48 audit from its current one-room/one-edge assumption to all graph surfaces; check LOD0 and transitions. Compare layout, density, mesh and topology hashes across worker counts and reordered inputs.
|
||||
- **Cost:** Measure planner time separately from tile evaluation. Run five paired graph-off/on views with matching tiles; report total and density time, graph BVH candidates, per-feature evaluations and memory. The current edge evaluator searches the curve per sample, so track cost as room and edge counts rise. No graph-scale cost result is established by this read-only review.
|
||||
- **Slices:** First, one mandatory spine with one room and no optional links. Second, Poisson room placement and connected branches. Third, loops, graph BVH and the generalized whole-surface audit. Run the seed sweep and paired cost measurements before enabling optional topology.
|
||||
|
||||
**Questions for the owner:** None blocking; the R17 authoring decision settles the main open question.
|
||||
|
||||
Read-only research only; no source edits, builds or tests were made.
|
||||
@@ -0,0 +1,77 @@
|
||||
# R19 — why FloatingIslands costs about 10×
|
||||
|
||||
## Log evidence
|
||||
|
||||
The closest same-view comparison is the unprofiled W12 pair in the L28 logs: [FloatingIslands](</home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/linux28-plain-baseline-floating-islands-l27-p1.log:30>) and [SurfaceWorld](</home/jahni/VoxelM/Plugins/VoxelForge/Saved/Linux/linux28-plain-baseline-surface-world-l27-p1.log:24>). Both generate **2,370 tiles**, including **960 full-detail tiles**.
|
||||
|
||||
| Measure | FloatingIslands | SurfaceWorld |
|
||||
|---|---:|---:|
|
||||
| Plan preparation | 222 ms | 41 ms |
|
||||
| Shape build | 0.891 s | 0.396 s |
|
||||
| First tile ready | 1.280 s | 0.471 s |
|
||||
| Near neighborhood ready | 1.556 s | 0.486 s |
|
||||
| Whole view ready | 18.971 s | 1.867 s |
|
||||
| Full-detail meshing worker sum | 138.139 s | 2.872 s |
|
||||
| Full-detail classification worker sum | 98 ms | 9 ms |
|
||||
|
||||
The shape builds have the same 138.2 million nodes and 18,420 primitives. FI’s extra shape time is mostly the surface-band stage (679 ms vs 209 ms); it does not explain the roughly 17 seconds of extra whole-view time. Later five-pair timings also retain about a 10× gap: 16.543 s FI vs 1.530 s SW ([L30 report](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux30/REPORT.md:40>)).
|
||||
|
||||
Per-LOD worker sums are aggregate worker time, **not wall time**:
|
||||
|
||||
| LOD | Tiles, both | FI worker sum | SW worker sum |
|
||||
|---:|---:|---:|---:|
|
||||
| 0 | 512 | 66.31 s | 1.98 s |
|
||||
| 1 | 448 | 72.76 s | 1.09 s |
|
||||
| 2 | 320 | 4.60 s | 1.26 s |
|
||||
| 3 | 208 | 3.81 s | 1.29 s |
|
||||
| 4 | 240 | 3.21 s | 2.69 s |
|
||||
| 5 | 144 | 1.14 s | 1.12 s |
|
||||
| 6 | 96 | 3.40 s | 2.78 s |
|
||||
| 7 | 96 | 8.98 s | 1.25 s |
|
||||
| 8 | 306 | 48.43 s | 3.28 s |
|
||||
|
||||
LOD0–1 accounts for most of the gap: its meshing-exclusive worker time is 138.13 s FI vs 2.87 s SW. LOD7–8 adds another large difference: 57.41 s vs 4.53 s total worker time. FI’s LOD8 log also reports 21.27 s of `column_eval_exclusive_ms`, vs 0.57 s for SW. That counter measures Heightfield2D column computation, **not** the FI island-column cache; the existing baseline logs do not split far-shell work finely enough to assign that cost further.
|
||||
|
||||
FI is mostly air by design, but the logs do not count whole-view empty meshes. They do show FI with **zero whole-tile skips** and **25,536 unknown boxes** in the timed run. A separate L9 proof audit reports **21,504 FI fallback/unknown blocks and zero checked blocks**; SW checked 26,977 blocks / 35.9 million samples with zero violations ([Linux28 proof summary](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux28/REPORT.md:79>), [Linux25 summary](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux25/REPORT.md:29>)). So the measured problem is clear: FI’s air is not being cheaply certified and skipped. The exact whole-view empty-tile count still needs `--mesh-coverage`.
|
||||
|
||||
## Cost model
|
||||
|
||||
- `FloatingIslandFieldV1::interval` returns unknown whenever a box intersects any island or connector influence AABB; it proves air only outside all those bounds ([`FloatingIslandsInternal.h:interval`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:2099>), [`ShapeWorldInternal.h:ClassifyBoxDetailed`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:4919>)). Broad bounds therefore leave many actual-air boxes ambiguous.
|
||||
- The owner plan has four islands. `buildFeatureTileList` disables candidate evaluation for plans of four or fewer, so the standard run does not use its per-tile group bounds to prune those island evaluations ([`FloatingIslandsInternal.h:buildFeatureTileList`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:1925>), [`WorldAssemblerInternal.h:find`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/WorldAssemblerInternal.h:33>)). On the current geometry-v2+ path, each island evaluates five root lobes; four islands mean up to 20 root SDF evaluations per field query, plus connector work ([`IslandDensity` / `FieldDensity`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/FloatingIslandsInternal.h:711>).
|
||||
- There is also avoidable repeated work: `DensityAtLatticeX8` calls `NativeFloatingIslandDensityAt` to detect an FI lane, then calls `DensityAtLattice` for each lane, which evaluates the native FI field again. Reusing the preflight result could remove a duplicate evaluation for lanes inside the FI field ([`DensityAtLatticeX8`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3100>), [mesher call site](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp:1289>)).
|
||||
- The native-only shortcut is SurfaceWorld-only and limited to LOD0. FI is not eligible, but the timed run reports zero shape-free desired tiles for both recipes, so this is not the main cause of this run’s gap ([`NativeOnlyBoundsForTile` / `isTileShapeFree`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1098>)).
|
||||
- Far work is a real second target: FI’s island plan prevents the native-surface-horizon shortcut from skipping far-shell repair at LOD7+, while ground/cap refinement also runs for LOD4+. The static run’s `--far-horizon-lod8-step=0` does not remove those paths ([`generateTile`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1960>)). Profile the shell and column sub-stages before attributing the LOD8 cost more narrowly.
|
||||
- Page proofs are not on the static benchmark path: it has no region crossing. Existing page-route audits pass, but they do not explain the static whole-view timing ([`compileTileRegion`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1487>), [Research11](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Research11/REPORT.md:9>)).
|
||||
|
||||
## Ranked work
|
||||
|
||||
1. **Tight conservative interval bounds, then coarse-to-fine proofs.** Certify empty air blocks outside actual island, helix, column, floor, ceiling and band support; refine only boxes whose bounds cross zero. This has the largest potential gain because it can avoid meshing air. Proof soundness is the main risk: preserve exact sampling for mixed boxes and validate proof audits, appearance and hashes.
|
||||
2. **Reuse the X8 FI preflight samples.** This is a direct source-level duplication and should be a lower-risk exact-output optimization. It can approach a 2× reduction in FI field work on affected lanes, but cannot by itself reach 1 s.
|
||||
3. **Use per-tile island-group pruning; add a BVH if plans grow.** First measure the existing group-bound path that the four-island setting currently disables. A new BVH is more useful as island/connector counts rise; for four islands, test the existing certificates first.
|
||||
4. **Profile and simplify LOD7–8 far-shell work.** It may save seconds, but cannot close the full 10× gap alone; changing LOD8 representation also risks visible horizon changes.
|
||||
5. **Measure before adding caches.** The shared XY column cache is already present. L7 showed a 19% SW whole-view improvement, while L29’s cache experiment did not improve cost; neither establishes a comparable FI gain ([L7 report](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Linux7/REPORT.md:40>)).
|
||||
6. **GPU later.** First remove redundant field work and skip proven air. GPU work adds dispatch, transfer and exact-parity costs without fixing loose bounds.
|
||||
|
||||
## Builder measurement plan
|
||||
|
||||
Run five fresh-process W12 timing pairs, alternating recipes, without hashing or profiling:
|
||||
|
||||
```sh
|
||||
taskset -c 0-11 env LP_NUM_THREADS=12 <headless-binary> \
|
||||
--world-assembler-prototype --stream-benchmark --stream-pipeline \
|
||||
--force-generate --clip-radius=3 --max-clip-level=8 \
|
||||
--far-horizon-lod8-step=0 --workers=12 --recipe=floating-islands \
|
||||
Standalone/data/owner_world.json
|
||||
```
|
||||
|
||||
Repeat with `--recipe=surface-world`. Then profile each recipe separately with `--profile-lod-work --mesh-coverage`; compare per-LOD density samples, MC grid/cell/gradient/interpolation work, shell calls, ground/cap columns, and non-empty mesh tiles. Profiling adds overhead, so use those runs for attribution, not headline timings. The headless parser has no generic `--profile` flag; the relevant switch is `--profile-lod-work` ([`StreamBenchmarkHeadless.cpp`](</home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamBenchmarkHeadless.cpp:869>)). Heightfield column counters do not expose FI’s `IslandColumnCache` hits; use the stack sample or add a focused diagnostic for island-cache hits/misses and root/connector evaluations if needed.
|
||||
|
||||
For proof coverage, rerun without `--force-generate` and add `--audit-uniform-block-proofs --mesh-coverage`; `--force-generate` disables the block audit. For travel, add `--camera-step=8,0,0` and then `--camera-step=24,0,0`, with `--profile-lod-work`. Compare entering-tile count, completion count, wall time, per-LOD work and queue backlog. Existing moving output is aggregate; smoothness also needs per-entering-tile queue wait and submit-to-complete p50/p95 latency.
|
||||
|
||||
For hotspot stacks, run the same unprofiled FI and SW commands under `perf record -F 99 -g --call-graph dwarf`, then inspect with `perf report --stdio --sort symbol`. Treat this as hotspot evidence, not a timing comparison; `perf-record(1)` documents the call-graph modes ([manual](https://man7.org/linux/man-pages/man1/perf-record.1.html)).
|
||||
|
||||
## What ≤1 s requires
|
||||
|
||||
FI needs roughly a **94% wall-time reduction** from 16–18 s; the 12-worker LOD totals in the reference log are about 213 worker-seconds, versus roughly 12 worker-seconds for an ideal one-second budget. SW also needs about a 40% reduction from the owner’s 1.7–1.8 s. A cold FI shape build alone took 0.89 s in the reference run, so strict sub-second whole-view generation also requires reducing or overlapping shape work.
|
||||
|
||||
For VR and fast travel, measure the near ring and newly entering tiles separately from completion of all 2,370 view tiles. Prioritize nearby work and let far LODs refine asynchronously; validate 8 m and 24 m movement, deterministic hashes across worker/compiler configurations, and tail tile latency. No edits, builds or binaries were run for this report.
|
||||
@@ -34,6 +34,7 @@ namespace
|
||||
float Height;
|
||||
float WorldPosition[3];
|
||||
float TileData[4];
|
||||
float Material[4]{};
|
||||
};
|
||||
|
||||
struct FScene
|
||||
@@ -121,11 +122,12 @@ namespace
|
||||
if(!File){Error="cannot open mesh scene";return false;}
|
||||
char Magic[8]{};
|
||||
File.read(Magic,sizeof(Magic));
|
||||
if(!File||std::memcmp(Magic,"VFMSH001",8)!=0)
|
||||
const bool HasMaterials=File&&std::memcmp(Magic,"VFMSH002",8)==0;
|
||||
if(!File||(!HasMaterials&&std::memcmp(Magic,"VFMSH001",8)!=0))
|
||||
{Error="mesh scene magic is invalid";return false;}
|
||||
std::uint32_t Version=0,ExpectedTiles=0;
|
||||
if(!Read(File,Version)||!Read(File,Scene.VoxelSizeMicrons)||!Read(File,ExpectedTiles)
|
||||
||Version!=1||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000)
|
||||
||Version!=(HasMaterials?2u:1u)||Scene.VoxelSizeMicrons==0||ExpectedTiles>1000000)
|
||||
{Error="mesh scene header is malformed";return false;}
|
||||
for(double& Value:Scene.CameraMeters)if(!Read(File,Value))
|
||||
{Error="mesh scene camera is truncated";return false;}
|
||||
@@ -161,6 +163,19 @@ namespace
|
||||
Scene.MinimumHeight=std::min(Scene.MinimumHeight,Scene.Vertices[I].Height);
|
||||
Scene.MaximumHeight=std::max(Scene.MaximumHeight,Scene.Vertices[I].Height);
|
||||
}
|
||||
if(HasMaterials)
|
||||
for(std::size_t I=Base;I<Base+VertexCount;++I)
|
||||
{
|
||||
std::uint16_t Host=0,Ids[2]{},Weights[2]{};
|
||||
if(!Read(File,Host)||!Read(File,Ids[0])||!Read(File,Ids[1])
|
||||
||!Read(File,Weights[0])||!Read(File,Weights[1]))
|
||||
{Error="tile material attributes are truncated";return false;}
|
||||
Scene.Vertices[I].Material[0]=static_cast<float>(Ids[0]);
|
||||
Scene.Vertices[I].Material[1]=static_cast<float>(Weights[0])/65535.0f;
|
||||
Scene.Vertices[I].Material[2]=static_cast<float>(Ids[1]);
|
||||
Scene.Vertices[I].Material[3]=static_cast<float>(Weights[1])/65535.0f;
|
||||
(void)Host;
|
||||
}
|
||||
const double TileVoxels=static_cast<double>(32u<<Key[3]);
|
||||
const float TileExtentMeters=static_cast<float>(TileVoxels
|
||||
*static_cast<double>(Scene.VoxelSizeMicrons)/1000000.0);
|
||||
@@ -540,10 +555,12 @@ layout(location=1) in vec3 inNormal;
|
||||
layout(location=2) in float inHeight;
|
||||
layout(location=3) in vec3 inWorldPosition;
|
||||
layout(location=4) in vec4 inTileData;
|
||||
layout(location=5) in vec4 inMaterial;
|
||||
out vec3 vNormal;
|
||||
out float vHeight;
|
||||
out vec3 vWorldPosition;
|
||||
out vec4 vTileData;
|
||||
out vec4 vMaterial;
|
||||
uniform float uTanHalfHorizontal;
|
||||
uniform float uTanHalfVertical;
|
||||
uniform float uNear;
|
||||
@@ -558,6 +575,7 @@ void main()
|
||||
vHeight=inHeight;
|
||||
vWorldPosition=inWorldPosition;
|
||||
vTileData=inTileData;
|
||||
vMaterial=inMaterial;
|
||||
})GLSL";
|
||||
static constexpr char FragmentSource[]=R"GLSL(#version 300 es
|
||||
precision highp float;
|
||||
@@ -565,6 +583,7 @@ in vec3 vNormal;
|
||||
in float vHeight;
|
||||
in vec3 vWorldPosition;
|
||||
in vec4 vTileData;
|
||||
in vec4 vMaterial;
|
||||
out vec4 outColor;
|
||||
uniform int uReadableMode;
|
||||
uniform int uTileEdges;
|
||||
@@ -582,6 +601,7 @@ void main()
|
||||
if(uClipEnabled!=0&&dot(vec4(vWorldPosition,1.0),uClipPlane)<0.0)discard;
|
||||
vec3 n=normalize(vNormal);
|
||||
vec3 color;
|
||||
float illumination=1.0;
|
||||
if(uReadableMode==0)
|
||||
{
|
||||
vec3 normalColor=abs(n)*0.78+vec3(0.12);
|
||||
@@ -597,10 +617,32 @@ void main()
|
||||
?mix(vec3(0.62,0.78,1.0),vec3(1.0,0.82,0.62),smoothHeightT)
|
||||
:vec3(1.0);
|
||||
float diffuse=max(dot(n,normalize(uLightDirection)),0.0);
|
||||
float illumination=uGrazingLight!=0?0.32+1.20*diffuse:
|
||||
illumination=uGrazingLight!=0?0.32+1.20*diffuse:
|
||||
uHeadlamp!=0?0.78+0.34*diffuse:0.62+0.38*diffuse;
|
||||
color=vec3(0.64,0.62,0.56)*heightTint*illumination;
|
||||
}
|
||||
if(uReadableMode!=0)
|
||||
{
|
||||
vec3 resourceColor=vec3(0.0);
|
||||
float resourceWeight=0.0;
|
||||
if(vMaterial.x>0.5)
|
||||
{
|
||||
float w=clamp(vMaterial.y,0.0,1.0);
|
||||
vec3 oreColor=vMaterial.x<1.5?vec3(0.94,0.36,0.12):
|
||||
vMaterial.x<2.5?vec3(0.20,0.48,0.92):vec3(0.25,0.88,0.84);
|
||||
resourceColor+=oreColor*w;resourceWeight+=w;
|
||||
}
|
||||
if(vMaterial.z>0.5)
|
||||
{
|
||||
float w=clamp(vMaterial.w,0.0,1.0);
|
||||
vec3 oreColor=vMaterial.z<1.5?vec3(0.94,0.36,0.12):
|
||||
vMaterial.z<2.5?vec3(0.20,0.48,0.92):vec3(0.25,0.88,0.84);
|
||||
resourceColor+=oreColor*w;resourceWeight+=w;
|
||||
}
|
||||
if(resourceWeight>0.0)
|
||||
color=mix(color,resourceColor/max(resourceWeight,0.001),
|
||||
clamp(resourceWeight,0.0,0.95))*illumination;
|
||||
}
|
||||
if(uTileEdges!=0&&vTileData.z>0.0)
|
||||
{
|
||||
vec2 local=mod(vWorldPosition.xy-vTileData.xy,vTileData.z);
|
||||
@@ -749,6 +791,8 @@ void main()
|
||||
reinterpret_cast<void*>(offsetof(FVertex,WorldPosition)));
|
||||
glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,TileData)));
|
||||
glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex),
|
||||
reinterpret_cast<void*>(offsetof(FVertex,Material)));
|
||||
glViewport(0,0,Width,Height);glDisable(GL_CULL_FACE);glEnable(GL_DEPTH_TEST);
|
||||
glDepthFunc(GL_LEQUAL);glClearColor(0.035f,0.055f,0.075f,1.0f);
|
||||
glClearDepthf(1.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -57,6 +58,8 @@ namespace
|
||||
bool FloatingIslandsRecipe=false;
|
||||
bool LandmarkChamberDemo=false;
|
||||
bool LandmarkL45RampControl=false;
|
||||
bool OreVeinsDemo=false;
|
||||
bool AuditOreVeinYield=false;
|
||||
bool AuditLandmarkReachability=false;
|
||||
bool TlsProbeRequested=false;
|
||||
int TlsProbeWorkers=0;
|
||||
@@ -620,6 +623,138 @@ namespace
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MakeOreVeinDemoRequests(
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3& layout,
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1>& requests,
|
||||
std::string& error)
|
||||
{
|
||||
using namespace VoxelForgeCore;
|
||||
requests.clear();
|
||||
const auto surface=std::find_if(layout.strates.begin(),layout.strates.end(),
|
||||
[](const auto& value){return value.generatorKind=="SurfaceWorld";});
|
||||
if(surface==layout.strates.end()||layout.coreVoxelSizeMicrometres==0)
|
||||
{error="ore-vein demo needs a SurfaceWorld strate";return false;}
|
||||
constexpr double Q=static_cast<double>(WorldAssemblySubvoxelsPerVoxel);
|
||||
const double coreMicrometres=layout.coreVoxelSizeMicrometres;
|
||||
const auto addCapsule=[&](std::string id,std::uint16_t resourceId,
|
||||
std::array<double,3> base,std::array<double,3> tangent,
|
||||
std::array<double,3> side,double routeClearRadius,
|
||||
std::int64_t radius,std::int64_t irregularity,std::int64_t blend,
|
||||
std::int64_t length)
|
||||
{
|
||||
const auto normalize=[](std::array<double,3>& value)
|
||||
{
|
||||
const double length=std::sqrt(value[0]*value[0]+value[1]*value[1]
|
||||
+value[2]*value[2]);
|
||||
if(length<=1.0e-9)return false;
|
||||
for(double& component:value)component/=length;
|
||||
return true;
|
||||
};
|
||||
if(!normalize(tangent))return false;
|
||||
const double sideLength=std::sqrt(side[0]*side[0]+side[1]*side[1]);
|
||||
if(sideLength<=1.0e-9)return false;
|
||||
side[0]/=sideLength;side[1]/=sideLength;
|
||||
const double offset=routeClearRadius+radius+irregularity+200000.0;
|
||||
base[0]+=side[0]*offset;base[1]+=side[1]*offset;
|
||||
ResolvedLayoutOreVeinRequestV1 request;
|
||||
request.stableId=std::move(id);request.strateIndex=surface->strateIndex;
|
||||
request.resourceMaterialId=resourceId;request.placementRadiusMicrometres=1500000;
|
||||
request.radiusMicrometres=radius;
|
||||
request.irregularityAmplitudeMicrometres=irregularity;
|
||||
request.irregularityWavelengthMicrometres=1400000;
|
||||
request.blendWidthMicrometres=blend;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double half=static_cast<double>(length)*0.5*tangent[axis];
|
||||
request.anchorEndpointsMicrometres[0][axis]=static_cast<std::int64_t>(
|
||||
std::llround(base[axis]-half));
|
||||
request.anchorEndpointsMicrometres[1][axis]=static_cast<std::int64_t>(
|
||||
std::llround(base[axis]+half));
|
||||
}
|
||||
requests.push_back(std::move(request));return true;
|
||||
};
|
||||
std::vector<const ResolvedLayoutAnchorV2*> route;
|
||||
for(const auto& anchor:surface->anchors)
|
||||
if(anchor.kind=="route_point"&&anchor.values.size()>=3)route.push_back(&anchor);
|
||||
if(route.size()<12)
|
||||
{error="ore-vein demo needs at least twelve resolved SurfaceWorld route points";return false;}
|
||||
const auto routePoint=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{
|
||||
return std::array<double,3>{
|
||||
anchor.values[0]*coreMicrometres/Q,
|
||||
anchor.values[1]*coreMicrometres/Q,
|
||||
anchor.values[2]*coreMicrometres/Q};
|
||||
};
|
||||
const auto routeRadius=[&](const ResolvedLayoutAnchorV2& anchor)
|
||||
{
|
||||
return anchor.values.size()>=4?anchor.values[3]*coreMicrometres/Q:0.0;
|
||||
};
|
||||
for(const auto [pointIndex,id,resourceId,sideSign]:{
|
||||
std::tuple<std::size_t,const char*,std::uint16_t,double>{7,
|
||||
"ore/sw-arrival-route-copper-v1",1,1.0},
|
||||
std::tuple<std::size_t,const char*,std::uint16_t,double>{10,
|
||||
"ore/sw-arrival-route-cobalt-v1",2,-1.0}})
|
||||
{
|
||||
const auto a=routePoint(*route[pointIndex]);
|
||||
const auto b=routePoint(*route[pointIndex+1]);
|
||||
std::array<double,3> tangent={b[0]-a[0],b[1]-a[1],b[2]-a[2]};
|
||||
std::array<double,3> side={-tangent[1]*sideSign,tangent[0]*sideSign,0.0};
|
||||
std::array<double,3> center={(a[0]+b[0])*0.5,(a[1]+b[1])*0.5,
|
||||
(a[2]+b[2])*0.5};
|
||||
const double clearance=std::max(routeRadius(*route[pointIndex]),
|
||||
routeRadius(*route[pointIndex+1]));
|
||||
if(!addCapsule(id,resourceId,center,tangent,side,clearance,
|
||||
resourceId==1?650000:750000,100000,600000,2500000))
|
||||
{error="ore-vein route capsule has a degenerate tangent";return false;}
|
||||
}
|
||||
const auto edge=std::find_if(layout.cavernEdges.begin(),layout.cavernEdges.end(),
|
||||
[&](const auto& value){return value.strateIndex==surface->strateIndex;});
|
||||
if(edge!=layout.cavernEdges.end())
|
||||
{
|
||||
constexpr double t=0.12,one=1.0-t;
|
||||
const std::array<double,4> weights={one*one*one,3.0*one*one*t,
|
||||
3.0*one*t*t,t*t*t};
|
||||
const std::array<double,4> derivative={-3.0*one*one,
|
||||
3.0*one*one-6.0*one*t,6.0*one*t-3.0*t*t,3.0*t*t};
|
||||
std::array<double,3> center{},tangent{};
|
||||
for(std::size_t control=0;control<4;++control)
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double point=edge->controlPointsMicrometres[control][axis];
|
||||
center[axis]+=weights[control]*point;
|
||||
tangent[axis]+=derivative[control]*point;
|
||||
}
|
||||
center[2]+=edge->boreHeightMicrometres*0.5;
|
||||
// The room lies west of the entry tangent; use the east wall for the
|
||||
// approach outcrop so its conservative bounds stay outside the room.
|
||||
std::array<double,3> side={tangent[1],-tangent[0],0.0};
|
||||
const double wallOffset=edge->boreRadiusMicrometres+800000.0+100000.0+200000.0;
|
||||
const double sideLength=std::sqrt(side[0]*side[0]+side[1]*side[1]);
|
||||
if(sideLength>1.0e-9)
|
||||
{side[0]*=wallOffset/sideLength;side[1]*=wallOffset/sideLength;}
|
||||
ResolvedLayoutOreVeinRequestV1 request;
|
||||
request.stableId="ore/sw-chamber-approach-quartz-v1";
|
||||
request.strateIndex=surface->strateIndex;request.resourceMaterialId=3;
|
||||
request.placementRadiusMicrometres=1000000;
|
||||
request.radiusMicrometres=800000;
|
||||
request.irregularityAmplitudeMicrometres=100000;
|
||||
request.irregularityWavelengthMicrometres=1800000;
|
||||
request.blendWidthMicrometres=600000;
|
||||
for(std::size_t axis=0;axis<3;++axis)
|
||||
{
|
||||
const double half=1250000.0*tangent[axis]
|
||||
/std::sqrt(tangent[0]*tangent[0]+tangent[1]*tangent[1]
|
||||
+tangent[2]*tangent[2]);
|
||||
request.anchorEndpointsMicrometres[0][axis]=static_cast<std::int64_t>(
|
||||
std::llround(center[axis]+side[axis]-half));
|
||||
request.anchorEndpointsMicrometres[1][axis]=static_cast<std::int64_t>(
|
||||
std::llround(center[axis]+side[axis]+half));
|
||||
}
|
||||
requests.push_back(std::move(request));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OverrideTransitions(VFJson::Value& root,int mode,int widthChunks,std::string& error)
|
||||
{
|
||||
auto* object=std::get_if<VFJson::Value::Object>(&root.Data);
|
||||
@@ -674,6 +809,8 @@ namespace
|
||||
" --profile-column-overlap --far-horizon-lod8-step=N --force-generate\n"
|
||||
" --seed=N --recipe=surface-world|floating-islands\n"
|
||||
" --landmark-chamber-demo (opt-in SW cavern room and swept tunnel)\n"
|
||||
" --ore-veins-demo (opt-in material-only SW capsule veins)\n"
|
||||
" --audit-ore-vein-yield (measure one deterministic sphere carve)\n"
|
||||
" --landmark-l45-ramp-control (use the buried L45 stamp geometry as an audit control)\n"
|
||||
" --resolved-layout-output=FILE (canonical plan-time layout snapshot)\n"
|
||||
" --resolved-layout-only (exit after plan and snapshot validation)\n"
|
||||
@@ -744,6 +881,8 @@ namespace
|
||||
}
|
||||
else if(arg=="--landmark-chamber-demo")options.LandmarkChamberDemo=true;
|
||||
else if(arg=="--landmark-l45-ramp-control")options.LandmarkL45RampControl=true;
|
||||
else if(arg=="--ore-veins-demo")options.OreVeinsDemo=true;
|
||||
else if(arg=="--audit-ore-vein-yield")options.AuditOreVeinYield=true;
|
||||
else if(arg.starts_with("--resolved-layout-output="))
|
||||
options.ResolvedLayoutOutputPath=std::string(arg.substr(std::string_view("--resolved-layout-output=").size()));
|
||||
else if(arg.starts_with("--seed="))
|
||||
@@ -828,6 +967,8 @@ namespace
|
||||
{std::fprintf(stderr,"--region-crossing requires --camera-step=X,Y,Z\n");return false;}
|
||||
if(options.LandmarkL45RampControl&&!options.LandmarkChamberDemo)
|
||||
{std::fprintf(stderr,"--landmark-l45-ramp-control requires --landmark-chamber-demo\n");return false;}
|
||||
if(options.AuditOreVeinYield&&!options.OreVeinsDemo)
|
||||
{std::fprintf(stderr,"--audit-ore-vein-yield requires --ore-veins-demo\n");return false;}
|
||||
if(options.TlsProbeRequested)return true;
|
||||
if(!options.WorldAssemblerPrototype||!options.StreamBenchmark||!options.StreamPipeline)
|
||||
{
|
||||
@@ -969,6 +1110,43 @@ int main(int argc,char** argv)
|
||||
resolvedLayout.cavernRooms.size(),resolvedLayout.cavernEdges.size(),
|
||||
resolvedLayout.stamps.size(),resolvedLayout.cavernGraphVocabularyVersion);
|
||||
}
|
||||
if(options.OreVeinsDemo)
|
||||
{
|
||||
std::vector<VoxelForgeCore::ResolvedLayoutOreVeinRequestV1> requests;
|
||||
if(!MakeOreVeinDemoRequests(resolvedLayout,requests,error)
|
||||
||!VoxelForgeCore::ResolveResolvedLayoutOreVeinsV1(
|
||||
resolvedLayout,seed,requests,error))
|
||||
{std::fprintf(stderr,"Ore-vein demo resolution failed: %s\n",error.c_str());return 2;}
|
||||
const auto audit=VoxelForgeCore::AuditResolvedLayoutOreVeinPlacementV1(resolvedLayout);
|
||||
std::printf("[ResolvedOreVeinPlacementAudit] vocabulary=%u records=%zu forced_clear_passage=rejected controls=%u/%u status=%s\n",
|
||||
resolvedLayout.oreVeinVocabularyVersion,resolvedLayout.oreVeins.size(),
|
||||
audit.negativeControlsRejected,audit.negativeControlsRun,
|
||||
audit.passed?"PASS":"FAIL");
|
||||
if(!audit.passed)
|
||||
{std::fprintf(stderr,"Resolved ore-vein placement negative control failed.\n");return 2;}
|
||||
for(const auto& vein:resolvedLayout.oreVeins)
|
||||
std::printf("[ResolvedOreVein] id=%s strate=%d resource=%u shape=capsule endpoints_m=(%.3f,%.3f,%.3f)->(%.3f,%.3f,%.3f) radius_m=%.2f irregularity_m=%.2f/%.2f blend_m=%.2f bounds_um=[%lld,%lld)x[%lld,%lld)x[%lld,%lld) seed=%llu\n",
|
||||
vein.stableId.c_str(),vein.strateIndex,vein.resourceMaterialId,
|
||||
vein.endpointsMicrometres[0][0]/1000000.0,
|
||||
vein.endpointsMicrometres[0][1]/1000000.0,
|
||||
vein.endpointsMicrometres[0][2]/1000000.0,
|
||||
vein.endpointsMicrometres[1][0]/1000000.0,
|
||||
vein.endpointsMicrometres[1][1]/1000000.0,
|
||||
vein.endpointsMicrometres[1][2]/1000000.0,
|
||||
vein.radiusMicrometres/1000000.0,
|
||||
vein.irregularityAmplitudeMicrometres/1000000.0,
|
||||
vein.irregularityWavelengthMicrometres/1000000.0,
|
||||
vein.blendWidthMicrometres/1000000.0,
|
||||
static_cast<long long>(vein.boundsMinimumMicrometres[0]),
|
||||
static_cast<long long>(vein.boundsMaximumMicrometres[0]),
|
||||
static_cast<long long>(vein.boundsMinimumMicrometres[1]),
|
||||
static_cast<long long>(vein.boundsMaximumMicrometres[1]),
|
||||
static_cast<long long>(vein.boundsMinimumMicrometres[2]),
|
||||
static_cast<long long>(vein.boundsMaximumMicrometres[2]),
|
||||
static_cast<unsigned long long>(vein.irregularitySeed));
|
||||
std::printf("[ResolvedOreVeinDemo] enabled=true seed=%d records=%zu stable_seed=seed,strate,stable_id placement=side-pocket-and-chamber-approach geometry_changes=0 status=PASS\n",
|
||||
seed,resolvedLayout.oreVeins.size());
|
||||
}
|
||||
const std::string layoutJson=resolvedLayout.ToJson();
|
||||
const auto roundTrip=VoxelForgeCore::ResolvedLayoutSnapshotV3::FromJson(layoutJson,error);
|
||||
if(!roundTrip||roundTrip->ToJson()!=layoutJson||roundTrip->stableIdentity!=resolvedLayout.stableIdentity)
|
||||
@@ -1240,5 +1418,6 @@ int main(int argc,char** argv)
|
||||
options.DiagnosticSlowShapeCompileMs,options.AuditBasePageEquivalence,
|
||||
options.AuditRouteParity,options.AuditResolvedLayoutBoundaries,
|
||||
options.AuditResolvedLayoutPassages,options.AuditStrateTransitions,
|
||||
&resolvedLayout,options.StreamHashTiles,options.AuditLandmarkReachability);
|
||||
&resolvedLayout,options.StreamHashTiles,options.AuditLandmarkReachability,
|
||||
options.AuditOreVeinYield);
|
||||
}
|
||||
@@ -808,6 +808,7 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
std::ofstream File;
|
||||
bool bGood=true;
|
||||
bool bIncludeMaterialAttributes=false;
|
||||
std::uint32_t TileCount=0;
|
||||
|
||||
template<typename T> void Write(const T& Value)
|
||||
@@ -818,7 +819,7 @@ namespace StandaloneStreamBenchmark
|
||||
|
||||
bool Open(const std::filesystem::path& Path,std::uint32_t VoxelSizeMicrons,
|
||||
const FVector& CameraVoxels,float YawDegrees,float PitchDegrees,
|
||||
std::uint32_t ExpectedTiles)
|
||||
std::uint32_t ExpectedTiles,bool IncludeMaterialAttributes)
|
||||
{
|
||||
std::error_code Error;
|
||||
if(!Path.parent_path().empty())
|
||||
@@ -826,9 +827,11 @@ namespace StandaloneStreamBenchmark
|
||||
if(Error)return false;
|
||||
File.open(Path,std::ios::binary|std::ios::trunc);
|
||||
if(!File)return false;
|
||||
constexpr char Magic[8]={'V','F','M','S','H','0','0','1'};
|
||||
bIncludeMaterialAttributes=IncludeMaterialAttributes;
|
||||
const char Magic[8]={'V','F','M','S','H','0','0',
|
||||
static_cast<char>(bIncludeMaterialAttributes?'2':'1')};
|
||||
File.write(Magic,sizeof(Magic));
|
||||
const std::uint32_t Version=1;
|
||||
const std::uint32_t Version=bIncludeMaterialAttributes?2u:1u;
|
||||
Write(Version);Write(VoxelSizeMicrons);Write(ExpectedTiles);
|
||||
const double MicronsToMeters=static_cast<double>(VoxelSizeMicrons)/1000000.0;
|
||||
const double Camera[3]={CameraVoxels.X*MicronsToMeters,
|
||||
@@ -855,6 +858,13 @@ namespace StandaloneStreamBenchmark
|
||||
for(const float Value:Vertex.Position)Write(Value);
|
||||
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||||
for(const float Value:Vertex.Normal)Write(Value);
|
||||
if(bIncludeMaterialAttributes)
|
||||
for(const FStandaloneCpuVertex& Vertex:Tile.Vertices)
|
||||
{
|
||||
Write(Vertex.HostMaterialId);
|
||||
for(const std::uint16_t id:Vertex.ResourceMaterialIds)Write(id);
|
||||
for(const std::uint16_t weight:Vertex.ResourceWeightsQ16)Write(weight);
|
||||
}
|
||||
if(IndexCount)
|
||||
File.write(reinterpret_cast<const char*>(Tile.Indices.data()),
|
||||
static_cast<std::streamsize>(sizeof(std::uint32_t)*Tile.Indices.size()));
|
||||
@@ -1097,7 +1107,8 @@ namespace StandaloneStreamBenchmark
|
||||
bool bAuditResolvedLayoutPassages=false,
|
||||
bool bAuditStrateTransitions=false,
|
||||
const VoxelForgeCore::ResolvedLayoutSnapshotV3* ResolvedLayout=nullptr,
|
||||
bool bPrintTileHashes=false,bool bAuditLandmarkReachability=false)
|
||||
bool bPrintTileHashes=false,bool bAuditLandmarkReachability=false,
|
||||
bool bAuditOreVeinYield=false)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
FStandaloneOwnerWorld& World=*OwnerWorld;
|
||||
@@ -1282,6 +1293,65 @@ namespace StandaloneStreamBenchmark
|
||||
Streamer.Stop();
|
||||
std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;
|
||||
}
|
||||
if(bAuditOreVeinYield)
|
||||
{
|
||||
if(!ResolvedLayout||ResolvedLayout->oreVeins.empty())
|
||||
{std::fprintf(stderr,"[OreMiningYield] missing resolved ore-vein records\n");return 2;}
|
||||
const auto vein=std::find_if(ResolvedLayout->oreVeins.begin(),
|
||||
ResolvedLayout->oreVeins.end(),[](const auto& value)
|
||||
{return value.stableId.find("arrival-route-copper")!=std::string::npos;});
|
||||
const auto& selected=vein==ResolvedLayout->oreVeins.end()
|
||||
?ResolvedLayout->oreVeins.front():*vein;
|
||||
const auto micrometresToVoxel=[&](const std::array<std::int64_t,3>& point)
|
||||
{
|
||||
const double size=static_cast<double>(World.CoreVoxelSizeMicrons);
|
||||
return VoxelForgeCore::Vec3d{point[0]/size,point[1]/size,point[2]/size};
|
||||
};
|
||||
const std::array<std::int64_t,3> editCenterMicrometres={
|
||||
(selected.endpointsMicrometres[0][0]+selected.endpointsMicrometres[1][0])/2,
|
||||
(selected.endpointsMicrometres[0][1]+selected.endpointsMicrometres[1][1])/2,
|
||||
(selected.endpointsMicrometres[0][2]+selected.endpointsMicrometres[1][2])/2};
|
||||
const VoxelForgeCore::Vec3d editCenter=micrometresToVoxel(editCenterMicrometres);
|
||||
const auto mined=Shape.measureOreMiningYieldForSphere(editCenter,1.25,0.25);
|
||||
std::uint64_t weightedSamples=0;double resourceVolume=0.0;
|
||||
for(const auto& resource:mined.resources)
|
||||
{weightedSamples+=resource.weightedSamplesQ16;resourceVolume+=resource.volumeCubicMetres;}
|
||||
const bool yieldPassed=weightedSamples>0&&resourceVolume>0.0;
|
||||
std::printf("[OreMiningYield] edit=sphere record=%s center_voxel=(%.3f,%.3f,%.3f) radius_m=1.250 resolution_m=%.3f removed_solid_samples=%llu resource_volume_m3=%.6f status=%s\n",
|
||||
selected.stableId.c_str(),editCenter.x,editCenter.y,editCenter.z,
|
||||
mined.sampleSpacingMetres,static_cast<unsigned long long>(mined.solidSamplesRemoved),
|
||||
resourceVolume,yieldPassed?"PASS":"FAIL");
|
||||
for(const auto& resource:mined.resources)
|
||||
std::printf("[OreMiningYieldResource] id=%u volume_m3=%.6f weighted_samples_q16=%llu\n",
|
||||
resource.resourceMaterialId,resource.volumeCubicMetres,
|
||||
static_cast<unsigned long long>(resource.weightedSamplesQ16));
|
||||
VoxelForgeCore::Vec3d airCenter{};
|
||||
if(!ResolvedLayout->cavernEdges.empty())
|
||||
{
|
||||
const auto& edge=ResolvedLayout->cavernEdges.front();
|
||||
airCenter=micrometresToVoxel(edge.controlPointsMicrometres[0]);
|
||||
airCenter.z+=edge.boreHeightMicrometres*0.5
|
||||
/static_cast<double>(World.CoreVoxelSizeMicrons);
|
||||
}
|
||||
else if(!ResolvedLayout->passages.empty())
|
||||
{
|
||||
const auto& mouth=ResolvedLayout->passages.front().lowerMouthSubvoxels;
|
||||
const double divisor=static_cast<double>(
|
||||
VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel);
|
||||
airCenter={mouth[0]/divisor,mouth[1]/divisor,mouth[2]/divisor};
|
||||
}
|
||||
const auto air=Shape.measureOreMiningYieldForSphere(airCenter,1.0,0.25);
|
||||
std::uint64_t airWeightedSamples=0;
|
||||
double airResourceVolume=0.0;
|
||||
for(const auto& resource:air.resources)
|
||||
{airWeightedSamples+=resource.weightedSamplesQ16;airResourceVolume+=resource.volumeCubicMetres;}
|
||||
const bool airPassed=airWeightedSamples==0&&airResourceVolume==0.0;
|
||||
std::printf("[OreMiningYieldControl] case=already-air center_voxel=(%.3f,%.3f,%.3f) radius_m=1.000 resolution_m=0.250 removed_solid_samples=%llu resource_volume_m3=%.6f status=%s\n",
|
||||
airCenter.x,airCenter.y,airCenter.z,
|
||||
static_cast<unsigned long long>(air.solidSamplesRemoved),
|
||||
airResourceVolume,airPassed?"PASS":"FAIL");
|
||||
if(!yieldPassed||!airPassed)return 14;
|
||||
}
|
||||
if(bAuditLandmarkReachability)
|
||||
{
|
||||
Streamer.Stop();
|
||||
@@ -3956,7 +4026,8 @@ namespace StandaloneStreamBenchmark
|
||||
{
|
||||
bSceneExportPassing=SceneExport.Open(MeshExportPath,World.CoreVoxelSizeMicrons,
|
||||
CameraPositionVoxels,CameraYawDegrees,CameraPitchDegrees,
|
||||
static_cast<std::uint32_t>(Update.DesiredTiles));
|
||||
static_cast<std::uint32_t>(Update.DesiredTiles),
|
||||
ResolvedLayout&&!ResolvedLayout->oreVeins.empty());
|
||||
if(!bSceneExportPassing)
|
||||
{
|
||||
Streamer.Stop();
|
||||
@@ -3966,7 +4037,9 @@ namespace StandaloneStreamBenchmark
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
||||
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows,MaterialRows;
|
||||
std::uint64_t MaterialQueries=0,MaterialCandidateChecks=0,MaterialOutsideTiles=0;
|
||||
double MaterialQueryMilliseconds=0.0;
|
||||
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
||||
int32 FullDetailExpected=0;
|
||||
uint64 WholeNonEmptyMeshTiles=0,WholeTriangleCount=0;
|
||||
@@ -4197,12 +4270,17 @@ namespace StandaloneStreamBenchmark
|
||||
NativeColumnComputeMilliseconds+=Tile.CoreDiagnostics.columnComputeMilliseconds;
|
||||
NativeMeshingMilliseconds+=Tile.CoreDiagnostics.meshingMilliseconds;
|
||||
}
|
||||
MaterialQueries+=Tile.CoreDiagnostics.materialQueries;
|
||||
MaterialCandidateChecks+=Tile.CoreDiagnostics.materialCandidateChecks;
|
||||
MaterialOutsideTiles+=Tile.CoreDiagnostics.materialTileOutsideEveryVein?1u:0u;
|
||||
MaterialQueryMilliseconds+=Tile.CoreDiagnostics.materialQueryMilliseconds;
|
||||
if(bHashDensity)
|
||||
{
|
||||
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
||||
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
||||
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
||||
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
||||
if(Tile.bHasMaterialHash)MaterialRows.push_back({Tile.Key,Tile.MaterialHash});
|
||||
}
|
||||
}
|
||||
if(!bFirstTileReported&&FirstTileReady.time_since_epoch().count()!=0)
|
||||
@@ -4580,6 +4658,11 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(NativeColumnMisses),
|
||||
NativeColumnComputeMilliseconds,NativeClassificationMilliseconds,
|
||||
NativeMeshingMilliseconds);
|
||||
std::printf("[MaterialQueryCost] queries=%llu candidate_checks=%llu outside_vein_tiles=%llu tiles=%d elapsed_ms=%.3f outside_query_ms=0 status=%s\n",
|
||||
static_cast<unsigned long long>(MaterialQueries),
|
||||
static_cast<unsigned long long>(MaterialCandidateChecks),
|
||||
static_cast<unsigned long long>(MaterialOutsideTiles),Finished,
|
||||
MaterialQueryMilliseconds,MaterialQueries==0?"zero":"measured");
|
||||
if(bAuditUniformBlockProofs)
|
||||
std::printf("[UniformBlockProofAudit] checked_blocks=%llu checked_samples=%llu fallback_blocks=%llu unknown_blocks=%llu violations=%llu\n",
|
||||
static_cast<unsigned long long>(BlockProofBlocksChecked),
|
||||
@@ -4594,7 +4677,8 @@ namespace StandaloneStreamBenchmark
|
||||
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
||||
&&MeshRows.size()==Desired.Tiles.size()
|
||||
&&PayloadRows.size()==Desired.Tiles.size()
|
||||
&&TopologyRows.size()==Desired.Tiles.size();
|
||||
&&TopologyRows.size()==Desired.Tiles.size()
|
||||
&&MaterialRows.size()==Desired.Tiles.size();
|
||||
if(bPrintTileHashes)
|
||||
{
|
||||
const auto makeHashIndex=[](const std::vector<FHashedTile>& rows)
|
||||
@@ -4607,6 +4691,7 @@ namespace StandaloneStreamBenchmark
|
||||
const auto MeshIndex=makeHashIndex(MeshRows);
|
||||
const auto PayloadIndex=makeHashIndex(PayloadRows);
|
||||
const auto TopologyIndex=makeHashIndex(TopologyRows);
|
||||
const auto MaterialIndex=makeHashIndex(MaterialRows);
|
||||
const auto findHash=[](const auto& index,const FVoxelTileKey& key)
|
||||
{
|
||||
const auto found=index.find(key);
|
||||
@@ -4619,6 +4704,10 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(findHash(MeshIndex,row.Key)),
|
||||
static_cast<unsigned long long>(findHash(PayloadIndex,row.Key)),
|
||||
static_cast<unsigned long long>(findHash(TopologyIndex,row.Key)));
|
||||
for(const FHashedTile& row:MaterialRows)
|
||||
std::printf("[PipelineTileMaterialHash] lod=%d x=%d y=%d z=%d material=0x%016llx\n",
|
||||
row.Key.Level,row.Key.Coord.X,row.Key.Coord.Y,row.Key.Coord.Z,
|
||||
static_cast<unsigned long long>(row.Hash));
|
||||
}
|
||||
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
|
||||
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
|
||||
@@ -4635,6 +4724,8 @@ namespace StandaloneStreamBenchmark
|
||||
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
||||
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
||||
std::printf("[PipelineMaterialHash] tiles=%zu material=0x%016llx\n",
|
||||
MaterialRows.size(),static_cast<unsigned long long>(HashTileRows(MaterialRows)));
|
||||
for(std::size_t LOD=0;LOD<TileCountByLOD.size();++LOD)
|
||||
{
|
||||
const auto FilterLOD=[LOD](const std::vector<FHashedTile>& Rows)
|
||||
|
||||
@@ -60,6 +60,8 @@ struct FStandaloneCpuTile
|
||||
bool bHasPayloadHash=false;
|
||||
uint64 TopologyHash=14695981039346656037ull;
|
||||
bool bHasTopologyHash=false;
|
||||
uint64 MaterialHash=14695981039346656037ull;
|
||||
bool bHasMaterialHash=false;
|
||||
bool bGenerationFailed=false;
|
||||
uint64 MeshPayloadAllocatedBytes=0;
|
||||
VoxelForgeCore::TileGenerationStats CoreDiagnostics{};
|
||||
@@ -846,15 +848,18 @@ private:
|
||||
Out.MeshPayloadAllocatedBytes=Result->meshPayloadAllocatedBytes;
|
||||
Out.DensityHash=Result->densityHash;Out.MeshHash=Result->meshHash;
|
||||
Out.PayloadHash=Result->payloadHash;Out.TopologyHash=Result->topologyHash;
|
||||
Out.MaterialHash=Result->materialHash;
|
||||
Out.bHasDensityHash=Result->hasDensityHash;Out.bHasMeshHash=Result->hasMeshHash;
|
||||
Out.bHasPayloadHash=Result->hasPayloadHash;Out.bHasTopologyHash=Result->hasTopologyHash;
|
||||
Out.bHasMaterialHash=Result->hasMaterialHash;
|
||||
Out.Vertices.reserve(Result->vertices.size());
|
||||
for(const auto& Vertex:Result->vertices)
|
||||
Out.Vertices.push_back({
|
||||
{static_cast<float>(Vertex.position.x*0.01),static_cast<float>(Vertex.position.y*0.01),
|
||||
static_cast<float>(Vertex.position.z*0.01)},
|
||||
{static_cast<float>(Vertex.normal.x),static_cast<float>(Vertex.normal.y),
|
||||
static_cast<float>(Vertex.normal.z)}});
|
||||
static_cast<float>(Vertex.normal.z)},
|
||||
Vertex.hostMaterialId,Vertex.resourceMaterialIds,Vertex.resourceWeightsQ16});
|
||||
Out.Indices=std::move(Result->indices);
|
||||
Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks);
|
||||
if(bProfile)
|
||||
|
||||
@@ -5,10 +5,19 @@
|
||||
#include "VoxelMarchingCubesMesher.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
|
||||
struct FStandaloneCpuVertex { float Position[3]; float Normal[3]; };
|
||||
struct FStandaloneCpuVertex
|
||||
{
|
||||
float Position[3];
|
||||
float Normal[3];
|
||||
std::uint16_t HostMaterialId=1;
|
||||
std::array<std::uint16_t,2> ResourceMaterialIds{};
|
||||
std::array<std::uint16_t,2> ResourceWeightsQ16{};
|
||||
};
|
||||
|
||||
struct FStandaloneCpuLevel
|
||||
{
|
||||
|
||||
@@ -56,6 +56,9 @@ TILE_HASH_RE = re.compile(
|
||||
r"^\[PipelineTileHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"density=(0x[0-9a-f]+) mesh=(0x[0-9a-f]+) "
|
||||
r"payload=(0x[0-9a-f]+) topology=(0x[0-9a-f]+)$", re.M)
|
||||
TILE_MATERIAL_HASH_RE = re.compile(
|
||||
r"^\[PipelineTileMaterialHash\] lod=(-?\d+) x=(-?\d+) y=(-?\d+) z=(-?\d+) "
|
||||
r"material=(0x[0-9a-f]+)$", re.M)
|
||||
CAMERA_SWEEP_SEED = 0x4C3434
|
||||
RECIPE_RE = re.compile(
|
||||
r"^\[WorldAssemblerRecipeAudit\] requested=([^ ]+) actual=([^ ]+).*status=([^ ]+)$", re.M)
|
||||
@@ -428,6 +431,115 @@ def parse_tile_hashes(output: str) -> dict[tuple[int, int, int, int], tuple[str,
|
||||
return rows
|
||||
|
||||
|
||||
def parse_tile_material_hashes(output: str) -> dict[tuple[int, int, int, int], str]:
|
||||
rows: dict[tuple[int, int, int, int], str] = {}
|
||||
for match in TILE_MATERIAL_HASH_RE.finditer(output):
|
||||
key = tuple(int(match.group(i)) for i in range(1, 5))
|
||||
rows[key] = match.group(5)
|
||||
return rows
|
||||
|
||||
|
||||
def ore_vein_audit_camera(layout_path: Path) -> tuple[float, float, float, float, float]:
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
core_um = int(layout["core_voxel_size_um"])
|
||||
route = [anchor for strate in layout["strates"]
|
||||
if strate["generator_kind"] == "SurfaceWorld"
|
||||
for anchor in strate["anchors"] if anchor["kind"] == "route_point"]
|
||||
route.sort(key=lambda anchor: int(anchor["stable_id"].split("/")[-1]))
|
||||
records = layout.get("ore_veins", [])
|
||||
vein = next((row for row in records
|
||||
if row["stable_id"] == "ore/sw-arrival-route-copper-v1"), None)
|
||||
if len(route) < 2 or vein is None:
|
||||
raise VerificationFailure(f"cannot derive ore camera from {layout_path}")
|
||||
target = [sum(value) / 2_000_000.0 for value in zip(*vein["endpoints_um"])]
|
||||
points = [[value * core_um / 256_000_000.0 for value in anchor["values"][:3]]
|
||||
for anchor in route]
|
||||
best: tuple[float, int, float, tuple[float, float, float]] | None = None
|
||||
for index, (a, b) in enumerate(zip(points, points[1:])):
|
||||
dx, dy = b[0] - a[0], b[1] - a[1]
|
||||
length2 = dx * dx + dy * dy
|
||||
t = 0.0 if length2 <= 1.0e-12 else max(0.0, min(1.0,
|
||||
((target[0] - a[0]) * dx + (target[1] - a[1]) * dy) / length2))
|
||||
point = tuple(a[axis] + t * (b[axis] - a[axis]) for axis in range(3))
|
||||
distance2 = (point[0] - target[0]) ** 2 + (point[1] - target[1]) ** 2
|
||||
candidate = (distance2, index, t, point)
|
||||
if best is None or candidate[:3] < best[:3]:
|
||||
best = candidate
|
||||
assert best is not None
|
||||
_, _, _, floor = best
|
||||
camera = (floor[0], floor[1], floor[2] + 1.6)
|
||||
dx, dy = target[0] - camera[0], target[1] - camera[1]
|
||||
horizontal = max(math.hypot(dx, dy), 1.0e-6)
|
||||
target_z = target[2] + 1.0
|
||||
yaw = math.degrees(math.atan2(dx, dy))
|
||||
pitch = math.degrees(math.atan2(target_z - camera[2], horizontal))
|
||||
return (*camera, yaw, pitch)
|
||||
|
||||
|
||||
def run_ore_vein_hash_controls(runner: VerificationRunner) -> None:
|
||||
camera_layout = OUT / "layouts" / "ore-vein-camera-layout.json"
|
||||
row, output = runner.run_cpp("ore-vein-camera-layout", CLANG, OWNER,
|
||||
"surface-world", ["--landmark-chamber-demo", "--ore-veins-demo",
|
||||
"--resolved-layout-only"],
|
||||
expected_style=2, expected_strate_styles={"0": 2, "1": 1}, workers=1,
|
||||
max_level=0, layout_path=camera_layout)
|
||||
audit = re.search(r"^\[ResolvedOreVeinPlacementAudit\].*status=PASS$", output, re.M)
|
||||
runner.require("ore-vein-resolved-records", audit is not None,
|
||||
f"layout={row['layout_id']} resolved={len(json.loads(camera_layout.read_text()).get('ore_veins', []))}/3")
|
||||
x, y, z, yaw, pitch = ore_vein_audit_camera(camera_layout)
|
||||
camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}"
|
||||
base_flags = ["--stream-hash-tiles", "--landmark-chamber-demo", camera_flag,
|
||||
"--clip-radius=2", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0"]
|
||||
geometry: dict[str, dict[tuple[int, int, int, int], tuple[str, ...]]] = {}
|
||||
material: dict[str, dict[tuple[int, int, int, int], str]] = {}
|
||||
outputs: dict[str, str] = {}
|
||||
for label, enabled in (("off", False), ("on", True)):
|
||||
flags = [*base_flags]
|
||||
if enabled:
|
||||
flags.extend(("--ore-veins-demo", "--audit-ore-vein-yield"))
|
||||
_, captured = runner.run_cpp(f"ore-vein-geometry-{label}", CLANG, OWNER,
|
||||
"surface-world", flags, expected_style=2,
|
||||
expected_strate_styles={"0": 2, "1": 1}, workers=1,
|
||||
max_level=0, layout_path=OUT / "layouts" / f"ore-vein-geometry-{label}.json")
|
||||
outputs[label] = captured
|
||||
geometry[label] = parse_tile_hashes(captured)
|
||||
material[label] = parse_tile_material_hashes(captured)
|
||||
whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", captured, re.M)
|
||||
expected_rows = int(whole.group(1)) if whole else -1
|
||||
runner.require(f"ore-vein-geometry-{label}:hash-rows",
|
||||
bool(geometry[label]) and len(geometry[label]) == expected_rows
|
||||
and len(material[label]) == expected_rows,
|
||||
f"geometry_rows={len(geometry[label])} material_rows={len(material[label])} "
|
||||
f"expected={expected_rows}")
|
||||
cost = re.search(r"^\[MaterialQueryCost\] queries=(\d+) candidate_checks=(\d+) "
|
||||
r"outside_vein_tiles=(\d+) tiles=(\d+) elapsed_ms=([0-9.]+) "
|
||||
r"outside_query_ms=0 status=([^ ]+)$", captured, re.M)
|
||||
cost_ok = cost is not None and int(cost.group(1)) == 0 if not enabled else (
|
||||
cost is not None and int(cost.group(1)) > 0 and int(cost.group(3)) > 0
|
||||
and float(cost.group(5)) >= 0.0)
|
||||
runner.require(f"ore-vein-material-cost-{label}", cost_ok,
|
||||
f"cost_line={cost.group(0) if cost else 'MISSING'}")
|
||||
differing = [key for key in set(material["off"]) | set(material["on"])
|
||||
if material["off"].get(key) != material["on"].get(key)]
|
||||
runner.require("ore-vein-geometry-unchanged-with-veins",
|
||||
geometry["off"] == geometry["on"],
|
||||
f"camera_m=({x:.3f},{y:.3f},{z:.3f}) density/mesh/payload/topology "
|
||||
f"tile_hashes_match={geometry['off']==geometry['on']} "
|
||||
f"material_tiles_changed={len(differing)}")
|
||||
runner.require("ore-vein-material-hash-changes",
|
||||
bool(differing) and material["off"] != material["on"],
|
||||
f"changed_tiles={len(differing)} camera_m=({x:.3f},{y:.3f},{z:.3f})")
|
||||
mined = re.search(r"^\[OreMiningYield\].*resource_volume_m3=([0-9.]+) status=PASS$",
|
||||
outputs["on"], re.M)
|
||||
air = re.search(r"^\[OreMiningYieldControl\] case=already-air.*"
|
||||
r"resource_volume_m3=0\.000000 status=PASS$", outputs["on"], re.M)
|
||||
runner.require("ore-vein-yield-positive", mined is not None and float(mined.group(1)) > 0,
|
||||
"fixed 0.25 m sample spacing measured positive removed resource volume")
|
||||
runner.require("ore-vein-yield-already-air-zero", air is not None,
|
||||
"the same sphere measurement on the already-air chamber tunnel returned zero")
|
||||
|
||||
|
||||
def cavern_audit_cameras(layout_path: Path) -> list[tuple[float, float, float, float, float]]:
|
||||
layout = json.loads(layout_path.read_text(encoding="utf-8"))
|
||||
edges = layout.get("cavern_edges", [])
|
||||
@@ -473,6 +585,7 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
label = (f"random-camera-{compiler}-{recipe}-s{CAMERA_SWEEP_SEED:06x}"
|
||||
f"-p{camera_index:02d}-r{clip_radius}-c{max_clip_level}-m{max_level}")
|
||||
signatures: dict[int, dict[tuple[int, int, int, int], tuple[str, ...]]] = {}
|
||||
material_signatures: dict[int, dict[tuple[int, int, int, int], str]] = {}
|
||||
for workers in (1, 8, 12):
|
||||
name = f"{label}-w{workers}"
|
||||
flags = ["--stream-hash-tiles", camera_flag,
|
||||
@@ -480,7 +593,7 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
f"--max-clip-level={max_clip_level}",
|
||||
f"--max-level={max_level}", "--render-distance-chunks=0"]
|
||||
if recipe == "surface-world":
|
||||
flags.append("--landmark-chamber-demo")
|
||||
flags.extend(("--landmark-chamber-demo", "--ore-veins-demo"))
|
||||
row, output = runner.run_cpp(name, binary, OWNER, recipe, flags,
|
||||
expected_style=2,
|
||||
expected_strate_styles={"0": 2, "1": 1},
|
||||
@@ -496,11 +609,17 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
}
|
||||
current = parse_tile_hashes(output)
|
||||
signatures[workers] = current
|
||||
material_signatures[workers] = parse_tile_material_hashes(output)
|
||||
whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M)
|
||||
expected_rows = int(whole.group(1)) if whole else -1
|
||||
runner.require(f"{name}:tile-hash-rows",
|
||||
bool(current) and len(current) == expected_rows,
|
||||
f"rows={len(current)} expected={expected_rows} settings={settings}")
|
||||
if recipe == "surface-world":
|
||||
material_rows = material_signatures[workers]
|
||||
runner.require(f"{name}:material-hash-rows",
|
||||
bool(material_rows) and len(material_rows) == expected_rows,
|
||||
f"rows={len(material_rows)} expected={expected_rows} settings={settings}")
|
||||
camera_text = (f"seed=0x{CAMERA_SWEEP_SEED:06x} camera_m="
|
||||
f"({x:.3f},{y:.3f},{z:.3f}) recipe={recipe} settings={settings}")
|
||||
for left, right in ((1, 8), (1, 12), (8, 12)):
|
||||
@@ -517,12 +636,19 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
topology_diff = [key for key in keys
|
||||
if left_rows.get(key, (None, None, None, None))[3]
|
||||
!= right_rows.get(key, (None, None, None, None))[3]]
|
||||
material_left=material_signatures[left]
|
||||
material_right=material_signatures[right]
|
||||
material_diff=([key for key in set(material_left)|set(material_right)
|
||||
if material_left.get(key)!=material_right.get(key)]
|
||||
if recipe=="surface-world" else [])
|
||||
detail = (f"{camera_text} W{left}=W{right} "
|
||||
f"density_tiles={len(density_diff)} mesh_tiles={len(mesh_diff)} "
|
||||
f"payload_tiles={len(payload_diff)} topology_tiles={len(topology_diff)} "
|
||||
f"material_tiles={len(material_diff)} "
|
||||
f"sample_coords={sorted(density_diff or mesh_diff)[:6]}")
|
||||
runner.require(f"{label}:W{left}-W{right}",
|
||||
left_rows == right_rows, detail)
|
||||
left_rows == right_rows and
|
||||
(recipe != "surface-world" or material_left == material_right), detail)
|
||||
|
||||
# Keep a pair of fixed cameras on the current owner graph: one in the bore and
|
||||
# one beside its curved wall. Their tile hashes join the seeded W1/W8/W12 sweep.
|
||||
@@ -536,10 +662,12 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
camera_flag = f"--camera={x:.6f},{y:.6f},{z:.6f},{yaw:.3f},{pitch:.3f}"
|
||||
label = f"cavern-camera-{compiler}-s{CAMERA_SWEEP_SEED:06x}-p{camera_index:02d}"
|
||||
signatures: dict[int, dict[tuple[int, int, int, int], tuple[str, ...]]] = {}
|
||||
material_signatures: dict[int, dict[tuple[int, int, int, int], str]] = {}
|
||||
for workers in (1, 8, 12):
|
||||
name = f"{label}-w{workers}"
|
||||
row, output = runner.run_cpp(name, binary, OWNER, "surface-world",
|
||||
["--stream-hash-tiles", camera_flag, "--landmark-chamber-demo",
|
||||
"--ore-veins-demo",
|
||||
"--clip-radius=3", "--max-clip-level=0", "--max-level=0",
|
||||
"--render-distance-chunks=0"],
|
||||
expected_style=2,
|
||||
@@ -548,17 +676,24 @@ def run_random_camera_sweep(runner: VerificationRunner, binaries: tuple[Path, ..
|
||||
row["camera_position_m"] = [round(value, 6) for value in camera[:3]]
|
||||
row["camera_region"] = "inside-tunnel" if camera_index == 0 else "near-tunnel-wall"
|
||||
signatures[workers] = parse_tile_hashes(output)
|
||||
material_signatures[workers] = parse_tile_material_hashes(output)
|
||||
whole = re.search(r"^\[PipelineWholeViewHash\] tiles=(\d+)", output, re.M)
|
||||
expected_rows = int(whole.group(1)) if whole else -1
|
||||
runner.require(f"{name}:tile-hash-rows",
|
||||
bool(signatures[workers]) and len(signatures[workers]) == expected_rows,
|
||||
f"rows={len(signatures[workers])} expected={expected_rows} camera={camera[:3]}")
|
||||
runner.require(f"{name}:material-hash-rows",
|
||||
bool(material_signatures[workers])
|
||||
and len(material_signatures[workers]) == expected_rows,
|
||||
f"rows={len(material_signatures[workers])} expected={expected_rows} "
|
||||
f"camera={camera[:3]}")
|
||||
for left, right in ((1, 8), (1, 12), (8, 12)):
|
||||
runner.require(f"{label}:W{left}-W{right}",
|
||||
signatures[left] == signatures[right],
|
||||
signatures[left] == signatures[right]
|
||||
and material_signatures[left] == material_signatures[right],
|
||||
f"camera_m=({x:.3f},{y:.3f},{z:.3f}) "
|
||||
f"region={'inside' if camera_index == 0 else 'near'}-tunnel "
|
||||
f"W{left}=W{right} density/mesh/payload/topology tile hashes match")
|
||||
f"W{left}=W{right} density/mesh/payload/topology/material tile hashes match")
|
||||
|
||||
|
||||
def run_ring(runner: VerificationRunner, *, full: bool) -> None:
|
||||
@@ -904,6 +1039,7 @@ def run_medium_suite(runner: VerificationRunner, *, both_compilers: bool = False
|
||||
run_transitions_and_invariants(runner, binaries)
|
||||
run_ring(runner, full=True)
|
||||
run_captures_and_mesh_audits(runner)
|
||||
run_ore_vein_hash_controls(runner)
|
||||
run_random_camera_sweep(runner, (CLANG,), 5,
|
||||
((3, 0, 0), (2, 1, 1), (3, 2, 1)))
|
||||
|
||||
@@ -939,6 +1075,7 @@ def main() -> int:
|
||||
run_landmark_reachability(runner, (CLANG,))
|
||||
run_regression_controls(runner)
|
||||
run_hash_pairs(runner, owners)
|
||||
run_ore_vein_hash_controls(runner)
|
||||
run_random_camera_sweep(runner, (CLANG,), 3, ((3, 0, 0),))
|
||||
run_ring(runner, full=False)
|
||||
elif ARGS.tier == "medium":
|
||||
|
||||
Reference in new issue
Block a user