From 3b37d5702e0b235c52851b9a1775ccf882fbc733 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 5 Oct 2026 18:17:25 +0200 Subject: [PATCH] standalone: owner review fixes -- paged streaming in Core, no LOD fins, LOD0 stair patches fixed From the owner's fly-through ("very impressive, in some cases slightly better than the original"): - Streaming: Core compiles world-aligned fine-detail pages for tiles entering the view and releases them when they leave; the viewer keeps resident tiles. One chunk crossing: 144 new tiles, 1,822 kept (was a full rebuild). A tile built as a late page hashes identically (density, mesh, payload, topology) to the same tile from an initial build. - LOD>=1 fins: off-surface planar connector panels are now checked against the source surface and projected onto it; coarse audit 0 off-surface panels; far survival passes, bad-shell still fails. - LOD0 stair patches: 2,630 sign-changing edges had one endpoint at the fixed +-4.125 fallback (carve surfaces were excluded from distance storage); distances now completed analytically, 462 packed signs corrected; the audit now fails on any sign edge missing either endpoint (0 of 952,997). - W1/W8 hashes equal; routes 5642/5642; look, passage and far audits pass. Verified by me, interleaved (8 workers, cold): 1.014 s vs 1.008 s for aeeedaa, no regression. Open: a chunk crossing still costs ~1.2 s of page compilation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- Standalone/Core/API.md | 26 ++ .../include/VoxelForgeCore/VoxelForgeCore.h | 14 +- Standalone/Core/src/FarShellsInternal.h | 189 +++++++- Standalone/Core/src/ShapeWorldInternal.h | 185 ++++++++ Standalone/Core/src/VoxelForgeCore.cpp | 148 ++++++- Standalone/Source/StandaloneViewer.cpp | 402 +++++++++++++++++- 6 files changed, 939 insertions(+), 25 deletions(-) diff --git a/Standalone/Core/API.md b/Standalone/Core/API.md index 330e59f..64f6864 100644 --- a/Standalone/Core/API.md +++ b/Standalone/Core/API.md @@ -48,6 +48,32 @@ build.includeFarWorld = true; if (!world.compile(build, hostScheduler, error)) { /* report error */ } ``` +For a streamed shape world, compile the initial camera region once, then add deterministic detail +pages before generating any newly desired fine tile outside that region: + +```cpp +const auto entering = world.desiredTiles(newCameraVoxels); +VoxelForgeCore::TileGenerationOptions options; +std::size_t pageCoveredTiles = 0; +if (!world.compileTileRegion(entering, 8, hostScheduler, error, &pageCoveredTiles)) { + /* report error */ +} +for (const auto& key : entering) { + if (auto mesh = world.generateTile(key, options, error)) { /* upload and retain */ } +} +world.retainTileRegions(entering); +``` + +The Core groups fine tiles into pages using absolute tile coordinates, so a page's contents do not +depend on the camera or the order in which the player reaches it. Pages cover a 128-voxel cube (or +one larger coarse tile), with an 8-voxel sampling apron. `compileTileRegion` skips LODs at or above +`fullResolutionLodCount`, keys covered by the initial `ShapeBuildRequest`, and pages already +resident. Its optional output counts requested fine tile keys that needed a newly compiled page; +it does not count the page's internal lattice samples. After all pages needed by the next desired +set compile, call `retainTileRegions` with that set to release pages no longer needed. The Core +owns page lifetime and mesh data; the host keeps still-visible meshes resident until replacement +tiles are generated and uploaded. + ## Queries and tiles `densityAt(position)` returns signed density; negative is solid and nonnegative is air. diff --git a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h index 2830875..3aee70d 100644 --- a/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h +++ b/Standalone/Core/include/VoxelForgeCore/VoxelForgeCore.h @@ -15,7 +15,11 @@ namespace VoxelForgeCore { struct Vec2f { float x = 0.0f, y = 0.0f; }; struct Vec3d { double x = 0.0, y = 0.0, z = 0.0; }; - struct Vec3i { std::int32_t x = 0, y = 0, z = 0; }; + struct Vec3i + { + std::int32_t x = 0, y = 0, z = 0; + friend bool operator==(const Vec3i&, const Vec3i&) = default; + }; struct TileKey { @@ -196,6 +200,13 @@ namespace VoxelForgeCore bool compile(const ShapeBuildRequest& request, const JobScheduler& scheduler, std::string& error); + // Compile any missing absolute-coordinate detail pages needed by entering fine tiles. + // The optional count is requested tile keys newly covered by compiled pages, not lattice + // samples. Existing pages stay resident until retainTileRegions. + bool compileTileRegion(std::span tiles, std::uint32_t workerLimit, + const JobScheduler& scheduler, std::string& error, + std::size_t* outCompiledTiles = nullptr); + void retainTileRegions(std::span desiredTiles); void buildFarWorld(); bool isCompiled() const noexcept; bool hasFarWorld() const noexcept; @@ -221,6 +232,7 @@ namespace VoxelForgeCore std::vector farConnectorCandidates(Vec3i minVoxel, Vec3i maxVoxel) const; bool auditDetailPassing() const; + bool auditGridStairIntegrity() const; bool auditSurfaceFamilies(bool passageOnly = false) const; bool auditViewSurface(Vec3d cameraVoxel, double yawDegrees, double pitchDegrees) const; bool debugCriticalAirAt(Vec3d voxelPosition) const; diff --git a/Standalone/Core/src/FarShellsInternal.h b/Standalone/Core/src/FarShellsInternal.h index 4dd60e5..7f7b874 100644 --- a/Standalone/Core/src/FarShellsInternal.h +++ b/Standalone/Core/src/FarShellsInternal.h @@ -255,6 +255,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV std::vector NewUVs; std::vector NewColors; std::vector NewTriangles; + std::vector> NewQuads; const int32 OldVertexCount=Mesh.Vertices.Num(); const int32 OldSurfaceVertices=std::clamp(Mesh.NumSurfaceVertices,0,OldVertexCount); const bool bHasUVs=Mesh.UVs.Num()==OldVertexCount; @@ -579,7 +580,7 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV const int32 C=RingVertexIndex[Ring+1][FaceSide]; const int32 D=RingVertexIndex[Ring+1][Next]; // Shared panels are emitted once even when both adjoining radial rays are missing. - NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D}); + NewQuads.push_back({A,B,C,D}); } } } @@ -587,7 +588,191 @@ inline void AppendFarConnectorShells(const FStandaloneShapeWorld& Shape,const FV RunStart=RunEnd+1; } } - if(NewVertices.empty()||NewTriangles.empty())return; + if(NewVertices.empty()||NewQuads.empty())return; + + // The survival repair normally emits long connected strips. A few isolated + // two-triangle panels can be assembled from a radius borrowed at another axial + // sample, though, leaving a free-edged fin away from the sampled density surface. + // Match the visible coarse-panel audit's topology and six density samples here. + std::vector QuadUseCount(NewVertices.size(),0); + for(const auto& Quad:NewQuads) + for(const int32 Vertex:Quad) + if(Vertex>=0&&static_cast(Vertex)(Vertex)]<255) + ++QuadUseCount[static_cast(Vertex)]; + std::vector KeepQuad(NewQuads.size(),1); + std::vector> RefinedQuads; + const double ResidualLimit=std::max(2.0,Step*0.10); + const double TileMin[3]={static_cast(TileOrigin.X),static_cast(TileOrigin.Y), + static_cast(TileOrigin.Z)}; + const double TileMax[3]={TileMin[0]+TileExtent,TileMin[1]+TileExtent,TileMin[2]+TileExtent}; + const auto ToVoxels=[&](int32 Vertex) + { + return FVector(NewVertices[Vertex].X*0.04,NewVertices[Vertex].Y*0.04, + NewVertices[Vertex].Z*0.04); + }; + const auto IsTileBoundaryEdge=[&](const FVector& A,const FVector& B) + { + for(int32 AxisIndex=0;AxisIndex<3;++AxisIndex) + { + const double PA=AxisIndex==0?A.X:(AxisIndex==1?A.Y:A.Z); + const double PB=AxisIndex==0?B.X:(AxisIndex==1?B.Y:B.Z); + if((std::abs(PA-TileMin[AxisIndex])<0.75&&std::abs(PB-TileMin[AxisIndex])<0.75) + ||(std::abs(PA-TileMax[AxisIndex])<0.75&&std::abs(PB-TileMax[AxisIndex])<0.75)) + return true; + } + return false; + }; + for(size_t QuadIndex=0;QuadIndex(Vertex)>=QuadUseCount.size() + ||QuadUseCount[static_cast(Vertex)]!=1;}))continue; + const FVector A=ToVoxels(Quad[0]),B=ToVoxels(Quad[1]); + const FVector C=ToVoxels(Quad[2]),D=ToVoxels(Quad[3]); + const FVector N0=FVector::CrossProduct(C-A,B-A); + const FVector N1=FVector::CrossProduct(C-B,D-B); + const double Area0=N0.Size(),Area1=N1.Size(); + const double AreaSum=Area0+Area1; + if(AreaSum<=1.0e-12)continue; + const double Coherence=(N0+N1).Size()/AreaSum; + if(Coherence<0.995)continue; + if(IsTileBoundaryEdge(A,B)&&IsTileBoundaryEdge(B,D) + &&IsTileBoundaryEdge(D,C)&&IsTileBoundaryEdge(C,A))continue; + const FVector Samples[6]={A,B,C,D,(A+C+B)/3.0,(B+C+D)/3.0}; + int32 OffSourceSamples=0; + for(const FVector& P:Samples) + if(std::abs(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), + static_cast(P.Z)))>ResidualLimit)++OffSourceSamples; + if(OffSourceSamples*4<6)continue; + + constexpr int32 Subdivisions=8; + const FVector PreferredNormal=(NewNormals[Quad[0]]+NewNormals[Quad[1]] + +NewNormals[Quad[2]]+NewNormals[Quad[3]]).GetSafeNormal(); + const auto SourceGradient=[&](const FVector& P) + { + constexpr float H=2.0f; + const float GX=(Shape.DensityAt(static_cast(P.X+H),static_cast(P.Y), + static_cast(P.Z))-Shape.DensityAt(static_cast(P.X-H), + static_cast(P.Y),static_cast(P.Z)))*0.25f; + const float GY=(Shape.DensityAt(static_cast(P.X),static_cast(P.Y+H), + static_cast(P.Z))-Shape.DensityAt(static_cast(P.X), + static_cast(P.Y-H),static_cast(P.Z)))*0.25f; + const float GZ=(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), + static_cast(P.Z+H))-Shape.DensityAt(static_cast(P.X), + static_cast(P.Y),static_cast(P.Z-H)))*0.25f; + return FVector(GX,GY,GZ); + }; + std::vector PatchVertices; + std::vector PatchNormals; + std::vector PatchUVs; + std::vector PatchColors; + const auto ProjectToSurface=[&](FVector& P,FVector& Normal) + { + for(int32 Iteration=0;Iteration<12;++Iteration) + { + const double Density=Shape.DensityAt(static_cast(P.X), + static_cast(P.Y),static_cast(P.Z)); + if(std::abs(Density)<=0.25)break; + FVector Gradient=SourceGradient(P); + const double GradientSquared=Gradient.SizeSquared(); + if(GradientSquared<1.0e-8)return false; + double Distance=std::clamp(Density/GradientSquared,-64.0,64.0); + P-=Gradient*Distance; + } + if(std::abs(Shape.DensityAt(static_cast(P.X),static_cast(P.Y), + static_cast(P.Z)))>2.0)return false; + Normal=SourceGradient(P); + if(Normal.SizeSquared()<1.0e-8)return false; + Normal.Normalize(); + if(PreferredNormal.SizeSquared()>1.0e-8&&FVector::DotProduct(Normal,PreferredNormal)<0.0) + Normal*= -1.0; + return true; + }; + std::vector Grid(static_cast(Subdivisions+1)*(Subdivisions+1),INDEX_NONE); + bool bProjectionSucceeded=true; + for(int32 Y=0;Y<=Subdivisions&&bProjectionSucceeded;++Y) + { + const double V=static_cast(Y)/Subdivisions; + for(int32 X=0;X<=Subdivisions;++X) + { + const double U=static_cast(X)/Subdivisions; + FVector P=A*((1.0-U)*(1.0-V))+B*(U*(1.0-V))+C*((1.0-U)*V)+D*(U*V); + FVector Normal; + if(!ProjectToSurface(P,Normal)){bProjectionSucceeded=false;break;} + const int32 LocalIndex=static_cast(PatchVertices.size()); + Grid[static_cast(Y)*(Subdivisions+1)+X]=LocalIndex; + PatchVertices.emplace_back(P.X*25.0,P.Y*25.0,P.Z*25.0); + PatchNormals.push_back(Normal); + if(bHasUVs) + { + const FVector2f UA=NewUVs[Quad[0]],UB=NewUVs[Quad[1]]; + const FVector2f UC=NewUVs[Quad[2]],UD=NewUVs[Quad[3]]; + PatchUVs.emplace_back(static_cast(UA.X*((1-U)*(1-V))+UB.X*(U*(1-V)) + +UC.X*((1-U)*V)+UD.X*(U*V)), + static_cast(UA.Y*((1-U)*(1-V))+UB.Y*(U*(1-V)) + +UC.Y*((1-U)*V)+UD.Y*(U*V))); + } + if(bHasColors)PatchColors.push_back(NewColors[Quad[0]]); + } + } + if(!bProjectionSucceeded)continue; + + KeepQuad[QuadIndex]=0; + const int32 PatchBase=static_cast(NewVertices.size()); + NewVertices.insert(NewVertices.end(),PatchVertices.begin(),PatchVertices.end()); + NewNormals.insert(NewNormals.end(),PatchNormals.begin(),PatchNormals.end()); + if(bHasUVs)NewUVs.insert(NewUVs.end(),PatchUVs.begin(),PatchUVs.end()); + if(bHasColors)NewColors.insert(NewColors.end(),PatchColors.begin(),PatchColors.end()); + for(int32 Y=0;Y(Y)*(Subdivisions+1)+X]; + const int32 B0=PatchBase+Grid[static_cast(Y)*(Subdivisions+1)+X+1]; + const int32 C0=PatchBase+Grid[static_cast(Y+1)*(Subdivisions+1)+X]; + const int32 D0=PatchBase+Grid[static_cast(Y+1)*(Subdivisions+1)+X+1]; + RefinedQuads.push_back({A0,B0,C0,D0}); + } + } + + std::vector> FinalQuads; + const size_t KeptQuadCount=static_cast(std::count(KeepQuad.begin(),KeepQuad.end(),uint8{1})); + FinalQuads.reserve(KeptQuadCount+RefinedQuads.size()); + for(size_t QuadIndex=0;QuadIndex Remap(NewVertices.size(),INDEX_NONE); + std::vector KeptVertices,KeptNormals; + std::vector KeptUVs; + std::vector KeptColors; + KeptVertices.reserve(FinalQuads.size()*2);KeptNormals.reserve(FinalQuads.size()*2); + if(bHasUVs)KeptUVs.reserve(FinalQuads.size()*2); + if(bHasColors)KeptColors.reserve(FinalQuads.size()*2); + const auto RemapVertex=[&](int32 Vertex) + { + int32& Mapped=Remap[static_cast(Vertex)]; + if(Mapped==INDEX_NONE) + { + Mapped=static_cast(KeptVertices.size()); + KeptVertices.push_back(NewVertices[static_cast(Vertex)]); + KeptNormals.push_back(NewNormals[static_cast(Vertex)]); + if(bHasUVs)KeptUVs.push_back(NewUVs[static_cast(Vertex)]); + if(bHasColors)KeptColors.push_back(NewColors[static_cast(Vertex)]); + } + return Mapped; + }; + NewTriangles.reserve(KeptQuadCount*6); + for(const auto& Quad:FinalQuads) + { + const int32 A=RemapVertex(Quad[0]),B=RemapVertex(Quad[1]); + const int32 C=RemapVertex(Quad[2]),D=RemapVertex(Quad[3]); + NewTriangles.insert(NewTriangles.end(),{A,C,B,B,C,D}); + } + NewVertices=std::move(KeptVertices);NewNormals=std::move(KeptNormals); + if(bHasUVs)NewUVs=std::move(KeptUVs); + if(bHasColors)NewColors=std::move(KeptColors); const int32 AddedVertices=static_cast(NewVertices.size()); auto InsertVertices=[&](auto& Existing,const auto& Added) diff --git a/Standalone/Core/src/ShapeWorldInternal.h b/Standalone/Core/src/ShapeWorldInternal.h index 2923d37..dc59610 100644 --- a/Standalone/Core/src/ShapeWorldInternal.h +++ b/Standalone/Core/src/ShapeWorldInternal.h @@ -319,6 +319,7 @@ public: Stage=Clock::now(); ApplyDetailBand(Stats.DetailBandLines); + Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances(); Stats.SurfaceSeconds=std::chrono::duration(Clock::now()-Stage).count(); ValidatePassageRoutes(); Built=true; @@ -782,6 +783,89 @@ public: return bPassageOnly?bPassageDetail:(Tested>0&&FlatShare<20.0&&bPass&&bPassageDetail); } + bool AuditGridStairIntegrity() const + { + uint64 CheckedEdges=0,MissingAny=0,MissingBoth=0; + std::array,8> Examples{}; + size_t ExampleCount=0; + const auto Record=[&](uint64 EdgeMask,uint64 CandidateA,uint64 CandidateB, + int32 X,int32 Y,int32 Z,int32 Axis) + { + CheckedEdges+=std::popcount(EdgeMask); + const uint64 Missing=EdgeMask&~(CandidateA&CandidateB); + uint64 AnyMissing=Missing; + MissingAny+=std::popcount(Missing); + MissingBoth+=std::popcount(EdgeMask&~(CandidateA|CandidateB)); + while(AnyMissing&&ExampleCount(Bit),Axis}; + } + }; + + const size_t Columns=static_cast(Size[0])*Size[1]; + for(size_t Line=0;Line(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[0]); + const size_t Base=Line*WordsPerZ; + for(size_t W=0;W>63:0; + const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0; + uint64 Valid=(Size[2]-W*64)>=64?~uint64{0} + :((uint64{1}<<(Size[2]-W*64))-1); + if(W==0)Valid&=~uint64{1}; + const uint64 ShiftedRock=(Rock<<1)|PreviousRock; + const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates; + Record((Rock^ShiftedRock)&Valid,Candidates,ShiftedCandidates, + X,Y,Min[2]+static_cast(W*64),2); + + if(X+1(W*64),0); + } + if(Y+1(Size[0])*WordsPerZ; + Record(Rock^RockWords[Other],Candidates,CandidateWords[Other], + X,Y,Min[2]+static_cast(W*64),1); + } + } + } + std::printf("[GridStairDistanceAudit] region=(%d,%d,%d)..(%d,%d,%d) sign_edges=%llu candidate_fallback_edges=%llu both_fallback_edges=%llu status=%s\n", + Min[0],Min[1],Min[2],Max[0],Max[1],Max[2], + static_cast(CheckedEdges), + static_cast(MissingAny), + static_cast(MissingBoth),MissingAny==0?"PASS":"FAIL"); + const auto CandidateAt=[&](int32 X,int32 Y,int32 Z) + { + if(!Contains(X,Y,Z))return false; + const size_t Column=static_cast(Y-Min[1])*Size[0]+static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + return (CandidateWords[Column*WordsPerZ+(Zi>>6)]&(uint64{1}<<(Zi&63u)))!=0; + }; + for(size_t I=0;I(AX),static_cast(AY),static_cast(AZ)); + const float BExact=AnalyticDensityAt(static_cast(BX),static_cast(BY),static_cast(BZ)); + std::printf("[GridStairDistanceCandidate] a=(%d,%d,%d) b=(%d,%d,%d) axis=%d candidate=(%d,%d) solid=(%d,%d) density=(%.4f,%.4f) exact=(%.4f,%.4f) critical_air=(%d,%d)\n", + AX,AY,AZ,BX,BY,BZ,Examples[I][3],CandidateAt(AX,AY,AZ),CandidateAt(BX,BY,BZ), + IsRock(AX,AY,AZ),IsRock(BX,BY,BZ), + AD,BD,AExact,BExact, + IsInsideCriticalAir(AX,AY,AZ),IsInsideCriticalAir(BX,BY,BZ)); + } + return MissingAny==0; + } + bool AuditViewSurface(double CameraX,double CameraY,double CameraZ, float Yaw,float Pitch) const { @@ -2812,6 +2896,107 @@ private: } } + uint64 CompleteSignEdgeDistances() + { + uint64 AddedTotal=0,SignCorrections=0; + const size_t Columns=static_cast(Size[0])*Size[1]; + for(int32 Pass=0;Pass<4;++Pass) + { + uint64 AddedThisPass=0; + bool bSignChanged=false; + const auto EnsureDistance=[&](int32 X,int32 Y,int32 Z) + { + const size_t Column=static_cast(Y-Min[1])*Size[0] + +static_cast(X-Min[0]); + const size_t Zi=static_cast(Z-Min[2]); + const size_t Word=Column*WordsPerZ+(Zi>>6); + const uint64 Mask=uint64{1}<<(Zi&63u); + if(CandidateWords[Word]&Mask)return; + + const float Density=AnalyticDensityAt(static_cast(X), + static_cast(Y),static_cast(Z)); + const size_t Node=(Zi*static_cast(Size[1]) + +static_cast(Y-Min[1]))*Size[0] + +static_cast(X-Min[0]); + StoreSurfaceDistance(Node,Density); + CandidateWords[Word]|=Mask; + ++AddedThisPass; + + const bool bExactRock=Density<0.0f; + const bool bPackedRock=(RockWords[Word]&Mask)!=0; + if(bExactRock!=bPackedRock) + { + if(bExactRock)RockWords[Word]|=Mask; + else RockWords[Word]&=~Mask; + ++SignCorrections; + bSignChanged=true; + } + }; + + for(size_t Line=0;Line(Line%Size[0]); + const int32 Y=Min[1]+static_cast(Line/Size[0]); + const size_t Base=Line*WordsPerZ; + for(size_t W=0;W>63:0; + const uint64 PreviousCandidates=W?CandidateWords[Word-1]>>63:0; + uint64 Valid=(Size[2]-W*64)>=64?~uint64{0} + :((uint64{1}<<(Size[2]-W*64))-1); + if(W==0)Valid&=~uint64{1}; + + const uint64 ShiftedRock=(Rock<<1)|PreviousRock; + const uint64 ShiftedCandidates=(Candidates<<1)|PreviousCandidates; + uint64 Missing=((Rock^ShiftedRock)&Valid) + &~(Candidates&ShiftedCandidates); + while(Missing) + { + const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; + const int32 Z=Min[2]+static_cast(W*64+Bit); + EnsureDistance(X,Y,Z); + EnsureDistance(X,Y,Z-1); + } + + if(X(W*64+Bit); + EnsureDistance(X,Y,Z); + EnsureDistance(X+1,Y,Z); + } + } + if(Y(Size[0])*WordsPerZ; + Missing=(Rock^RockWords[Other])&Valid + &~(Candidates&CandidateWords[Other]); + while(Missing) + { + const uint32 Bit=std::countr_zero(Missing);Missing&=Missing-1; + const int32 Z=Min[2]+static_cast(W*64+Bit); + EnsureDistance(X,Y,Z); + EnsureDistance(X,Y+1,Z); + } + } + } + } + AddedTotal+=AddedThisPass; + if(!bSignChanged)break; + } + std::printf("[GridStairDistanceRepair] exact_endpoint_proxies=%llu occupancy_corrections=%llu\n", + static_cast(AddedTotal), + static_cast(SignCorrections)); + return AddedTotal; + } + void StoreSurfaceDistance(size_t Node,float Density) { int32 Q=static_cast(std::lround(Density*16.0f)); diff --git a/Standalone/Core/src/VoxelForgeCore.cpp b/Standalone/Core/src/VoxelForgeCore.cpp index 9d3255b..83317cb 100644 --- a/Standalone/Core/src/VoxelForgeCore.cpp +++ b/Standalone/Core/src/VoxelForgeCore.cpp @@ -2,6 +2,12 @@ #include "StandaloneSnapshotAdapter.h" #include "VoxelForgeCore/VoxelForgeCore.h" +#include +#include +#include +#include +#include + namespace vfcore::detail { #include "FarShellsInternal.h" @@ -29,6 +35,40 @@ namespace VoxelForgeCore TileKey FromNative(const FVoxelTileKey& value) { return {{value.Coord.X,value.Coord.Y,value.Coord.Z},value.Level}; } + std::int32_t FloorDiv(std::int32_t value,std::int32_t divisor) + { + const std::int32_t quotient=value/divisor,remainder=value%divisor; + return quotient-((remainder!=0&&remainder<0)?1:0); + } + + std::int32_t TilesPerShapePage(std::int32_t lod) + { + constexpr std::int32_t PageWidthVoxels=128; + return std::max(1,(PageWidthVoxels/CHUNK_SIZE)>>std::clamp(lod,0,30)); + } + + TileKey ShapePageKey(const TileKey& tile) + { + TileKey page=tile; + const std::int32_t span=TilesPerShapePage(tile.lod); + page.coordinate={FloorDiv(tile.coordinate.x,span), + FloorDiv(tile.coordinate.y,span),FloorDiv(tile.coordinate.z,span)}; + return page; + } + + bool CoveredByBaseShape(const ShapeBuildRequest& request, + const WorldConfiguration& configuration,const TileKey& tile) + { + if(tile.lod<0||tile.lod>=configuration.fullResolutionLodCount)return false; + const FVoxelTileKey native=ToNative(tile); + const FIntVector origin=native.OriginVoxels(); + const std::int32_t extent=native.ExtentVoxels(); + for(std::int32_t axis=0;axis<3;++axis) + if((&origin.X)[axis]<(&request.minVoxel.x)[axis] + ||(&origin.X)[axis]+extent>(&request.maxVoxel.x)[axis])return false; + return true; + } + WorldConfiguration MakeConfiguration(const FStandaloneOwnerWorld& owner) { WorldConfiguration out; @@ -233,6 +273,10 @@ namespace VoxelForgeCore std::shared_ptr owner; WorldConfiguration configuration; std::unique_ptr shape; + ShapeBuildRequest buildRequest; + mutable std::shared_mutex tileRegionMutex; + std::unordered_map,TileKeyHash> tileRegions; + std::atomic tileRegionCount{0}; std::vector editLog; std::map transactions; bool hasLastSequence=false; @@ -259,10 +303,88 @@ namespace VoxelForgeCore ToNative(request.clipMinVoxel),ToNative(request.clipMaxVoxel), static_cast(std::clamp(request.workerLimit,1,8)),ToNative(request.detail), request.includeFarWorld,error,&scheduler,request.roundTunnels,request.profileDetailBand); - if(ok)impl_->shape=std::move(shape); + if(ok) + { + impl_->shape=std::move(shape); + impl_->buildRequest=request; + std::unique_lock lock(impl_->tileRegionMutex); + impl_->tileRegions.clear(); + impl_->tileRegionCount.store(0,std::memory_order_release); + } return ok; } + bool World::compileTileRegion(std::span tiles,std::uint32_t workerLimit, + const JobScheduler& scheduler,std::string& error, + std::size_t* outCompiledTiles) + { + if(outCompiledTiles)*outCompiledTiles=0; + if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()) + {error="the base world must be compiled before adding tile regions";return false;} + if(tiles.empty())return true; + + std::unordered_map,TileKeyHash> pendingPages; + pendingPages.reserve(tiles.size()); + { + std::shared_lock lock(impl_->tileRegionMutex); + for(const TileKey& key:tiles) + { + if(key.lod<0||key.lod>8) + {error="tile region contains an invalid LOD";return false;} + if(key.lod>=impl_->configuration.fullResolutionLodCount + ||CoveredByBaseShape(impl_->buildRequest,impl_->configuration,key))continue; + const TileKey pageKey=ShapePageKey(key); + if(!impl_->tileRegions.contains(pageKey))pendingPages[pageKey].push_back(key); + } + } + if(pendingPages.empty())return true; + + std::size_t compiledTileCount=0; + for(const auto& [pageKey,requestedTiles]:pendingPages) + { + const std::int32_t tileCount=TilesPerShapePage(pageKey.lod); + const std::int32_t tileExtent=CHUNK_SIZE<(); + const ShapeBuildRequest& base=impl_->buildRequest; + if(!page->Build(*impl_->owner,fineMin,fineMax,pageMin,pageMax, + static_cast(std::clamp(workerLimit,1,8)), + ToNative(base.detail),false,error,&scheduler, + base.roundTunnels,base.profileDetailBand))return false; + if(!impl_->editLog.empty())page->SetEditTransaction(impl_->editLog); + + { + std::unique_lock lock(impl_->tileRegionMutex); + impl_->tileRegions.try_emplace(pageKey,std::move(page)); + impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release); + } + compiledTileCount+=requestedTiles.size(); + } + if(outCompiledTiles)*outCompiledTiles=compiledTileCount; + return true; + } + + void World::retainTileRegions(std::span desiredTiles) + { + if(!impl_)return; + std::unordered_set desired; + desired.reserve(desiredTiles.size()*2+1); + for(const TileKey& key:desiredTiles) + if(key.lod>=0&&key.lodconfiguration.fullResolutionLodCount) + desired.insert(ShapePageKey(key)); + std::unique_lock lock(impl_->tileRegionMutex); + std::erase_if(impl_->tileRegions,[&](const auto& entry){return !desired.contains(entry.first);}); + impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release); + } + void World::buildFarWorld() { if(impl_->shape)impl_->shape->BuildFarWorld(); } bool World::isCompiled() const noexcept { return impl_&&impl_->shape&&impl_->shape->IsBuilt(); } @@ -357,9 +479,17 @@ namespace VoxelForgeCore if(!impl_||!impl_->owner||!impl_->shape||!impl_->shape->IsBuilt()) { error="world must be compiled before generating tiles";return std::nullopt; } const auto& world=*impl_->owner; + std::shared_ptr page; + if(impl_->tileRegionCount.load(std::memory_order_acquire)!=0) + { + std::shared_lock lock(impl_->tileRegionMutex); + if(const auto found=impl_->tileRegions.find(ShapePageKey(key));found!=impl_->tileRegions.end()) + page=found->second; + } + const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape; UVoxelMarchingCubesMesher mesher; mesher.SetGenerator(world.Generator.get()); - mesher.SetStandaloneShapeWorld(impl_->shape.get()); + mesher.SetStandaloneShapeWorld(&tileShape); mesher.bStandaloneSkipUvAndColors=options.skipUvAndColors; mesher.bStandaloneSkipUV=options.skipUv;mesher.bStandaloneSkipColors=options.skipColors; mesher.bStandaloneSkipMaterialGroups=options.skipMaterialGroups; @@ -374,10 +504,11 @@ namespace VoxelForgeCore FVoxelMeshData mesh=mesher.GenerateMesh(origin,step,cells,nullptr, options.bandMinZVoxel,options.bandMaxZVoxel,&sampleCount,nullptr,nullptr, (options.hashDensity||options.captureDensitySamples)?&densityValues:nullptr); - if(impl_->shape->HasFarWorld()) + const FStandaloneShapeWorld& farShape=tileShape.HasFarWorld()?tileShape:*impl_->shape; + if(farShape.HasFarWorld()) { - AppendFarStratumShells(world,*impl_->shape,nativeKey,step,mesh); - AppendFarConnectorShells(*impl_->shape,nativeKey,step,world.CoarseTileCells,mesh); + AppendFarStratumShells(world,farShape,nativeKey,step,mesh); + AppendFarConnectorShells(farShape,nativeKey,step,world.CoarseTileCells,mesh); } TileMesh out;out.key=key;out.densitySampleCount=static_cast(std::max(0,sampleCount)); @@ -451,6 +582,11 @@ namespace VoxelForgeCore } impl_->editLog.insert(impl_->editLog.end(),native.begin(),native.end()); impl_->shape->SetEditTransaction(impl_->editLog); + { + std::shared_lock lock(impl_->tileRegionMutex); + for(const auto& [key,page]:impl_->tileRegions) + if(page)page->SetEditTransaction(impl_->editLog); + } const FIntVector min=impl_->shape->GetEditBoundsMin(),max=impl_->shape->GetEditBoundsMax(); EditResult result;result.digest=digest; result.affectedByLod.resize(static_cast(std::max(impl_->configuration.maxLod,0)+1)); @@ -487,6 +623,8 @@ namespace VoxelForgeCore } bool World::auditDetailPassing() const { return impl_&&impl_->shape&&impl_->shape->IsDetailAuditPassing(); } + bool World::auditGridStairIntegrity() const + { return impl_&&impl_->shape&&impl_->shape->AuditGridStairIntegrity(); } bool World::auditSurfaceFamilies(bool passageOnly) const { return impl_&&impl_->shape&&impl_->shape->AuditSurfaceBandFamilies(passageOnly); } bool World::auditViewSurface(Vec3d camera,double yaw,double pitch) const diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 7996a6a..453a880 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -207,6 +207,170 @@ namespace return Result; } + struct FCoarsePanelAudit + { + uint64 Tiles=0,PlanarOpenComponents=0,OffSurfacePanels=0,Triangles=0; + double MaximumSourceErrorVoxels=0.0; + }; + + FCoarsePanelAudit AuditCoarseSurfacePanels(const FStandaloneCpuTile& Tile, + const VoxelForgeCore::World& Shape,const VoxelForgeCore::WorldConfiguration& Config, + bool bPrintCandidates) + { + FCoarsePanelAudit Result; + const size_t VertexCount=Tile.Vertices.size(); + const size_t TriangleCount=Tile.Indices.size()/3; + if(Tile.Key.Level<1||VertexCount==0||TriangleCount==0)return Result; + std::vector Parent(VertexCount); + std::vector Rank(VertexCount,0); + std::iota(Parent.begin(),Parent.end(),0u); + const auto Find=[&](uint32 Value) + { + uint32 Root=Value; + while(Parent[Root]!=Root)Root=Parent[Root]; + while(Parent[Value]!=Value) + {const uint32 Next=Parent[Value];Parent[Value]=Root;Value=Next;} + return Root; + }; + const auto Union=[&](uint32 A,uint32 B) + { + A=Find(A);B=Find(B);if(A==B)return; + if(Rank[A]B)std::swap(A,B);return (static_cast(A)<<32)|B;}; + std::unordered_map EdgeUse; + EdgeUse.reserve(TriangleCount*3/2+1); + for(size_t T=0;T=VertexCount||B>=VertexCount||C>=VertexCount)continue; + Union(A,B);Union(B,C); + for(const auto [U,V]:std::array,3>{{{A,B},{B,C},{C,A}}}) + { + uint8& Count=EdgeUse[EdgeKey(U,V)]; + if(Count<255)++Count; + } + } + struct FComponent + { + uint64 Triangles=0,InteriorFreeEdges=0,SourceSamples=0,OffSourceSamples=0; + double Normal[3]={0,0,0},NormalWeight=0,MaximumSourceError=0; + double WorstPosition[3]={0,0,0}; + bool bWorstTriangleCentroid=false; + }; + std::vector Components(VertexCount); + for(size_t T=0;T=VertexCount||B>=VertexCount||C>=VertexCount)continue; + const uint32 Root=Find(A);FComponent& Stats=Components[Root];++Stats.Triangles; + const auto& PA=Tile.Vertices[A].Position;const auto& PB=Tile.Vertices[B].Position; + const auto& PC=Tile.Vertices[C].Position; + const double U[3]={PB[0]-PA[0],PB[1]-PA[1],PB[2]-PA[2]}; + const double V[3]={PC[0]-PA[0],PC[1]-PA[1],PC[2]-PA[2]}; + double N[3]={U[1]*V[2]-U[2]*V[1],U[2]*V[0]-U[0]*V[2],U[0]*V[1]-U[1]*V[0]}; + const double Area2=std::sqrt(N[0]*N[0]+N[1]*N[1]+N[2]*N[2]); + if(Area2>1.0e-12) + { + for(int32 Axis=0;Axis<3;++Axis)Stats.Normal[Axis]+=N[Axis]/Area2*Area2; + Stats.NormalWeight+=Area2; + } + } + const FIntVector Origin=Tile.Key.OriginVoxels(); + const double TileMin[3]={static_cast(Origin.X),static_cast(Origin.Y),static_cast(Origin.Z)}; + const double TileMax[3]={TileMin[0]+Tile.Key.ExtentVoxels(),TileMin[1]+Tile.Key.ExtentVoxels(),TileMin[2]+Tile.Key.ExtentVoxels()}; + const auto IsTileBoundaryEdge=[&](uint32 A,uint32 B) + { + for(int32 Axis=0;Axis<3;++Axis) + { + const double PA=Tile.Vertices[A].Position[Axis]*4.0; + const double PB=Tile.Vertices[B].Position[Axis]*4.0; + if((std::abs(PA-TileMin[Axis])<0.75&&std::abs(PB-TileMin[Axis])<0.75) + ||(std::abs(PA-TileMax[Axis])<0.75&&std::abs(PB-TileMax[Axis])<0.75))return true; + } + return false; + }; + for(const auto& [Key,Count]:EdgeUse) + { + if(Count!=1)continue; + const uint32 A=static_cast(Key>>32),B=static_cast(Key); + if(A>=VertexCount||B>=VertexCount||IsTileBoundaryEdge(A,B))continue; + ++Components[Find(A)].InteriorFreeEdges; + } + const int32 Extent=Tile.Key.ExtentVoxels(); + const int32 Cells=Tile.Key.Level Candidate(VertexCount,0); + for(size_t Root=0;Root256||Stats.InteriorFreeEdges==0||Stats.NormalWeight<=0.0)continue; + const double NormalLength=std::sqrt(Stats.Normal[0]*Stats.Normal[0] + +Stats.Normal[1]*Stats.Normal[1]+Stats.Normal[2]*Stats.Normal[2]); + const double Coherence=NormalLength/Stats.NormalWeight; + if(Coherence<0.995)continue; + Candidate[Root]=1;++Result.PlanarOpenComponents; + } + if(Result.PlanarOpenComponents==0)return Result; + for(size_t Vertex=0;Vertex(Vertex)); + if(!Candidate[Root])continue; + const auto& P=Tile.Vertices[Vertex].Position; + const double Error=std::abs(Shape.densityAt({P[0]*4.0,P[1]*4.0,P[2]*4.0})); + FComponent& Stats=Components[Root];++Stats.SourceSamples; + if(Error>Stats.MaximumSourceError) + { + Stats.MaximumSourceError=Error; + std::copy_n(P,3,Stats.WorstPosition); + Stats.bWorstTriangleCentroid=false; + } + if(Error>ResidualLimit)++Stats.OffSourceSamples; + } + for(size_t T=0;T=VertexCount||B>=VertexCount||C>=VertexCount)continue; + const uint32 Root=Find(A);if(!Candidate[Root])continue; + const auto& PA=Tile.Vertices[A].Position;const auto& PB=Tile.Vertices[B].Position; + const auto& PC=Tile.Vertices[C].Position; + const double Error=std::abs(Shape.densityAt({(PA[0]+PB[0]+PC[0])*4.0/3.0, + (PA[1]+PB[1]+PC[1])*4.0/3.0,(PA[2]+PB[2]+PC[2])*4.0/3.0})); + FComponent& Stats=Components[Root];++Stats.SourceSamples; + if(Error>Stats.MaximumSourceError) + { + Stats.MaximumSourceError=Error; + Stats.WorstPosition[0]=(PA[0]+PB[0]+PC[0])/3.0; + Stats.WorstPosition[1]=(PA[1]+PB[1]+PC[1])/3.0; + Stats.WorstPosition[2]=(PA[2]+PB[2]+PC[2])/3.0; + Stats.bWorstTriangleCentroid=true; + } + if(Error>ResidualLimit)++Stats.OffSourceSamples; + } + for(size_t Root=0;Root(Stats.OffSourceSamples)/Stats.SourceSamples; + Result.MaximumSourceErrorVoxels=std::max(Result.MaximumSourceErrorVoxels,Stats.MaximumSourceError); + if(OffShare<0.25)continue; + ++Result.OffSurfacePanels;Result.Triangles+=Stats.Triangles; + if(bPrintCandidates&&Result.OffSurfacePanels<=8) + std::printf("[CoarsePanelCandidate] lod=%d tile=(%d,%d,%d) triangles=%llu interior_free_edges=%llu source_error=%.3f vox off_share=%.1f%% limit=%.3f vox worst=(%.2f,%.2f,%.2f)m sample=%s\n", + Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z, + static_cast(Stats.Triangles), + static_cast(Stats.InteriorFreeEdges),Stats.MaximumSourceError, + OffShare*100.0,ResidualLimit, + Stats.WorstPosition[0],Stats.WorstPosition[1],Stats.WorstPosition[2], + Stats.bWorstTriangleCentroid?"centroid":"vertex"); + } + Result.Tiles=1; + return Result; + } + struct M4 { float M[4][4]{}; }; M4 Identity(){M4 R{};for(int I=0;I<4;++I)R.M[I][I]=1.0f;return R;} M4 Transpose(M4 A){M4 B{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)B.M[I][J]=A.M[J][I];return B;} @@ -277,6 +441,8 @@ namespace uint64 Tiles=0; uint64 Samples=0; double WorkerSeconds=0.0; + double CorePageSeconds=0.0; + uint64 PageCoveredTiles=0; std::chrono::steady_clock::time_point StartTime{}; bool bReported=false; }; @@ -357,6 +523,7 @@ namespace V3 CameraPositionOverride{}; float CameraYawOverride=0.0f,CameraPitchOverride=0.0f; std::unordered_map ResidentTiles; + std::unordered_set DeferredLeaverTiles; std::unordered_map,FStandaloneBatchKeyHash> BatchMembers; std::unordered_map StreamingBatches; std::unordered_set DirtyBatchSet; @@ -441,6 +608,7 @@ namespace } StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear(); DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear(); + DeferredLeaverTiles.clear(); for(int32 I=0;I<=MaxLevels;++I) {DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0; std::fill(std::begin(ProfileCyclesByLevel[I]),std::end(ProfileCyclesByLevel[I]),0); @@ -1019,25 +1187,69 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; if(!World||!Streamer)return; const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0); FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels); - if(NewDesired.CenterChunk==BuildTargetChunk)return; - if(bShapeWorld) + if(bShapeWorld&&ShapeWorld) { - bRebuildPending=true; - std::printf("[ShapeRegion] camera crossed its compiled full-detail bounds; rebuilding the interval region.\n"); - return; + const auto CoreTiles=ShapeWorld->desiredTiles( + {CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z}); + NewDesired.Tiles.clear();NewDesired.Tiles.reserve(CoreTiles.size()); + for(const auto& Key:CoreTiles) + NewDesired.Tiles.emplace_back( + FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod); } + if(NewDesired.CenterChunk==BuildTargetChunk)return; const FIntVector Previous=BuildTargetChunk; std::unordered_set NewSet; NewSet.reserve(NewDesired.Tiles.size()*2+1); for(const FVoxelTileKey& Key:NewDesired.Tiles)NewSet.insert(Key); std::vector Leavers; for(const auto& Pair:ResidentTiles)if(!NewSet.contains(Pair.first))Leavers.push_back(Pair.first); - for(const FVoxelTileKey& Key:Leavers)RemoveResidentTile(Key); + const auto CrossingStart=std::chrono::steady_clock::now(); + double PageCompileSeconds=0.0; + uint64 PageCoveredTiles=0; + if(bShapeWorld&&ShapeWorld) + { + std::vector NewCoreTiles; + NewCoreTiles.reserve(NewDesired.Tiles.size()); + for(const FVoxelTileKey& Key:NewDesired.Tiles) + if(!ResidentTiles.contains(Key)) + NewCoreTiles.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level}); + std::string PageError; + size_t CompiledPageCount=0; + if(!ShapeWorld->compileTileRegion(NewCoreTiles, + static_cast(std::min(4,WorkerCount)),CoreScheduler,PageError, + &CompiledPageCount)) + { + std::fprintf(stderr,"[ShapeRegionPage] compile failed at center=(%d,%d,%d): %s\n", + NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z, + PageError.c_str()); + return; + } + PageCoveredTiles=static_cast(CompiledPageCount); + std::vector CoreDesired; + CoreDesired.reserve(NewDesired.Tiles.size()); + for(const FVoxelTileKey& Key:NewDesired.Tiles) + CoreDesired.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level}); + ShapeWorld->retainTileRegions(CoreDesired); + for(const FVoxelTileKey& Key:Leavers)DeferredLeaverTiles.insert(Key); + const double PageSeconds=std::chrono::duration( + std::chrono::steady_clock::now()-CrossingStart).count(); + PageCompileSeconds=PageSeconds; + std::printf("[ShapeRegionPage] center=(%d,%d,%d) requested_tiles=%zu page_covered_tiles=%llu worker_limit=%d seconds=%.4f retained_resident=%zu deferred_leavers=%zu\n", + NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z, + NewCoreTiles.size(),static_cast(PageCoveredTiles), + std::min(4,WorkerCount),PageSeconds, + ResidentTiles.size()-std::min(ResidentTiles.size(),Leavers.size()),Leavers.size()); + } + else + { + for(const FVoxelTileKey& Key:Leavers)RemoveResidentTile(Key); + } DesiredTiles=std::move(NewDesired.Tiles); for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0; for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level]; std::vector ResidentKeys;ResidentKeys.reserve(ResidentTiles.size()); - for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first); + for(const auto& Pair:ResidentTiles) + if(NewSet.contains(Pair.first))ResidentKeys.push_back(Pair.first); ++CrossingIndex; const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex); for(const auto& Pair:Update.CancelledByCrossing) @@ -1048,12 +1260,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } FCrossingWork& Work=CrossingWork[CrossingIndex]; Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs; - Work.StartTime=std::chrono::steady_clock::now(); + Work.StartTime=CrossingStart; + Work.PageCoveredTiles=PageCoveredTiles;Work.CorePageSeconds=PageCompileSeconds; BuildTargetChunk=Update.CenterChunk;bGenerationRunning=true; - std::printf("[ChunkCrossing] id=%llu center=(%d,%d,%d) delta=(%d,%d,%d) desired=%d retained=%d new_jobs=%d dropped_resident=%zu cancelled_queued=%d full_detail=%d\n", + std::printf("[ChunkCrossing] id=%llu center=(%d,%d,%d) delta=(%d,%d,%d) desired=%d retained=%d new_jobs=%d deferred_resident=%zu cancelled_queued=%d full_detail=%d\n", static_cast(CrossingIndex),Update.CenterChunk.X,Update.CenterChunk.Y,Update.CenterChunk.Z, Update.CenterChunk.X-Previous.X,Update.CenterChunk.Y-Previous.Y,Update.CenterChunk.Z-Previous.Z, - Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,Leavers.size(), + Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs, + bShapeWorld?Leavers.size():0, Update.CancelledQueuedJobs,Update.FullResTiles); std::fflush(stdout); ReportCrossingWork(); @@ -1157,7 +1371,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; { if(!Streamer||World->FullResClipLevels<=0)return false; int32 ResidentFullRes=0; - for(const auto& Pair:ResidentTiles)if(Pair.first.LevelFullResClipLevels)++ResidentFullRes; + for(const FVoxelTileKey& Key:DesiredTiles) + if(Key.LevelFullResClipLevels&&ResidentTiles.contains(Key))++ResidentFullRes; if(ResidentFullRes!=Streamer->FullResDesiredCount())return false; for(const FStandaloneBatchKey& Key:DirtyBatchSet)if(Key.LevelFullResClipLevels)return false; return true; @@ -1203,14 +1418,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; ?Work.WorkerSeconds*1000.0/static_cast(Work.FinishedJobs):0.0; const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0 ?std::chrono::duration(std::chrono::steady_clock::now()-Work.StartTime).count():0.0; - std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM worker_sum=%.3fs wall=%.3fs avg=%.3fms\n", + std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM page_covered_tiles=%llu core_page=%.4fs worker_sum=%.3fs wall=%.3fs avg=%.3fms\n", static_cast(Pair.first), static_cast(Work.SubmittedJobs), static_cast(Work.FinishedJobs), static_cast(Work.Tiles), static_cast(Work.CancelledQueuedJobs), static_cast(Work.StaleResults), - static_cast(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs); + static_cast(Work.Samples)/1.0e6, + static_cast(Work.PageCoveredTiles),Work.CorePageSeconds, + Work.WorkerSeconds,WallSeconds,AverageMs); } } @@ -1240,6 +1457,23 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } ReportCrossingWork(); UploadDirtyBatches(); + if(!DeferredLeaverTiles.empty()&&DirtyBatchSet.empty()) + { + bool bDesiredResident=true; + for(const FVoxelTileKey& Key:DesiredTiles) + if(!ResidentTiles.contains(Key)){bDesiredResident=false;break;} + if(bDesiredResident) + { + std::vector ReadyToDrop; + for(const FVoxelTileKey& Key:DeferredLeaverTiles) + { + if(Streamer->IsDesired(Key))continue; + if(ResidentTiles.contains(Key))ReadyToDrop.push_back(Key); + } + for(const FVoxelTileKey& Key:ReadyToDrop)RemoveResidentTile(Key); + DeferredLeaverTiles.clear(); + } + } if(bShapePipelineActive)TryCompleteShapePipeline(); if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted &&DirtyBatchSet.empty()) @@ -1262,17 +1496,20 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; ReportTileTimings();std::fflush(stdout); StartFarShapeGeneration(); } - if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty()) + size_t DesiredResidentCount=0; + for(const FVoxelTileKey& Key:DesiredTiles) + if(ResidentTiles.contains(Key))++DesiredResidentCount; + if(!bInitialClipmapReported&&DesiredResidentCount==DesiredTiles.size()&&DirtyBatchSet.empty()) { bInitialClipmapReported=true; const double Elapsed=std::chrono::duration(std::chrono::steady_clock::now()-ColdProcessStart).count(); std::printf("[InitialClipmapReady] %.3f s after cold process start; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n", - Elapsed,ResidentTiles.size(),static_cast(TotalVertices), + Elapsed,DesiredResidentCount,static_cast(TotalVertices), static_cast(TotalIndices),StreamingBatches.size()); ReportTileTimings();std::fflush(stdout); if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0); } - bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty(); + bGenerationRunning=DesiredResidentCount!=DesiredTiles.size()||!DirtyBatchSet.empty(); } void CaptureRenderedFrame() @@ -2207,6 +2444,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; double FullDetailSeconds=0.0; const int32 DesiredTotal=Update.DesiredTiles; FMeshSheetAudit MeshSheetStats; + FCoarsePanelAudit CoarsePanelStats; uint64 MeshAuditedTiles=0; while(Finished=1&&CoreWorld) + { + const FCoarsePanelAudit TileAudit=AuditCoarseSurfacePanels( + Tile,*CoreWorld,CoreWorld->configuration(),CoarsePanelStats.OffSurfacePanels<8); + CoarsePanelStats.Tiles+=TileAudit.Tiles; + CoarsePanelStats.PlanarOpenComponents+=TileAudit.PlanarOpenComponents; + CoarsePanelStats.OffSurfacePanels+=TileAudit.OffSurfacePanels; + CoarsePanelStats.Triangles+=TileAudit.Triangles; + CoarsePanelStats.MaximumSourceErrorVoxels=std::max( + CoarsePanelStats.MaximumSourceErrorVoxels,TileAudit.MaximumSourceErrorVoxels); + } if(!BakeCachePath.empty())CacheTiles.push_back(std::move(Tile)); } if(!bFullDetailTimeReported&&FullDetailFinished>=FullDetailExpected) @@ -2350,6 +2599,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; static_cast(MeshSheetStats.ThinSheets), static_cast(MeshSheetStats.TrianglesInThinSheets), MeshSheetStats.ThinSheets==0&&MeshSheetStats.SmallComponentsInView==0?"PASS":"FAIL"); + if(bShardAudit&&CoreWorld) + std::printf("[CoarsePanelAudit] tiles=%llu planar_open_components=%llu off_surface_panels=%llu panel_triangles=%llu max_source_error=%.3f vox status=%s\n", + static_cast(CoarsePanelStats.Tiles), + static_cast(CoarsePanelStats.PlanarOpenComponents), + static_cast(CoarsePanelStats.OffSurfacePanels), + static_cast(CoarsePanelStats.Triangles), + CoarsePanelStats.MaximumSourceErrorVoxels, + CoarsePanelStats.OffSurfacePanels==0?"PASS":"FAIL"); for(int32 Level=0;Level<=MaxLevels;++Level) { if(TilesByLevel[Level]==0)continue; @@ -2392,6 +2649,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; } if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6; if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13; + if(bShardAudit&&CoreWorld&&CoarsePanelStats.OffSurfacePanels!=0)return 13; if(FarAudit) { FarAudit->Finalize(); @@ -2483,6 +2741,106 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75; AffectedByLevel[Level]==FinishedByLevel[Level]?"PASS":"FAIL"); return Finished==static_cast(Affected.size())?0:10; } + + bool MakeRegionBuildRequest(std::span tiles, + const VoxelForgeCore::WorldConfiguration& config,int32 workers, + VoxelForgeCore::DetailVariant detail,bool roundTunnels,bool enhancedWalls, + VoxelForgeCore::ShapeBuildRequest& request,std::string& error) + { + FIntVector fineMin(MAX_int32,MAX_int32,MAX_int32),fineMax(MIN_int32,MIN_int32,MIN_int32); + FIntVector clipMin(MAX_int32,MAX_int32,MAX_int32),clipMax(MIN_int32,MIN_int32,MIN_int32); + for(const auto& key:tiles) + { + if(key.lod<0||key.lod>8){error="desired tile has an invalid LOD";return false;} + const int32 extent=CHUNK_SIZE<(static_cast(key.coordinate.x)*extent), + static_cast(static_cast(key.coordinate.y)*extent), + static_cast(static_cast(key.coordinate.z)*extent)); + for(int32 axis=0;axis<3;++axis) + { + (&clipMin.X)[axis]=std::min((&clipMin.X)[axis],(&origin.X)[axis]); + (&clipMax.X)[axis]=std::max((&clipMax.X)[axis],(&origin.X)[axis]+extent); + if(key.lod(std::clamp(workers,1,8)); + request.detail=detail;request.roundTunnels=roundTunnels; + request.enhancedTunnelWalls=enhancedWalls;request.includeFarWorld=false; + return true; + } + + int RunTilePathHashAudit(VoxelForgeCore::World& base, + const VoxelForgeCore::ResolvedWorldSnapshot& snapshot, + const VoxelForgeCore::JobScheduler& scheduler,const FVector& camera, + int32 workers,VoxelForgeCore::DetailVariant detail,bool roundTunnels, + bool enhancedWalls) + { + const auto baseTiles=base.desiredTiles({camera.X,camera.Y,camera.Z}); + const auto& config=base.configuration(); + if(baseTiles.empty()) + {std::fprintf(stderr,"[TilePathHashAudit] camera window is empty.\n");return 11;} + constexpr int32 TargetChunkOffset=40; + const int32 CameraChunkX=FMath::FloorToInt(camera.X/CHUNK_SIZE); + const int32 CameraChunkY=FMath::FloorToInt(camera.Y/CHUNK_SIZE); + const int32 CameraChunkZ=FMath::FloorToInt(camera.Z/CHUNK_SIZE); + const VoxelForgeCore::TileKey target{{CameraChunkX+TargetChunkOffset, + CameraChunkY,CameraChunkZ},0}; + const FVector alternateCamera((target.coordinate.x+0.5)*CHUNK_SIZE,camera.Y,camera.Z); + const auto alternateTiles=base.desiredTiles( + {alternateCamera.X,alternateCamera.Y,alternateCamera.Z}); + if(alternateTiles.empty()) + {std::fprintf(stderr,"[TilePathHashAudit] target camera window is empty.\n");return 11;} + + VoxelForgeCore::TileGenerationOptions options;options.hashDensity=true; + std::string error; + const std::array pageTiles={target}; + std::size_t pageTilesA=0; + if(!base.compileTileRegion(pageTiles,static_cast(std::clamp(workers,1,8)), + scheduler,error,&pageTilesA)) + {std::fprintf(stderr,"[TilePathHashAudit] path-A page compile failed: %s\n",error.c_str());return 11;} + auto pathA=base.generateTile(target,options,error); + if(!pathA){std::fprintf(stderr,"[TilePathHashAudit] path-A generation failed: %s\n",error.c_str());return 11;} + + VoxelForgeCore::World alternate(snapshot); + VoxelForgeCore::ShapeBuildRequest alternateRequest; + if(!MakeRegionBuildRequest(alternateTiles,config,workers,detail,roundTunnels, + enhancedWalls,alternateRequest,error) + ||!alternate.compile(alternateRequest,scheduler,error)) + {std::fprintf(stderr,"[TilePathHashAudit] alternate camera compile failed: %s\n",error.c_str());return 11;} + std::size_t pageTilesB=0; + auto pathB=alternate.generateTile(target,options,error); + if(!pathB){std::fprintf(stderr,"[TilePathHashAudit] path-B generation failed: %s\n",error.c_str());return 11;} + const auto Same=[&](const VoxelForgeCore::TileMesh& a,const VoxelForgeCore::TileMesh& b) + { + return a.densityHash==b.densityHash&&a.meshHash==b.meshHash + &&a.payloadHash==b.payloadHash&&a.topologyHash==b.topologyHash; + }; + const int32 CameraDeltaX=static_cast(std::lround(alternateCamera.X-camera.X)); + const bool pass=pageTilesA==1&&pageTilesB==0&&Same(*pathA,*pathB); + std::printf("[TilePathHashAudit] tile=(%d,%d,%d) lod=%d path_a=late_page path_b=initial_base camera_delta_vox=(%d,0,0) page_covered_tiles=(%zu,%zu) density=(0x%016llx,0x%016llx) mesh=(0x%016llx,0x%016llx) payload=(0x%016llx,0x%016llx) topology=(0x%016llx,0x%016llx) status=%s\n", + target.coordinate.x,target.coordinate.y,target.coordinate.z,target.lod,CameraDeltaX, + pageTilesA,pageTilesB, + static_cast(pathA->densityHash), + static_cast(pathB->densityHash), + static_cast(pathA->meshHash), + static_cast(pathB->meshHash), + static_cast(pathA->payloadHash), + static_cast(pathB->payloadHash), + static_cast(pathA->topologyHash), + static_cast(pathB->topologyHash),pass?"PASS":"FAIL"); + return pass?0:12; + } } int main(int Argc,char** Argv) @@ -2500,6 +2858,8 @@ int main(int Argc,char** Argv) bool bStreamPipeline=false; bool bProfileDetailBand=false; bool bPassageDetailAuditOnly=false; + bool bGridStairAudit=false; + bool bTilePathHashAudit=false; bool bShardAudit=false; bool bFarSurvivalAudit=false; bool bFarAuditBadShell=false; @@ -2523,6 +2883,8 @@ int main(int Argc,char** Argv) else if(Arg=="--density-hash")App.bDensityHash=true; else if(Arg=="--shape-world")App.bShapeWorld=true; else if(Arg=="--shape-audit")App.bShapeWorld=true,App.bShapeAuditOnly=true; + else if(Arg=="--grid-stair-audit")bGridStairAudit=true,App.bShapeWorld=true; + else if(Arg=="--tile-path-hash-audit")bTilePathHashAudit=true,App.bShapeWorld=true; else if(Arg=="--passage-wall-detail-audit") {App.bShapeWorld=true;App.bShapeAuditOnly=true;bPassageDetailAuditOnly=true;} else if(Arg=="--shape-compare")App.bShapeWorld=true,App.bShapeCompare=true; @@ -2636,7 +2998,7 @@ int main(int Argc,char** Argv) ?(StreamBenchmarkMaxLevel<0||StreamBenchmarkMaxLevel>=App.World->FullResClipLevels) :(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels :(BenchmarkLevel>=App.World->FullResClipLevels)); - if(App.bShapeWorld&&(App.bShapeAuditOnly||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0) + if(App.bShapeWorld&&(App.bShapeAuditOnly||bGridStairAudit||bTilePathHashAudit||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0) &&!bStreamPipeline &&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld)) {std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;} @@ -2657,6 +3019,12 @@ int main(int Argc,char** Argv) App.Yaw,App.Pitch); return App.ShapeWorld->auditDetailPassing()&&bSurfacePassing?0:7; } + if(bGridStairAudit) + return App.ShapeWorld->auditGridStairIntegrity()?0:9; + if(bTilePathHashAudit) + return RunTilePathHashAudit(*App.ShapeWorld,App.Snapshot,App.CoreScheduler, + FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), + App.WorkerCount,App.ShapeDetailVariant,App.bRoundTunnels,App.bRoundTunnelEnhanced); if(App.bShapeCompare) return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.Yaw,App.Pitch,App.WorkerCount, ShapeCompareLevel,ShapeCompareTileLimit);