diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index ad4171b..d1f9f51 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -81,6 +81,9 @@ namespace VoxelForgeCore Vec3d normal; Vec2f uv; ColorRgba8 color; + std::uint16_t hostMaterialId = 1; + std::array resourceMaterialIds{}; + std::array 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 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, 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, 2> endpointsMicrometres{}; + std::array boundsMinimumMicrometres{}; + std::array 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 cavernRooms; std::vector cavernEdges; + // Omitted from canonical identity when no records are present. + std::uint32_t oreVeinVocabularyVersion = 0; + std::vector oreVeins; std::string stableIdentity; bool finalize(std::string& error); @@ -1016,6 +1059,40 @@ namespace VoxelForgeCore std::span rooms, std::span edges, std::string& error); + VFCORE_API bool ResolveResolvedLayoutOreVeinsV1( + ResolvedLayoutSnapshotV3& layout, std::int32_t seed, + std::span 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 resourceMaterialIds{}; + std::array 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 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 passageVoidCrossings() const; bool isPassageColumnSample(std::size_t passageIndex, std::size_t segmentIndex, diff --git a/Standalone/Core/src/ResolvedLayoutInternal.h b/Standalone/Core/src/ResolvedLayoutInternal.h index bf0ead6..83e4283 100644 --- a/Standalone/Core/src/ResolvedLayoutInternal.h +++ b/Standalone/Core/src/ResolvedLayoutInternal.h @@ -283,6 +283,187 @@ namespace } return true; } + double LayoutSegmentDistance(const std::array& p0, + const std::array& p1,const std::array& q0, + const std::array& q1) + { + std::array 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 LayoutOrePointUm(const std::array& point) + {return {static_cast(point[0]),static_cast(point[1]), + static_cast(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]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(vein.radiusMicrometres + +vein.irregularityAmplitudeMicrometres+vein.blendWidthMicrometres); + const double coreMicrometres=std::max(1,layout.coreVoxelSizeMicrometres); + constexpr double RouteClearanceMicrometres=100000.0; + const auto qToUm=[&](const std::array& q) + {return std::array{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 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 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{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 previous{}; + const auto bezier=[&](double t) + { + const double one=1.0-t; + const std::array weights={one*one*one,3.0*one*one*t, + 3.0*one*t*t,t*t*t}; + std::array 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(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<(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 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::FromJson( s.stampVocabularyVersion=static_cast(LayoutInt(root,"stamp_vocabulary_version")); s.cavernGraphVocabularyVersion=static_cast( LayoutInt(root,"cavern_graph_vocabulary_version")); + s.oreVeinVocabularyVersion=static_cast( + 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::FromJson( edge.irregularitySeed=static_cast(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(LayoutInt(v,"strate_index")); + vein.resourceMaterialId=static_cast(LayoutInt(v,"resource_material_id")); + const auto& endpoints=v.Get("endpoints_um").AsArray(); + for(std::size_t endpoint=0;endpoint<2&&endpoint& target) + { + const auto& values=v.Get(key).AsArray(); + for(std::size_t axis=0;axis<3&&axis(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 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 ordered(requests.begin(),requests.end()); + std::sort(ordered.begin(),ordered.end(),[](const auto& a,const auto& b) + {return a.stableId 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(radius)*2+1; + return static_cast(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 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(offset[0])*offset[0] + +static_cast(offset[1])*offset[1] + +static_cast(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 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::llround( + static_cast(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 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) { diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index a2b706d..fb8c825 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -369,6 +369,7 @@ public: } std::sort(BoundStratumFields.begin(),BoundStratumFields.end(), [](const auto& A,const auto& B){return A.BottomChunkZ OreVeinTileCandidates( + const std::array& minimum, + const std::array& maximum) const + {return BuildOreVeinTileList(minimum,maximum);} + + VoxelForgeCore::MaterialSampleV1 EvaluateMaterialAt(float X,float Y,float Z, + std::span 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 minimumSubvoxels{},maximumSubvoxels{}; + }; + struct FOreVeinSpatialNode + { + std::array minimumSubvoxels{},maximumSubvoxels{}; + std::size_t first=0,end=0; + int left=-1,right=-1; + }; + struct FOreVeinRuntime + { + const VoxelForgeCore::ResolvedLayoutOreVeinV1* record=nullptr; + std::array,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::max()); + node.maximumSubvoxels.fill(std::numeric_limits::min()); + for(std::size_t offset=first;offset(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(node.maximumSubvoxels[axis]) + -node.minimumSubvoxels[axis] + >static_cast(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( + left.minimumSubvoxels[splitAxis])+left.maximumSubvoxels[splitAxis]; + const long double rightCenter=static_cast( + right.minimumSubvoxels[splitAxis])+right.maximumSubvoxels[splitAxis]; + return leftCenter==rightCenter?a& minimum, + const std::array& maximum, + std::vector& result) const + { + if(nodeIndex<0)return; + const auto& node=OreVeinSpatialNodes[static_cast(nodeIndex)]; + for(std::size_t axis=0;axis<3;++axis) + if(node.maximumSubvoxels[axis]maximum[axis])return; + if(node.left<0) + { + for(std::size_t offset=node.first;offset=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::floor( + static_cast(vein.boundsMinimumMicrometres[axis])*scale)); + bound.maximumSubvoxels[axis]=static_cast(std::ceil( + static_cast(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(vein.endpointsMicrometres[endpoint][axis]) + /Owner->CoreVoxelSizeMicrons; + runtime.radiusVoxels=static_cast(vein.radiusMicrometres) + /Owner->CoreVoxelSizeMicrons; + runtime.irregularityAmplitudeVoxels= + static_cast(vein.irregularityAmplitudeMicrometres) + /Owner->CoreVoxelSizeMicrons; + runtime.irregularityWavelengthVoxels= + static_cast(vein.irregularityWavelengthMicrometres) + /Owner->CoreVoxelSizeMicrons; + runtime.blendWidthVoxels=static_cast(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(i); + if(!OreVeinSpatialOrder.empty())BuildOreVeinSpatialNode(0,OreVeinSpatialOrder.size()); + } + + std::vector BuildOreVeinTileList( + const std::array& minimum, + const std::array& maximum) const + { + std::vector 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(i); + return result; + } + QueryOreVeinSpatialNode(0,minimum,maximum,result); + std::sort(result.begin(),result.end()); + return result; + } + + std::vector 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(value)*Q; + if(!std::isfinite(value)||scaled::min() + ||scaled>std::numeric_limits::max())return std::int64_t{0}; + return static_cast(std::llround(scaled)); + }; + const std::array point={toQ(X),toQ(Y),toQ(Z)}; + return BuildOreVeinTileList(point,point); + } + + VoxelForgeCore::MaterialSampleV1 MaterialAt(float X,float Y,float Z, + std::span 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 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(x), + static_cast(y),static_cast(z)}) + for(int byte=0;byte<8;++byte) + {hash^=static_cast(word>>(byte*8));hash*=1099511628211ull;} + hash^=hash>>30;hash*=0xbf58476d1ce4e5b9ull; + hash^=hash>>27;hash*=0x94d049bb133111ebull;hash^=hash>>31; + return static_cast(static_cast(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 cell{};std::array t{}; + for(std::size_t axis=0;axis<3;++axis) + { + const double coordinate=point[axis]/wavelength; + cell[axis]=static_cast(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::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( + result.resourceWeightsQ16[slot])+weightQ16; + result.resourceWeightsQ16[slot]=static_cast( + std::min(65535,sum)); + } + } + const std::uint32_t total=static_cast(result.resourceWeightsQ16[0]) + +result.resourceWeightsQ16[1]; + if(total>65535) + { + result.resourceWeightsQ16[0]=static_cast( + static_cast(result.resourceWeightsQ16[0])*65535/total); + result.resourceWeightsQ16[1]=static_cast( + 65535-result.resourceWeightsQ16[0]); + } + return result; + } + std::vector BuildStampTileList( const std::array& minimum, const std::array& maximum) const @@ -9412,6 +9705,10 @@ private: std::vector StampSpatialNodes; std::vector StampSpatialOrder; std::vector StampRuntime; + std::vector OreVeinSpatialBounds; + std::vector OreVeinSpatialNodes; + std::vector OreVeinSpatialOrder; + std::vector OreVeinRuntime; std::vector CavernRuntime; std::vector PassageFeatureBounds; std::vector PassageTileFeatures; diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index a01386e..8c86c94 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -216,6 +217,19 @@ namespace VoxelForgeCore return hash; } + std::uint64_t HashMaterialSamples(std::span 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(position.x),static_cast(position.y),static_cast(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(position.x), + static_cast(position.y),static_cast(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(voxelMicrometres)/1000000.0; + const double radiusVoxels=radiusMetres/voxelMetres; + const double spacingVoxels=sampleSpacingMetres/voxelMetres; + const int steps=static_cast(std::ceil(radiusMetres/sampleSpacingMetres)); + const double radiusSquared=radiusMetres*radiusMetres; + std::map 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(point.x), + static_cast(point.y),static_cast(point.z)); + const double carveDensity=radiusVoxels + -std::sqrt(distanceSquared)/voxelMetres; + const double after=std::max(static_cast(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(weighted)/65535.0*sampleVolume}); + return result; + } std::vector World::passageVoidCrossings() const { std::vector 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 materialTileMinimum={ + (static_cast(origin.X)-step)*MaterialSubvoxelsPerVoxel, + (static_cast(origin.Y)-step)*MaterialSubvoxelsPerVoxel, + (static_cast(origin.Z)-step)*MaterialSubvoxelsPerVoxel}; + const std::array materialTileMaximum={ + (static_cast(origin.X)+extent+step)*MaterialSubvoxelsPerVoxel, + (static_cast(origin.Y)+extent+step)*MaterialSubvoxelsPerVoxel, + (static_cast(origin.Z)+extent+step)*MaterialSubvoxelsPerVoxel}; + const auto materialCandidates=tileShape.OreVeinTileCandidates( + materialTileMinimum,materialTileMaximum); + out.diagnostics.materialTileOutsideEveryVein=materialCandidates.empty(); + const double geometryVoxelSizeCm=static_cast(world.CoreVoxelSizeMicrons)/10000.0; + std::uint64_t materialQueries=0,materialCandidateChecks=0; + const auto materialQueryStart=std::chrono::steady_clock::now(); out.vertices.reserve(static_cast(mesh.Vertices.Num())); for(int32 i=0;i(p.X/geometryVoxelSizeCm), + static_cast(p.Y/geometryVoxelSizeCm), + static_cast(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( + std::chrono::steady_clock::now()-materialQueryStart).count():0.0; out.indices.reserve(static_cast(mesh.Triangles.Num())); for(const int32 index:mesh.Triangles)out.indices.push_back(static_cast(index)); if(options.captureDensitySamples)out.debugDensitySamples=std::move(densityValues); @@ -2657,6 +2758,7 @@ namespace VoxelForgeCore { densityHash^=std::bit_cast(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( std::chrono::steady_clock::now()-MeshAssemblyStart).count(); diff --git a/Standalone/Research/WorldAssemblerM3/Linux50/REPORT.md b/Standalone/Research/WorldAssemblerM3/Linux50/REPORT.md new file mode 100644 index 0000000..8855da7 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Linux50/REPORT.md @@ -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). diff --git a/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-approach-wide.png b/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-approach-wide.png new file mode 100644 index 0000000..da12301 Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-approach-wide.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-quartz-eye.png b/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-quartz-eye.png new file mode 100644 index 0000000..70d14fe Binary files /dev/null and b/Standalone/Research/WorldAssemblerM3/Linux50/captures/tunnel-quartz-eye.png differ diff --git a/Standalone/Research/WorldAssemblerM3/Research18/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research18/REPORT.md new file mode 100644 index 0000000..3ee40de --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research18/REPORT.md @@ -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]() · [R3 coil analysis]() + +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. \ No newline at end of file diff --git a/Standalone/Research/WorldAssemblerM3/Research19/REPORT.md b/Standalone/Research/WorldAssemblerM3/Research19/REPORT.md new file mode 100644 index 0000000..fa0e900 --- /dev/null +++ b/Standalone/Research/WorldAssemblerM3/Research19/REPORT.md @@ -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]() and [SurfaceWorld](). 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]()). + +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](), [Linux25 summary]()). 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`](), [`ShapeWorldInternal.h:ClassifyBoxDetailed`]()). 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`](), [`WorldAssemblerInternal.h:find`]()). 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`](). +- 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`](), [mesher call site]()). +- 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`]()). +- 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`]()). 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`](), [Research11]()). + +## 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]()). +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 \ + --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`]()). 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. \ No newline at end of file diff --git a/Standalone/Source/LinuxOffscreenCapture.cpp b/Standalone/Source/LinuxOffscreenCapture.cpp index 03a2a3d..ec306a5 100644 --- a/Standalone/Source/LinuxOffscreenCapture.cpp +++ b/Standalone/Source/LinuxOffscreenCapture.cpp @@ -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(Ids[0]); + Scene.Vertices[I].Material[1]=static_cast(Weights[0])/65535.0f; + Scene.Vertices[I].Material[2]=static_cast(Ids[1]); + Scene.Vertices[I].Material[3]=static_cast(Weights[1])/65535.0f; + (void)Host; + } const double TileVoxels=static_cast(32u<(TileVoxels *static_cast(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(offsetof(FVertex,WorldPosition))); glEnableVertexAttribArray(4);glVertexAttribPointer(4,4,GL_FLOAT,GL_FALSE,sizeof(FVertex), reinterpret_cast(offsetof(FVertex,TileData))); + glEnableVertexAttribArray(5);glVertexAttribPointer(5,4,GL_FLOAT,GL_FALSE,sizeof(FVertex), + reinterpret_cast(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); diff --git a/Standalone/Source/StreamBenchmarkHeadless.cpp b/Standalone/Source/StreamBenchmarkHeadless.cpp index 35c8717..6dd3811 100644 --- a/Standalone/Source/StreamBenchmarkHeadless.cpp +++ b/Standalone/Source/StreamBenchmarkHeadless.cpp @@ -22,6 +22,7 @@ #include #include #include +#include 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& 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(WorldAssemblySubvoxelsPerVoxel); + const double coreMicrometres=layout.coreVoxelSizeMicrometres; + const auto addCapsule=[&](std::string id,std::uint16_t resourceId, + std::array base,std::array tangent, + std::array side,double routeClearRadius, + std::int64_t radius,std::int64_t irregularity,std::int64_t blend, + std::int64_t length) + { + const auto normalize=[](std::array& 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(length)*0.5*tangent[axis]; + request.anchorEndpointsMicrometres[0][axis]=static_cast( + std::llround(base[axis]-half)); + request.anchorEndpointsMicrometres[1][axis]=static_cast( + std::llround(base[axis]+half)); + } + requests.push_back(std::move(request));return true; + }; + std::vector 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{ + 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{7, + "ore/sw-arrival-route-copper-v1",1,1.0}, + std::tuple{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 tangent={b[0]-a[0],b[1]-a[1],b[2]-a[2]}; + std::array side={-tangent[1]*sideSign,tangent[0]*sideSign,0.0}; + std::array 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 weights={one*one*one,3.0*one*one*t, + 3.0*one*t*t,t*t*t}; + const std::array 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 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 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::llround(center[axis]+side[axis]-half)); + request.anchorEndpointsMicrometres[1][axis]=static_cast( + 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(&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 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(vein.boundsMinimumMicrometres[0]), + static_cast(vein.boundsMaximumMicrometres[0]), + static_cast(vein.boundsMinimumMicrometres[1]), + static_cast(vein.boundsMaximumMicrometres[1]), + static_cast(vein.boundsMinimumMicrometres[2]), + static_cast(vein.boundsMaximumMicrometres[2]), + static_cast(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); } diff --git a/Standalone/Source/StreamBenchmarkPipeline.h b/Standalone/Source/StreamBenchmarkPipeline.h index 7e55c0c..0b3187c 100644 --- a/Standalone/Source/StreamBenchmarkPipeline.h +++ b/Standalone/Source/StreamBenchmarkPipeline.h @@ -808,6 +808,7 @@ namespace StandaloneStreamBenchmark { std::ofstream File; bool bGood=true; + bool bIncludeMaterialAttributes=false; std::uint32_t TileCount=0; template 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(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(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(Tile.Indices.data()), static_cast(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& point) + { + const double size=static_cast(World.CoreVoxelSizeMicrons); + return VoxelForgeCore::Vec3d{point[0]/size,point[1]/size,point[2]/size}; + }; + const std::array 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(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(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(World.CoreVoxelSizeMicrons); + } + else if(!ResolvedLayout->passages.empty()) + { + const auto& mouth=ResolvedLayout->passages.front().lowerMouthSubvoxels; + const double divisor=static_cast( + 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(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(Update.DesiredTiles)); + static_cast(Update.DesiredTiles), + ResolvedLayout&&!ResolvedLayout->oreVeins.empty()); if(!bSceneExportPassing) { Streamer.Stop(); @@ -3966,7 +4037,9 @@ namespace StandaloneStreamBenchmark } } - std::vector DensityRows,MeshRows,PayloadRows,TopologyRows; + std::vector 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(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(MaterialQueries), + static_cast(MaterialCandidateChecks), + static_cast(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(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& 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(findHash(MeshIndex,row.Key)), static_cast(findHash(PayloadIndex,row.Key)), static_cast(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(row.Hash)); } std::vector FullDensity,FullMesh,FullPayload,FullTopology; for(const FHashedTile& Row:DensityRows)if(Row.Key.Level(HashTileRows(MeshRows)), static_cast(HashTileRows(PayloadRows)), static_cast(HashTileRows(TopologyRows))); + std::printf("[PipelineMaterialHash] tiles=%zu material=0x%016llx\n", + MaterialRows.size(),static_cast(HashTileRows(MaterialRows))); for(std::size_t LOD=0;LOD& Rows) diff --git a/Standalone/Source/StreamingWorld.h b/Standalone/Source/StreamingWorld.h index b27dd7e..3ea89e7 100644 --- a/Standalone/Source/StreamingWorld.h +++ b/Standalone/Source/StreamingWorld.h @@ -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(Vertex.position.x*0.01),static_cast(Vertex.position.y*0.01), static_cast(Vertex.position.z*0.01)}, {static_cast(Vertex.normal.x),static_cast(Vertex.normal.y), - static_cast(Vertex.normal.z)}}); + static_cast(Vertex.normal.z)}, + Vertex.hostMaterialId,Vertex.resourceMaterialIds,Vertex.resourceWeightsQ16}); Out.Indices=std::move(Result->indices); Out.TriangleSkirtFaceMasks=std::move(Result->skirtFaceMasks); if(bProfile) diff --git a/Standalone/Source/WorldMesher.h b/Standalone/Source/WorldMesher.h index d344f88..43fb5c0 100644 --- a/Standalone/Source/WorldMesher.h +++ b/Standalone/Source/WorldMesher.h @@ -5,10 +5,19 @@ #include "VoxelMarchingCubesMesher.h" #include +#include #include +#include #include -struct FStandaloneCpuVertex { float Position[3]; float Normal[3]; }; +struct FStandaloneCpuVertex +{ + float Position[3]; + float Normal[3]; + std::uint16_t HostMaterialId=1; + std::array ResourceMaterialIds{}; + std::array ResourceWeightsQ16{}; +}; struct FStandaloneCpuLevel { diff --git a/Standalone/Tools/verify/verify.py b/Standalone/Tools/verify/verify.py index 8c6739e..15b504b 100644 --- a/Standalone/Tools/verify/verify.py +++ b/Standalone/Tools/verify/verify.py @@ -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":