standalone: cheap chunk crossings -- exact-tile slab compile, nearest tiles first (~1.2 s -> 121 ms peak)
- Core compiles exact-tile analytic slabs for entering tiles; a Z-column sampler reuses the coherent
noise context along each tile column; the viewer keeps residents and prioritises nearby tiles.
- 30-crossing straight flight: nearest tile ready 15.8 ms median / 27.6 ms max; full crossing wall
121 ms peak; worker CPU per crossing 545 ms median, 834 ms max.
- W1/W8 hashes equal; routes 5642/5642; look, passage, stair, thin-sheet, coarse-panel and far
audits pass; bad-shell control fails; path-hash audit kept.
Cold whole view, measured by me interleaved (busy machine): 1.242 s vs 1.255 s for 3b37d57, parity.
Still open from the owner (LOD0): fins along a strata seam, choppy stair facets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
3b37d5702e
commit
08ceed200d
7 files changed
+611
-83
No files matched your search
+14
-9
@@ -64,15 +64,20 @@ for (const auto& key : entering) {
|
||||
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.
|
||||
By default, the Core groups missing keys by LOD and builds one analytic primitive/bin region for
|
||||
each entering LOD slab. The region bounds come from the requested absolute tile coordinates, with
|
||||
an 8-voxel sampling apron; no dense lattice is allocated for unused parts of the slab. A tile's
|
||||
samples therefore depend on its world coordinates and the world snapshot, not the camera path or
|
||||
the order in which tiles arrive. `compileTileRegion` skips LODs at or above
|
||||
`fullResolutionLodCount`, keys covered by the initial `ShapeBuildRequest`, and keys already
|
||||
resident. Its optional output counts requested keys that received a new region. After the regions
|
||||
needed by the next desired set compile, call `retainTileRegions` with that set to release regions
|
||||
that are no longer needed. The Core owns region lifetime and mesh data; the host keeps still-visible
|
||||
meshes resident until replacement tiles are generated and uploaded.
|
||||
|
||||
Pass `analyticOnly = false` to retain the reference packed-page compiler. Packed pages use absolute
|
||||
parent-cell coordinates and an 8-voxel apron; each covers a 128-voxel cube or one larger coarse
|
||||
tile. This path remains available for parity checks and workloads that need the packed lattice.
|
||||
|
||||
## Queries and tiles
|
||||
|
||||
|
||||
@@ -148,6 +148,18 @@ namespace VoxelForgeCore
|
||||
std::uint64_t walkFailures = 0;
|
||||
};
|
||||
|
||||
struct TileRegionCompileStats
|
||||
{
|
||||
std::size_t regionsBuilt = 0;
|
||||
std::size_t tileKeysCovered = 0;
|
||||
std::uint64_t latticeNodes = 0;
|
||||
double shapeSeconds = 0.0;
|
||||
double intervalSeconds = 0.0;
|
||||
double surfaceSeconds = 0.0;
|
||||
double workerSeconds = 0.0;
|
||||
double wallSeconds = 0.0;
|
||||
};
|
||||
|
||||
struct FarConnectorSegment { Vec3d start, end; float radius = 0.0f; };
|
||||
|
||||
struct EditBrush
|
||||
@@ -200,12 +212,16 @@ 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.
|
||||
// Compile missing detail regions for entering fine tiles. Generated tile keys map to
|
||||
// shared world-coordinate regions. The default analyticOnly path coalesces each entering
|
||||
// LOD slab, builds its primitives/bins, and evaluates density on demand without
|
||||
// rasterizing unused lattice volumes. Set false to use fixed absolute packed pages. The
|
||||
// optional count is newly covered tile keys; regions stay resident until retainTileRegions.
|
||||
bool compileTileRegion(std::span<const TileKey> tiles, std::uint32_t workerLimit,
|
||||
const JobScheduler& scheduler, std::string& error,
|
||||
std::size_t* outCompiledTiles = nullptr);
|
||||
std::size_t* outCompiledTiles = nullptr,
|
||||
TileRegionCompileStats* outStats = nullptr,
|
||||
bool analyticOnly = true);
|
||||
void retainTileRegions(std::span<const TileKey> desiredTiles);
|
||||
void buildFarWorld();
|
||||
bool isCompiled() const noexcept;
|
||||
|
||||
@@ -271,12 +271,14 @@ public:
|
||||
bool bIncludeFarWorld,
|
||||
std::string& Error,
|
||||
const VoxelForgeCore::JobScheduler* InScheduler=nullptr,
|
||||
bool bInRoundTunnels=false,bool bInProfileDetailBand=false)
|
||||
bool bInRoundTunnels=false,bool bInProfileDetailBand=false,
|
||||
bool bValidatePassageRoutes=true)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto TotalStart=Clock::now();
|
||||
Owner=&World;WorkerCount=std::clamp(Workers,1,8);DetailVariant=Variant;
|
||||
Scheduler=InScheduler;RoundTunnelsEnabled=bInRoundTunnels;
|
||||
bAnalyticOnly=false;
|
||||
bProfileDetailBand=bInProfileDetailBand;
|
||||
for(int32 A=0;A<3;++A)
|
||||
{
|
||||
@@ -321,7 +323,9 @@ public:
|
||||
ApplyDetailBand(Stats.DetailBandLines);
|
||||
Stats.SurfaceProxyNodes+=CompleteSignEdgeDistances();
|
||||
Stats.SurfaceSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||
ValidatePassageRoutes();
|
||||
// Route validation is world-wide. Detail regions reuse the same authored route data and
|
||||
// do not need to repeat the 5,642-point world audit for every entering tile group.
|
||||
if(bValidatePassageRoutes)ValidatePassageRoutes();
|
||||
Built=true;
|
||||
if(bIncludeFarWorld)BuildFarWorld();
|
||||
const uint64 WordBytes=static_cast<uint64>(RockWords.size()+CandidateWords.size())*sizeof(uint64);
|
||||
@@ -403,6 +407,35 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildAnalyticRegion(FStandaloneOwnerWorld& World,const FIntVector& MinVoxels,
|
||||
const FIntVector& MaxVoxels,const FIntVector& ClipMinVoxels,
|
||||
const FIntVector& ClipMaxVoxels,EDetailVariant Variant,
|
||||
bool bInRoundTunnels,std::string& Error)
|
||||
{
|
||||
using Clock=std::chrono::steady_clock;
|
||||
Owner=&World;WorkerCount=1;DetailVariant=Variant;Scheduler=nullptr;
|
||||
RoundTunnelsEnabled=bInRoundTunnels;bAnalyticOnly=true;bProfileDetailBand=false;
|
||||
for(int32 A=0;A<3;++A)
|
||||
{
|
||||
Min[A]=(&MinVoxels.X)[A]-2;Max[A]=(&MaxVoxels.X)[A]+2;
|
||||
ClipMin[A]=(&ClipMinVoxels.X)[A];ClipMax[A]=(&ClipMaxVoxels.X)[A];
|
||||
Size[A]=Max[A]-Min[A]+1;
|
||||
if(Size[A]<=0||Size[A]>4096||ClipMax[A]<ClipMin[A])
|
||||
{Error="analytic tile region is invalid or outside the bounded 4096-node compiler envelope";return false;}
|
||||
}
|
||||
Stats={};
|
||||
const auto Stage=Clock::now();
|
||||
BuildLayerBounds();BuildOwnerPrimitives();
|
||||
if(RoundTunnelsEnabled)BuildRoundTunnelPrimitives();
|
||||
BuildSpatialBins(LineBinSize);
|
||||
Stats.ShapeSeconds=std::chrono::duration<double>(Clock::now()-Stage).count();
|
||||
Stats.PrimitiveCount=Primitives.size();
|
||||
if(Primitives.empty())
|
||||
{Error="owner shape extraction produced no rooms, tunnels, or passages";return false;}
|
||||
Built=true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const FBuildStats& GetStats() const { return Stats; }
|
||||
bool IsBuilt() const { return Built; }
|
||||
bool HasFarWorld() const { return FarBuilt; }
|
||||
@@ -490,7 +523,7 @@ public:
|
||||
{
|
||||
int32 IX=0,IY=0,IZ=0;
|
||||
const bool bInteger=NearInteger(X,IX)&&NearInteger(Y,IY)&&NearInteger(Z,IZ);
|
||||
const bool bPacked=bInteger&&Contains(IX,IY,IZ);
|
||||
const bool bPacked=!bAnalyticOnly&&bInteger&&Contains(IX,IY,IZ);
|
||||
if(bPacked)
|
||||
return PackedDensityAt(IX,IY,IZ,IsRock(IX,IY,IZ),X,Y,Z);
|
||||
return AnalyticDensityAt(X,Y,Z);
|
||||
@@ -498,7 +531,7 @@ public:
|
||||
|
||||
float DensityAtLattice(int32 X,int32 Y,int32 Z) const
|
||||
{
|
||||
const bool bPacked=Contains(X,Y,Z);
|
||||
const bool bPacked=!bAnalyticOnly&&Contains(X,Y,Z);
|
||||
if(bPacked)
|
||||
return PackedDensityAt(X,Y,Z,IsRock(X,Y,Z),static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(Z));
|
||||
@@ -506,11 +539,19 @@ public:
|
||||
static_cast<float>(Z));
|
||||
}
|
||||
|
||||
bool IsAnalyticOnly() const { return bAnalyticOnly; }
|
||||
|
||||
void DensityAtLatticeX8(int32 XStart,int32 XStep,int32 Y,int32 Z,
|
||||
int32 Count,float OutDensity[8]) const
|
||||
{
|
||||
Count=std::clamp(Count,0,8);
|
||||
if(Count==0)return;
|
||||
if(bAnalyticOnly)
|
||||
{
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
OutDensity[Lane]=DensityAtLattice(XStart+Lane*XStep,Y,Z);
|
||||
return;
|
||||
}
|
||||
bool bFast=EditBrushes.empty()&&Y>=Min[1]&&Y<=Max[1]
|
||||
&&Z>=Min[2]&&Z<=Max[2]
|
||||
&&XStart>=Min[0]
|
||||
@@ -3430,6 +3471,75 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void DensityAtLatticeZRun(int32 X,int32 Y,int32 ZStart,int32 ZStep,
|
||||
int32 Count,float* OutDensity) const
|
||||
{
|
||||
if(Count<=0||OutDensity==nullptr)return;
|
||||
if(!bAnalyticOnly)
|
||||
{
|
||||
for(int32 Lane=0;Lane<Count;++Lane)
|
||||
OutDensity[Lane]=DensityAtLattice(X,Y,ZStart+Lane*ZStep);
|
||||
return;
|
||||
}
|
||||
|
||||
FDetailNoiseLineContext DetailNoiseLine(
|
||||
static_cast<float>(X),static_cast<float>(Y),static_cast<uint32>(Owner->Seed));
|
||||
for(int32 Offset=0;Offset<Count;)
|
||||
{
|
||||
int32 Z[8];
|
||||
const int32 Remaining=std::min(8,Count-Offset);
|
||||
for(int32 Lane=0;Lane<Remaining;++Lane)
|
||||
Z[Lane]=ZStart+(Offset+Lane)*ZStep;
|
||||
const int32 BinZ=FloorDiv(Z[0],ActiveSpatialBinSize);
|
||||
const auto Found=SpatialBins.find({FloorDiv(X,ActiveSpatialBinSize),
|
||||
FloorDiv(Y,ActiveSpatialBinSize),BinZ});
|
||||
const FBinItems* BinItems=Found==SpatialBins.end()?nullptr:&Found->second;
|
||||
int32 BatchCount=1;
|
||||
while(BatchCount<Remaining
|
||||
&&FloorDiv(Z[BatchCount],ActiveSpatialBinSize)==BinZ)++BatchCount;
|
||||
|
||||
FEffectiveRockSample Samples[8]{};
|
||||
EffectiveRockDistanceZ8(X,Y,Z,BatchCount,BinItems,Samples);
|
||||
for(int32 Lane=0;Lane<BatchCount;++Lane)
|
||||
{
|
||||
const int32 SampleZ=Z[Lane];
|
||||
const bool bInsideFine=X>=Min[0]&&X<=Max[0]
|
||||
&&Y>=Min[1]&&Y<=Max[1]&&SampleZ>=Min[2]&&SampleZ<=Max[2];
|
||||
float RockDistance=Samples[Lane].Distance;
|
||||
if(bInsideFine&&DetailVariant!=EDetailVariant::Smooth
|
||||
&&std::abs(RockDistance)<=DetailSupremum+0.125f)
|
||||
{
|
||||
const float Fade=DetailBandFadeAt(static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(SampleZ),RockDistance,
|
||||
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
|
||||
Samples[Lane].FloorInfluence);
|
||||
if(Fade>0.0f)RockDistance+=DetailNoiseOnLine(static_cast<float>(X),
|
||||
static_cast<float>(Y),static_cast<float>(SampleZ),
|
||||
Samples[Lane].SurfaceSDF,Samples[Lane].CaveInfluence,
|
||||
Samples[Lane].FloorInfluence,Samples[Lane].PassageInfluence,
|
||||
DetailNoiseLine)*Fade;
|
||||
}
|
||||
if(Samples[Lane].bCriticalAir)
|
||||
RockDistance=std::min(RockDistance,-1.0f/16.0f);
|
||||
const bool bRock=RockDistance>=0.0f;
|
||||
float Density=-RockDistance;
|
||||
if((Density<0.0f)!=bRock||std::abs(Density)<0.05f)
|
||||
Density=(bRock?-1.0f:1.0f)*std::max(std::abs(Density),0.05f);
|
||||
OutDensity[Offset+Lane]=ApplyEditBrushes(Density,X,Y,SampleZ);
|
||||
}
|
||||
Offset+=BatchCount;
|
||||
}
|
||||
}
|
||||
|
||||
void DensityAtLatticeZ8(int32 X,int32 Y,int32 ZStart,int32 ZStep,
|
||||
int32 Count,float OutDensity[8]) const
|
||||
{
|
||||
DensityAtLatticeZRun(X,Y,ZStart,ZStep,std::clamp(Count,0,8),OutDensity);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
float CoarseRockDistance(double X,double Y,double Z,bool* OutCriticalAir) const
|
||||
{
|
||||
const float Base=StrataDistance(Z);
|
||||
@@ -3906,4 +4016,5 @@ private:
|
||||
std::array<std::vector<uint64>,3> CandidateAxisWords;
|
||||
std::vector<uint64> CandidateWords;
|
||||
FBuildStats Stats;
|
||||
bool bAnalyticOnly=false;
|
||||
};
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <shared_mutex>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -316,59 +317,156 @@ namespace VoxelForgeCore
|
||||
|
||||
bool World::compileTileRegion(std::span<const TileKey> tiles,std::uint32_t workerLimit,
|
||||
const JobScheduler& scheduler,std::string& error,
|
||||
std::size_t* outCompiledTiles)
|
||||
std::size_t* outCompiledTiles,
|
||||
TileRegionCompileStats* outStats,bool analyticOnly)
|
||||
{
|
||||
if(outCompiledTiles)*outCompiledTiles=0;
|
||||
if(outStats)*outStats={};
|
||||
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<TileKey,std::vector<TileKey>,TileKeyHash> pendingPages;
|
||||
pendingPages.reserve(tiles.size());
|
||||
struct FPendingRegion
|
||||
{
|
||||
TileKey pageKey;
|
||||
std::vector<TileKey> tiles;
|
||||
};
|
||||
std::vector<FPendingRegion> pendingRegions;
|
||||
pendingRegions.reserve(tiles.size());
|
||||
std::unordered_map<TileKey,std::size_t,TileKeyHash> regionByPage;
|
||||
regionByPage.reserve(tiles.size());
|
||||
std::unordered_set<TileKey,TileKeyHash> seenTiles;
|
||||
seenTiles.reserve(tiles.size()*2+1);
|
||||
{
|
||||
std::shared_lock lock(impl_->tileRegionMutex);
|
||||
for(const TileKey& key:tiles)
|
||||
for(std::size_t inputIndex=0;inputIndex<tiles.size();++inputIndex)
|
||||
{
|
||||
const TileKey& key=tiles[inputIndex];
|
||||
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(impl_->tileRegions.contains(key)||!seenTiles.insert(key).second)continue;
|
||||
// Analytic regions evaluate only requested fine tiles and can safely coalesce a
|
||||
// whole entering LOD slab. Packed pages retain their fixed absolute parent cells.
|
||||
const TileKey pageKey=analyticOnly?TileKey{{0,0,0},key.lod}:ShapePageKey(key);
|
||||
auto [found,inserted]=regionByPage.try_emplace(pageKey,pendingRegions.size());
|
||||
if(inserted)pendingRegions.push_back({pageKey,{}});
|
||||
pendingRegions[found->second].tiles.push_back(key);
|
||||
}
|
||||
}
|
||||
if(pendingPages.empty())return true;
|
||||
if(pendingRegions.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<<pageKey.lod;
|
||||
const FIntVector pageMin(pageKey.coordinate.x*tileCount*tileExtent,
|
||||
pageKey.coordinate.y*tileCount*tileExtent,
|
||||
pageKey.coordinate.z*tileCount*tileExtent);
|
||||
const FIntVector pageMax(pageMin.X+tileCount*tileExtent,
|
||||
pageMin.Y+tileCount*tileExtent,pageMin.Z+tileCount*tileExtent);
|
||||
constexpr std::int32_t PagePad=8;
|
||||
const FIntVector fineMin(pageMin.X-PagePad,pageMin.Y-PagePad,pageMin.Z-PagePad);
|
||||
const FIntVector fineMax(pageMax.X+PagePad,pageMax.Y+PagePad,pageMax.Z+PagePad);
|
||||
|
||||
auto page=std::make_shared<FStandaloneShapeWorld>();
|
||||
using Clock=std::chrono::steady_clock;
|
||||
const auto compileStart=Clock::now();
|
||||
TileRegionCompileStats stats;
|
||||
std::size_t compiledTileCount=0;
|
||||
std::vector<std::shared_ptr<FStandaloneShapeWorld>> compiledRegions(pendingRegions.size());
|
||||
std::vector<std::string> regionErrors(pendingRegions.size());
|
||||
const ShapeBuildRequest& base=impl_->buildRequest;
|
||||
if(!page->Build(*impl_->owner,fineMin,fineMax,pageMin,pageMax,
|
||||
static_cast<int32>(std::clamp<std::uint32_t>(workerLimit,1,8)),
|
||||
ToNative(base.detail),false,error,&scheduler,
|
||||
base.roundTunnels,base.profileDetailBand))return false;
|
||||
if(!impl_->editLog.empty())page->SetEditTransaction(impl_->editLog);
|
||||
const std::uint32_t clampedWorkers=std::clamp<std::uint32_t>(workerLimit,1,8);
|
||||
|
||||
if(analyticOnly)
|
||||
{
|
||||
const auto BuildAnalyticRegionAt=[&](std::size_t index)
|
||||
{
|
||||
const FPendingRegion& pending=pendingRegions[index];
|
||||
FIntVector regionMin(MAX_int32,MAX_int32,MAX_int32);
|
||||
FIntVector regionMax(MIN_int32,MIN_int32,MIN_int32);
|
||||
for(const TileKey& key:pending.tiles)
|
||||
{
|
||||
const FVoxelTileKey native=ToNative(key);
|
||||
const FIntVector origin=native.OriginVoxels();
|
||||
const std::int32_t extent=native.ExtentVoxels();
|
||||
for(std::int32_t axis=0;axis<3;++axis)
|
||||
{
|
||||
(®ionMin.X)[axis]=std::min((®ionMin.X)[axis],(&origin.X)[axis]);
|
||||
(®ionMax.X)[axis]=std::max((®ionMax.X)[axis],(&origin.X)[axis]+extent);
|
||||
}
|
||||
}
|
||||
const FIntVector fineMin(regionMin.X-PagePad,regionMin.Y-PagePad,regionMin.Z-PagePad);
|
||||
const FIntVector fineMax(regionMax.X+PagePad,regionMax.Y+PagePad,regionMax.Z+PagePad);
|
||||
auto region=std::make_shared<FStandaloneShapeWorld>();
|
||||
if(!region->BuildAnalyticRegion(*impl_->owner,fineMin,fineMax,regionMin,regionMax,
|
||||
ToNative(base.detail),base.roundTunnels,regionErrors[index]))return;
|
||||
compiledRegions[index]=std::move(region);
|
||||
};
|
||||
struct FAnalyticBuildContext
|
||||
{
|
||||
decltype(BuildAnalyticRegionAt)* BuildAt;
|
||||
} context{&BuildAnalyticRegionAt};
|
||||
std::array<double,16> workerActive{};
|
||||
if(pendingRegions.size()==1)
|
||||
{
|
||||
// A chunk crossing normally contributes one entering slab. Building that small
|
||||
// immutable primitive/bin view inline avoids paying for a scheduler wake-up while
|
||||
// the existing tile workers are already busy finishing the previous crossing.
|
||||
BuildAnalyticRegionAt(0);
|
||||
if(compiledRegions[0])stats.workerSeconds=compiledRegions[0]->GetStats().ShapeSeconds;
|
||||
}
|
||||
else
|
||||
stats.workerSeconds=scheduler.parallel_for(pendingRegions.size(),1,clampedWorkers,
|
||||
&context,[](void* raw,std::size_t index,std::size_t)
|
||||
{
|
||||
auto& ctx=*static_cast<FAnalyticBuildContext*>(raw);
|
||||
(*ctx.BuildAt)(index);
|
||||
},workerActive.data(),workerActive.size());
|
||||
for(std::size_t i=0;i<pendingRegions.size();++i)
|
||||
{
|
||||
if(!regionErrors[i].empty()){error=regionErrors[i];return false;}
|
||||
if(!compiledRegions[i]){error="analytic tile region build did not produce a result";return false;}
|
||||
if(!impl_->editLog.empty())compiledRegions[i]->SetEditTransaction(impl_->editLog);
|
||||
stats.shapeSeconds+=compiledRegions[i]->GetStats().ShapeSeconds;
|
||||
stats.tileKeysCovered+=pendingRegions[i].tiles.size();
|
||||
}
|
||||
stats.regionsBuilt=pendingRegions.size();
|
||||
}
|
||||
else
|
||||
{
|
||||
for(std::size_t i=0;i<pendingRegions.size();++i)
|
||||
{
|
||||
const FPendingRegion& pending=pendingRegions[i];
|
||||
const std::int32_t tileCount=TilesPerShapePage(pending.pageKey.lod);
|
||||
const std::int32_t tileExtent=CHUNK_SIZE<<pending.pageKey.lod;
|
||||
const FIntVector regionMin(pending.pageKey.coordinate.x*tileCount*tileExtent,
|
||||
pending.pageKey.coordinate.y*tileCount*tileExtent,
|
||||
pending.pageKey.coordinate.z*tileCount*tileExtent);
|
||||
const FIntVector regionMax(regionMin.X+tileCount*tileExtent,
|
||||
regionMin.Y+tileCount*tileExtent,regionMin.Z+tileCount*tileExtent);
|
||||
const FIntVector fineMin(regionMin.X-PagePad,regionMin.Y-PagePad,regionMin.Z-PagePad);
|
||||
const FIntVector fineMax(regionMax.X+PagePad,regionMax.Y+PagePad,regionMax.Z+PagePad);
|
||||
auto region=std::make_shared<FStandaloneShapeWorld>();
|
||||
if(!region->Build(*impl_->owner,fineMin,fineMax,regionMin,regionMax,
|
||||
static_cast<int32>(clampedWorkers),ToNative(base.detail),false,error,&scheduler,
|
||||
base.roundTunnels,base.profileDetailBand,false))return false;
|
||||
if(!impl_->editLog.empty())region->SetEditTransaction(impl_->editLog);
|
||||
const auto& build=region->GetStats();
|
||||
++stats.regionsBuilt;
|
||||
stats.tileKeysCovered+=pending.tiles.size();
|
||||
stats.latticeNodes+=build.SignNodes;
|
||||
stats.shapeSeconds+=build.ShapeSeconds;
|
||||
stats.intervalSeconds+=build.IntervalSeconds;
|
||||
stats.surfaceSeconds+=build.SurfaceSeconds;
|
||||
stats.workerSeconds+=build.RockIntervalLines.WorkerSeconds
|
||||
+build.CandidateAxisLines.WorkerSeconds
|
||||
+build.CandidateMergeLines.WorkerSeconds
|
||||
+build.DetailBandLines.WorkerSeconds;
|
||||
compiledRegions[i]=std::move(region);
|
||||
}
|
||||
}
|
||||
{
|
||||
std::unique_lock lock(impl_->tileRegionMutex);
|
||||
impl_->tileRegions.try_emplace(pageKey,std::move(page));
|
||||
for(std::size_t i=0;i<pendingRegions.size();++i)
|
||||
{
|
||||
for(const TileKey& key:pendingRegions[i].tiles)
|
||||
if(impl_->tileRegions.try_emplace(key,compiledRegions[i]).second)++compiledTileCount;
|
||||
}
|
||||
impl_->tileRegionCount.store(impl_->tileRegions.size(),std::memory_order_release);
|
||||
}
|
||||
compiledTileCount+=requestedTiles.size();
|
||||
}
|
||||
stats.wallSeconds=std::chrono::duration<double>(Clock::now()-compileStart).count();
|
||||
if(outCompiledTiles)*outCompiledTiles=compiledTileCount;
|
||||
if(outStats)*outStats=stats;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -379,7 +477,7 @@ namespace VoxelForgeCore
|
||||
desired.reserve(desiredTiles.size()*2+1);
|
||||
for(const TileKey& key:desiredTiles)
|
||||
if(key.lod>=0&&key.lod<impl_->configuration.fullResolutionLodCount)
|
||||
desired.insert(ShapePageKey(key));
|
||||
desired.insert(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);
|
||||
@@ -483,7 +581,7 @@ namespace VoxelForgeCore
|
||||
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())
|
||||
if(const auto found=impl_->tileRegions.find(key);found!=impl_->tileRegions.end())
|
||||
page=found->second;
|
||||
}
|
||||
const FStandaloneShapeWorld& tileShape=page?*page:*impl_->shape;
|
||||
@@ -584,8 +682,11 @@ namespace VoxelForgeCore
|
||||
impl_->shape->SetEditTransaction(impl_->editLog);
|
||||
{
|
||||
std::shared_lock lock(impl_->tileRegionMutex);
|
||||
std::unordered_set<FStandaloneShapeWorld*> updatedRegions;
|
||||
updatedRegions.reserve(impl_->tileRegions.size());
|
||||
for(const auto& [key,page]:impl_->tileRegions)
|
||||
if(page)page->SetEditTransaction(impl_->editLog);
|
||||
if(page&&updatedRegions.insert(page.get()).second)
|
||||
page->SetEditTransaction(impl_->editLog);
|
||||
}
|
||||
const FIntVector min=impl_->shape->GetEditBoundsMin(),max=impl_->shape->GetEditBoundsMax();
|
||||
EditResult result;result.digest=digest;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Chunk streaming optimization results
|
||||
|
||||
Build: `VoxelForgeChunkStreamFastColumn.exe`; built with `Standalone/Build.ps1` (`/MP8`) and run via `Core/verification/RunMachine.ps1` (`BelowNormal`, affinity `0xFF00`). No `0xC0000142` occurred. Cache-use audits report zero hits.
|
||||
|
||||
## Flight benchmark
|
||||
|
||||
Command: `--shape-world --test-flight-crossings=30 --flight-speed-mps=16 --workers=8`. Straight +Y flight, 30 unit chunk crossings. `crossing wall` is from crossing start through all requested tiles; `worker CPU sum` sums each tile job's execution time across workers.
|
||||
|
||||
| Crossing | Tiles | Page-covered | Regions | Core region wall ms | Nearest ready ms | Nearest tile | Worker CPU sum ms | Crossing wall ms |
|
||||
|---:|---:|---:|---:|---:|---:|:---|---:|---:|
|
||||
| 1 | 144 | 128 | 1 | 9.1 | 16.62 | `-1,-4,-2,lod1` | 478 | 105 |
|
||||
| 2 | 144 | 0 | 0 | 0.0 | 2.48 | `-3,-10,-3,lod0` | 128 | 19 |
|
||||
| 3 | 108 | 0 | 0 | 0.0 | 2.73 | `-1,0,-1,lod2` | 98 | 15 |
|
||||
| 4 | 144 | 128 | 1 | 6.2 | 9.75 | `-3,-8,-3,lod0` | 450 | 67 |
|
||||
| 5 | 144 | 128 | 1 | 7.7 | 11.81 | `-2,-2,-2,lod1` | 405 | 62 |
|
||||
| 6 | 144 | 128 | 1 | 6.6 | 11.25 | `-3,-6,-4,lod0` | 545 | 78 |
|
||||
| 7 | 76 | 0 | 0 | 0.0 | 1.90 | `-1,2,-1,lod3` | 44 | 8 |
|
||||
| 8 | 144 | 128 | 1 | 5.7 | 9.35 | `-3,-4,-4,lod0` | 452 | 69 |
|
||||
| 9 | 144 | 128 | 1 | 7.2 | 14.34 | `-1,0,-2,lod1` | 618 | 89 |
|
||||
| 10 | 144 | 128 | 1 | 5.8 | 9.20 | `-3,-2,-3,lod0` | 511 | 75 |
|
||||
| 11 | 108 | 0 | 0 | 0.0 | 2.25 | `-1,2,-1,lod2` | 80 | 12 |
|
||||
| 12 | 144 | 144 | 2 | 8.9 | 16.00 | `-3,0,-3,lod0` | 729 | 108 |
|
||||
| 13 | 144 | 128 | 1 | 7.5 | 21.19 | `-2,2,-2,lod1` | 700 | 100 |
|
||||
| 14 | 144 | 144 | 2 | 11.7 | 27.57 | `-3,2,-4,lod0` | 786 | 118 |
|
||||
| 15 | 26 | 0 | 0 | 0.0 | 10.29 | `-1,6,-1,lod8` | 123 | 19 |
|
||||
| 16 | 144 | 144 | 2 | 9.2 | 23.58 | `-3,4,-4,lod0` | 798 | 115 |
|
||||
| 17 | 144 | 128 | 1 | 7.5 | 24.21 | `-1,4,-2,lod1` | 675 | 105 |
|
||||
| 18 | 144 | 144 | 2 | 9.2 | 20.95 | `-3,6,-3,lod0` | 834 | 120 |
|
||||
| 19 | 108 | 0 | 0 | 0.0 | 2.22 | `-1,4,-1,lod2` | 62 | 10 |
|
||||
| 20 | 144 | 144 | 2 | 9.8 | 20.24 | `-3,8,-3,lod0` | 704 | 106 |
|
||||
| 21 | 144 | 128 | 1 | 6.7 | 22.71 | `-2,6,-2,lod1` | 771 | 121 |
|
||||
| 22 | 144 | 144 | 2 | 10.2 | 21.40 | `-3,10,-4,lod0` | 605 | 93 |
|
||||
| 23 | 76 | 0 | 0 | 0.0 | 1.88 | `-1,4,-1,lod3` | 38 | 6 |
|
||||
| 24 | 144 | 144 | 2 | 8.5 | 19.45 | `-3,12,-4,lod0` | 620 | 93 |
|
||||
| 25 | 144 | 128 | 1 | 8.2 | 23.03 | `-1,8,-2,lod1` | 758 | 116 |
|
||||
| 26 | 144 | 144 | 2 | 8.2 | 15.76 | `-3,14,-3,lod0` | 659 | 97 |
|
||||
| 27 | 108 | 0 | 0 | 0.0 | 2.74 | `-1,6,-1,lod2` | 51 | 9 |
|
||||
| 28 | 144 | 144 | 2 | 8.2 | 16.06 | `-3,16,-3,lod0` | 663 | 97 |
|
||||
| 29 | 144 | 128 | 1 | 7.6 | 20.73 | `-2,10,-2,lod1` | 483 | 80 |
|
||||
| 30 | 144 | 144 | 2 | 9.2 | 21.09 | `-3,18,-4,lod0` | 700 | 104 |
|
||||
|
||||
Flight summary: [FlightBenchmarkComplete] crossings=30/30 speed=16.00m/s elapsed=15.006s cache_hits=0 crossing_deltas=unit-+Y nearest_ready_p50=15.762ms nearest_ready_p95=23.577ms nearest_ready_max=27.570ms max_crossing_wall=0.121s max_worker_cpu_sum=0.834s resident=2226/2226 missing_tiles=0 deferred_leavers=0 uploads_drained=yes status=PASS
|
||||
|
||||
Nearest-ready median/p95/max: 15.762 / 23.577 / 27.570 ms. Maximum full crossing wall: 121 ms. Aggregate worker CPU sum: median 545 ms, p95 798 ms, max 834 ms. The 200 ms wall-latency target is met; a 200 ms aggregate-CPU interpretation is not. All 30 crossings completed; resident 2226/2226, missing tiles 0, deferred leavers 0, uploads drained, cache hits 0.
|
||||
|
||||
## Profile
|
||||
|
||||
The prior packed-page crossing in `owner_final6_chunk_crossing.stdout.log` rebuilt 16 pages covering 128 tiles: Core page wall 1.1828 s, then tile worker sum 156 ms and crossing wall 1.206 s. The analytic path now builds one region for a typical new slab (sometimes two); the flight log shows 0 lattice nodes and 0 interval/sign or detail-band compile time, with primitive/bin setup at about 6–12 ms per region. Crossings already covered by the initial region or retained tiles build 0 new regions. The 128 LOD1 tile meshes dominate new-slab work at roughly 0.4–0.8 s aggregate worker CPU, while all tile jobs finish in 0.06–0.12 s wall. The 30-crossing residency audit found no holes, deferred leavers, or undrained uploads.
|
||||
|
||||
Crossing 1 profile: Core slab build 9.6 ms wall / 9.1 ms worker, zero lattice nodes and zero interval/detail work; tile meshing 478 ms aggregate worker CPU (128 LOD1 tiles: 472 ms; 16 LOD2: 6 ms), 105 ms crossing wall, nearest tile ready in 16.62 ms. This replaces the baseline 1.183 s page build and 1.206 s crossing wall.
|
||||
|
||||
## Determinism and cold view
|
||||
|
||||
W1 and W8 full-clipmap command: `--stream-benchmark --shape-world --force-generate --stream-hash --workers=N --clip-radius=3`. Both generated 2226/2226 tiles with cache hits 0 and identical hashes: density `0x0ed4113e468cda86`, mesh `0x20c2f9d3e3d28c6e`, payload `0xd0349793eed57609`, topology `0x0ed6c6bc668bfb9b`.
|
||||
|
||||
Interleaved cold comparison used `--stream-benchmark --shape-world --force-generate --stream-hash --workers=8 --clip-radius=3`, order baseline A / candidate A / candidate B / baseline B. `StreamFullClipmap` walls: baseline 0.826 / 0.759 s (mean 0.793 s); candidate 0.744 / 0.729 s (mean 0.737 s), with identical hashes and zero cache hits.
|
||||
|
||||
## Audits
|
||||
|
||||
- Route audit: 5642/5642 PASS.
|
||||
- Shape/look checks: flat surface, grid stair, thin sheet, flat patch, surface band, detail PASS. Six fixed captures match the prior candidate bit-for-bit. Against Round 11, changed pixels were 1432 (chamber), 310 (tunnel), 3879 (passage), 514 (far), 1811 (passage-floor close-up), and 0 (tunnel bend); each view remains 800×1280. Against `OwnerFinal6`, five views are bit-identical and 339/1,024,000 passage-floor pixels differ (mean channel delta 0.003896, max 58). Passage-floor recapture is bit-identical.
|
||||
- Passage wall detail: 13,271 passage samples, ratio 1.152, PASS.
|
||||
- Grid-stair distance: 952,997 sign edges, zero candidate fallback edges and zero both-fallback edges, PASS.
|
||||
- Fine shard audit: zero thin sheets, PASS. Coarse panel audit: zero off-surface panels and zero panel triangles, PASS.
|
||||
- Far survival: 7,200 boundary columns, zero missing/extra/hole samples, PASS. Bad-shell control: expected FAIL, 16 missing boundary surfaces, 512 removed triangles, exit 14.
|
||||
|
||||
## Log files
|
||||
|
||||
- Flight: `chunk_fast_column_flight30.stdout.log`
|
||||
- Path hash: `chunk_fast_column_path_hash.stdout.log`
|
||||
- W1/W8: `chunk_fast_column_hash_w1.stdout.log`, `chunk_fast_column_hash_w8.stdout.log`
|
||||
- Cold runs: `chunk_fast_column_cold_baseline_a.stdout.log`, `chunk_fast_column_cold_candidate_a.stdout.log`, `chunk_fast_column_cold_candidate_b.stdout.log`, `chunk_fast_column_cold_baseline_b.stdout.log`
|
||||
- Audits: `chunk_fast_column_shape_audit.stdout.log`, `chunk_fast_column_passage_detail.stdout.log`, `chunk_fast_column_grid_stair.stdout.log`, `chunk_fast_column_mesh_far_audits.stdout.log`, `chunk_fast_column_bad_shell_control.stdout.log`
|
||||
@@ -442,7 +442,17 @@ namespace
|
||||
uint64 Samples=0;
|
||||
double WorkerSeconds=0.0;
|
||||
double CorePageSeconds=0.0;
|
||||
double CorePageWorkerSeconds=0.0;
|
||||
uint64 PageCoveredTiles=0;
|
||||
uint64 PageRegions=0;
|
||||
uint64 PageLatticeNodes=0;
|
||||
FVoxelTileKey NearestNewTile{};
|
||||
double NearestReadySeconds=-1.0;
|
||||
bool bHasNearestNewTile=false;
|
||||
uint64 TilesByLevel[MaxLevels+1]{};
|
||||
uint64 SamplesByLevel[MaxLevels+1]{};
|
||||
double WorkerSecondsByLevel[MaxLevels+1]{};
|
||||
double WallSeconds=0.0;
|
||||
std::chrono::steady_clock::time_point StartTime{};
|
||||
bool bReported=false;
|
||||
};
|
||||
@@ -559,11 +569,16 @@ namespace
|
||||
bool bCompareEvaluators=false;
|
||||
bool bParityOnly=false;
|
||||
bool bDensityHash=false;
|
||||
bool bTestOneChunkCrossing=false;
|
||||
bool bCrossingMoveStarted=false;
|
||||
bool bCrossingMoveFinished=false;
|
||||
bool bCrossingClosePosted=false;
|
||||
std::chrono::steady_clock::time_point CrossingMoveStopTime{};
|
||||
int32 FlightTargetCrossings=0;
|
||||
int32 FlightObservedCrossings=0;
|
||||
uint64 FlightStartCrossingIndex=0;
|
||||
float FlightSpeedMps=16.0f;
|
||||
bool bFlightStarted=false;
|
||||
bool bFlightFinished=false;
|
||||
bool bFlightClosePosted=false;
|
||||
bool bFlightCrossingsValid=true;
|
||||
V3 FlightStartPosition{};
|
||||
std::chrono::steady_clock::time_point FlightStartTime{};
|
||||
bool bCaptureAfterClipmap=false;
|
||||
bool bQuitAfterClipmap=false;
|
||||
bool bPrintedClipOverride=false;
|
||||
@@ -1198,6 +1213,14 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
if(NewDesired.CenterChunk==BuildTargetChunk)return;
|
||||
const FIntVector Previous=BuildTargetChunk;
|
||||
const FIntVector CenterDelta(NewDesired.CenterChunk.X-Previous.X,
|
||||
NewDesired.CenterChunk.Y-Previous.Y,NewDesired.CenterChunk.Z-Previous.Z);
|
||||
if(FlightTargetCrossings>0&&bFlightStarted&&!bFlightFinished)
|
||||
{
|
||||
++FlightObservedCrossings;
|
||||
if(CenterDelta.X!=0||CenterDelta.Y!=1||CenterDelta.Z!=0)
|
||||
bFlightCrossingsValid=false;
|
||||
}
|
||||
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> NewSet;
|
||||
NewSet.reserve(NewDesired.Tiles.size()*2+1);
|
||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)NewSet.insert(Key);
|
||||
@@ -1206,6 +1229,17 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
const auto CrossingStart=std::chrono::steady_clock::now();
|
||||
double PageCompileSeconds=0.0;
|
||||
uint64 PageCoveredTiles=0;
|
||||
VoxelForgeCore::TileRegionCompileStats PageStats;
|
||||
const uint64 NextCrossing=CrossingIndex+1;
|
||||
FCrossingWork& NextWork=CrossingWork[NextCrossing];
|
||||
NextWork.StartTime=CrossingStart;
|
||||
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
||||
if(NewSet.contains(Key)&&!ResidentTiles.contains(Key))
|
||||
{
|
||||
NextWork.NearestNewTile=Key;
|
||||
NextWork.bHasNearestNewTile=true;
|
||||
break;
|
||||
}
|
||||
if(bShapeWorld&&ShapeWorld)
|
||||
{
|
||||
std::vector<VoxelForgeCore::TileKey> NewCoreTiles;
|
||||
@@ -1216,8 +1250,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
std::string PageError;
|
||||
size_t CompiledPageCount=0;
|
||||
if(!ShapeWorld->compileTileRegion(NewCoreTiles,
|
||||
static_cast<uint32>(std::min(4,WorkerCount)),CoreScheduler,PageError,
|
||||
&CompiledPageCount))
|
||||
static_cast<uint32>(std::clamp(WorkerCount,1,8)),CoreScheduler,PageError,
|
||||
&CompiledPageCount,&PageStats,true))
|
||||
{
|
||||
std::fprintf(stderr,"[ShapeRegionPage] compile failed at center=(%d,%d,%d): %s\n",
|
||||
NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z,
|
||||
@@ -1233,11 +1267,13 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
for(const FVoxelTileKey& Key:Leavers)DeferredLeaverTiles.insert(Key);
|
||||
const double PageSeconds=std::chrono::duration<double>(
|
||||
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",
|
||||
PageCompileSeconds=PageStats.wallSeconds;
|
||||
std::printf("[ShapeRegionPage] region_mode=analytic-lod-slab center=(%d,%d,%d) requested_tiles=%zu page_covered_tiles=%llu regions=%zu lattice_nodes=%llu worker_limit=%d shape=%.4fs intervals_signs=%.4fs detail=%.4fs core_worker_sum=%.4fs seconds=%.4f retained_resident=%zu deferred_leavers=%zu\n",
|
||||
NewDesired.CenterChunk.X,NewDesired.CenterChunk.Y,NewDesired.CenterChunk.Z,
|
||||
NewCoreTiles.size(),static_cast<unsigned long long>(PageCoveredTiles),
|
||||
std::min(4,WorkerCount),PageSeconds,
|
||||
PageStats.regionsBuilt,static_cast<unsigned long long>(PageStats.latticeNodes),
|
||||
std::clamp(WorkerCount,1,8),PageStats.shapeSeconds,PageStats.intervalSeconds,
|
||||
PageStats.surfaceSeconds,PageStats.workerSeconds,PageSeconds,
|
||||
ResidentTiles.size()-std::min(ResidentTiles.size(),Leavers.size()),Leavers.size());
|
||||
}
|
||||
else
|
||||
@@ -1260,8 +1296,9 @@ 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=CrossingStart;
|
||||
Work.PageCoveredTiles=PageCoveredTiles;Work.CorePageSeconds=PageCompileSeconds;
|
||||
Work.CorePageWorkerSeconds=PageStats.workerSeconds;
|
||||
Work.PageRegions=PageStats.regionsBuilt;Work.PageLatticeNodes=PageStats.latticeNodes;
|
||||
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 deferred_resident=%zu cancelled_queued=%d full_detail=%d\n",
|
||||
static_cast<unsigned long long>(CrossingIndex),Update.CenterChunk.X,Update.CenterChunk.Y,Update.CenterChunk.Z,
|
||||
@@ -1416,9 +1453,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
Work.bReported=true;
|
||||
const double AverageMs=Work.FinishedJobs>0
|
||||
?Work.WorkerSeconds*1000.0/static_cast<double>(Work.FinishedJobs):0.0;
|
||||
const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0
|
||||
Work.WallSeconds=Work.StartTime.time_since_epoch().count()!=0
|
||||
?std::chrono::duration<double>(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 page_covered_tiles=%llu core_page=%.4fs worker_sum=%.3fs wall=%.3fs avg=%.3fms\n",
|
||||
const double NearestReadyMs=Work.NearestReadySeconds>=0.0
|
||||
?Work.NearestReadySeconds*1000.0:-1.0;
|
||||
std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM page_covered_tiles=%llu page_regions=%llu page_nodes=%llu core_page=%.4fs core_page_worker=%.4fs nearest_ready=%.3fms nearest_tile=(%d,%d,%d,lod%d) worker_sum=%.3fs wall=%.3fs avg=%.3fms\n",
|
||||
static_cast<unsigned long long>(Pair.first),
|
||||
static_cast<unsigned long long>(Work.SubmittedJobs),
|
||||
static_cast<unsigned long long>(Work.FinishedJobs),
|
||||
@@ -1426,8 +1465,22 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
static_cast<unsigned long long>(Work.CancelledQueuedJobs),
|
||||
static_cast<unsigned long long>(Work.StaleResults),
|
||||
static_cast<double>(Work.Samples)/1.0e6,
|
||||
static_cast<unsigned long long>(Work.PageCoveredTiles),Work.CorePageSeconds,
|
||||
Work.WorkerSeconds,WallSeconds,AverageMs);
|
||||
static_cast<unsigned long long>(Work.PageCoveredTiles),
|
||||
static_cast<unsigned long long>(Work.PageRegions),
|
||||
static_cast<unsigned long long>(Work.PageLatticeNodes),
|
||||
Work.CorePageSeconds,Work.CorePageWorkerSeconds,NearestReadyMs,
|
||||
Work.NearestNewTile.Coord.X,Work.NearestNewTile.Coord.Y,
|
||||
Work.NearestNewTile.Coord.Z,Work.NearestNewTile.Level,
|
||||
Work.WorkerSeconds,Work.WallSeconds,AverageMs);
|
||||
for(int32 Level=0;Level<=MaxLevels;++Level)
|
||||
if(Work.TilesByLevel[Level]>0)
|
||||
std::printf("[ChunkCrossingLevel] id=%llu lod=%d tiles=%llu samples=%.3fM worker_sum=%.4fs avg=%.3fms\n",
|
||||
static_cast<unsigned long long>(Pair.first),Level,
|
||||
static_cast<unsigned long long>(Work.TilesByLevel[Level]),
|
||||
static_cast<double>(Work.SamplesByLevel[Level])/1.0e6,
|
||||
Work.WorkerSecondsByLevel[Level],
|
||||
Work.WorkerSecondsByLevel[Level]*1000.0/
|
||||
static_cast<double>(Work.TilesByLevel[Level]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1442,6 +1495,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing];
|
||||
if(Crossing.PendingJobs>0)--Crossing.PendingJobs;
|
||||
++Crossing.FinishedJobs;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds;
|
||||
if(Level>=0&&Level<=MaxLevels)
|
||||
{
|
||||
++Crossing.TilesByLevel[Level];
|
||||
Crossing.SamplesByLevel[Level]+=Tile.DensitySamples;
|
||||
Crossing.WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
||||
}
|
||||
if(Crossing.bHasNearestNewTile&&Tile.Key==Crossing.NearestNewTile
|
||||
&&Crossing.NearestReadySeconds<0.0)
|
||||
Crossing.NearestReadySeconds=std::chrono::duration<double>(
|
||||
Tile.CompletedAt-Crossing.StartTime).count();
|
||||
if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))
|
||||
{++Crossing.StaleResults;continue;}
|
||||
++Crossing.Tiles;
|
||||
@@ -1475,14 +1538,56 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
}
|
||||
}
|
||||
if(bShapePipelineActive)TryCompleteShapePipeline();
|
||||
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
|
||||
&&DirtyBatchSet.empty())
|
||||
if(FlightTargetCrossings>0&&bFlightFinished&&!bFlightClosePosted
|
||||
&&DirtyBatchSet.empty()&&DeferredLeaverTiles.empty())
|
||||
{
|
||||
const auto Crossing=CrossingWork.find(1);
|
||||
if(Crossing!=CrossingWork.end()&&Crossing->second.bReported)
|
||||
bool bAllFlightWorkReported=true;
|
||||
bool bCrossingLatencyUnderTarget=true;
|
||||
double MaxCrossingWallSeconds=0.0,MaxWorkerCpuSeconds=0.0;
|
||||
std::vector<double> NearestReadyMilliseconds;
|
||||
for(uint64 Crossing=FlightStartCrossingIndex+1;
|
||||
Crossing<=CrossingIndex;++Crossing)
|
||||
{
|
||||
bCrossingClosePosted=true;
|
||||
std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n");
|
||||
const auto It=CrossingWork.find(Crossing);
|
||||
if(It==CrossingWork.end()||!It->second.bReported)
|
||||
{bAllFlightWorkReported=false;continue;}
|
||||
const FCrossingWork& Work=It->second;
|
||||
MaxCrossingWallSeconds=std::max(MaxCrossingWallSeconds,Work.WallSeconds);
|
||||
MaxWorkerCpuSeconds=std::max(MaxWorkerCpuSeconds,Work.WorkerSeconds);
|
||||
if(Work.WallSeconds>=0.200)bCrossingLatencyUnderTarget=false;
|
||||
if(Work.NearestReadySeconds>=0.0)
|
||||
NearestReadyMilliseconds.push_back(Work.NearestReadySeconds*1000.0);
|
||||
}
|
||||
if(bAllFlightWorkReported)
|
||||
{
|
||||
bFlightClosePosted=true;
|
||||
const double Elapsed=std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now()-FlightStartTime).count();
|
||||
const uint64 CacheHits=Streamer?Streamer->GetCacheHits():0;
|
||||
std::size_t DesiredResidentCount=0;
|
||||
for(const FVoxelTileKey& Key:DesiredTiles)
|
||||
if(ResidentTiles.contains(Key))++DesiredResidentCount;
|
||||
std::sort(NearestReadyMilliseconds.begin(),NearestReadyMilliseconds.end());
|
||||
const auto Percentile=[&](std::size_t Percent)
|
||||
{
|
||||
if(NearestReadyMilliseconds.empty())return -1.0;
|
||||
const std::size_t Index=(NearestReadyMilliseconds.size()-1)*Percent/100;
|
||||
return NearestReadyMilliseconds[Index];
|
||||
};
|
||||
const double NearestMaximum=NearestReadyMilliseconds.empty()?-1.0:
|
||||
NearestReadyMilliseconds.back();
|
||||
const std::size_t MissingDesired=DesiredTiles.size()-DesiredResidentCount;
|
||||
const bool bPass=FlightObservedCrossings==FlightTargetCrossings
|
||||
&&bFlightCrossingsValid&&CacheHits==0&&bCrossingLatencyUnderTarget
|
||||
&&MissingDesired==0&&DeferredLeaverTiles.empty()&&DirtyBatchSet.empty();
|
||||
std::printf("[FlightBenchmarkComplete] crossings=%d/%d speed=%.2fm/s elapsed=%.3fs cache_hits=%llu crossing_deltas=%s nearest_ready_p50=%.3fms nearest_ready_p95=%.3fms nearest_ready_max=%.3fms max_crossing_wall=%.3fs max_worker_cpu_sum=%.3fs resident=%zu/%zu missing_tiles=%zu deferred_leavers=%zu uploads_drained=%s status=%s\n",
|
||||
FlightObservedCrossings,FlightTargetCrossings,FlightSpeedMps,Elapsed,
|
||||
static_cast<unsigned long long>(CacheHits),
|
||||
bFlightCrossingsValid?"unit-+Y":"invalid",
|
||||
Percentile(50),Percentile(95),NearestMaximum,MaxCrossingWallSeconds,
|
||||
MaxWorkerCpuSeconds,DesiredResidentCount,DesiredTiles.size(),MissingDesired,
|
||||
DeferredLeaverTiles.size(),DirtyBatchSet.empty()?"yes":"no",
|
||||
bPass?"PASS":"FAIL");
|
||||
std::fflush(stdout);
|
||||
PostMessageA(Window,WM_CLOSE,0,0);
|
||||
}
|
||||
@@ -1547,21 +1652,28 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
{
|
||||
PumpGeneration();
|
||||
const auto Now=std::chrono::steady_clock::now();
|
||||
if(bTestOneChunkCrossing&&bFullDetailReported&&!bCrossingMoveStarted&&!bCrossingMoveFinished)
|
||||
if(FlightTargetCrossings>0&&bFullDetailReported&&!bFlightStarted)
|
||||
{
|
||||
bCrossingMoveStarted=true;
|
||||
CrossingMoveStopTime=Now+std::chrono::milliseconds(350);
|
||||
Keys[VK_SHIFT]=true;Keys['W']=true;
|
||||
std::printf("[CrossingTestMove] moving forward for 350 ms after full detail.\n");
|
||||
bFlightStarted=true;FlightStartTime=Now;FlightStartPosition=Position;
|
||||
FlightStartCrossingIndex=CrossingIndex;FlightObservedCrossings=0;
|
||||
bFlightCrossingsValid=true;
|
||||
std::printf("[FlightBenchmarkStart] crossings=%d speed=%.2fm/s direction=+Y start=(%.2f,%.2f,%.2f)m cache_mode=none\n",
|
||||
FlightTargetCrossings,FlightSpeedMps,Position.X,Position.Y,Position.Z);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
if(bCrossingMoveStarted&&Now>=CrossingMoveStopTime)
|
||||
if(FlightTargetCrossings>0&&bFlightStarted&&!bFlightFinished)
|
||||
{
|
||||
Keys[VK_SHIFT]=false;Keys['W']=false;
|
||||
bCrossingMoveStarted=false;bCrossingMoveFinished=true;
|
||||
std::printf("[CrossingTestMove] movement stopped; waiting for crossing work and uploads.\n");
|
||||
const double Elapsed=std::chrono::duration<double>(Now-FlightStartTime).count();
|
||||
Position=V3(FlightStartPosition.X,FlightStartPosition.Y+
|
||||
static_cast<float>(FlightSpeedMps*Elapsed),FlightStartPosition.Z);
|
||||
if(FlightObservedCrossings>=FlightTargetCrossings)
|
||||
{
|
||||
bFlightFinished=true;
|
||||
std::printf("[FlightBenchmarkMove] movement stopped after %d center-chunk crossings; waiting for new nearest tiles, meshes and uploads.\n",
|
||||
FlightObservedCrossings);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
|
||||
PreviousFrame=Now;FrameMs=Dt*1000.0;
|
||||
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
|
||||
@@ -2807,7 +2919,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
const std::array<VoxelForgeCore::TileKey,1> pageTiles={target};
|
||||
std::size_t pageTilesA=0;
|
||||
if(!base.compileTileRegion(pageTiles,static_cast<uint32>(std::clamp(workers,1,8)),
|
||||
scheduler,error,&pageTilesA))
|
||||
scheduler,error,&pageTilesA,nullptr,false))
|
||||
{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;}
|
||||
@@ -2839,7 +2951,63 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
|
||||
static_cast<unsigned long long>(pathB->payloadHash),
|
||||
static_cast<unsigned long long>(pathA->topologyHash),
|
||||
static_cast<unsigned long long>(pathB->topologyHash),pass?"PASS":"FAIL");
|
||||
return pass?0:12;
|
||||
|
||||
VoxelForgeCore::World analyticForward(snapshot),analyticReverse(snapshot);
|
||||
VoxelForgeCore::ShapeBuildRequest forwardRequest,reverseRequest;
|
||||
const FVector reverseCamera((target.coordinate.x+TargetChunkOffset+0.5)*CHUNK_SIZE,
|
||||
camera.Y,camera.Z);
|
||||
const auto reverseTiles=base.desiredTiles(
|
||||
{reverseCamera.X,reverseCamera.Y,reverseCamera.Z});
|
||||
if(!MakeRegionBuildRequest(baseTiles,config,workers,detail,roundTunnels,
|
||||
enhancedWalls,forwardRequest,error)
|
||||
||!analyticForward.compile(forwardRequest,scheduler,error)
|
||||
||reverseTiles.empty()
|
||||
||!MakeRegionBuildRequest(reverseTiles,config,workers,detail,roundTunnels,
|
||||
enhancedWalls,reverseRequest,error)
|
||||
||!analyticReverse.compile(reverseRequest,scheduler,error))
|
||||
{std::fprintf(stderr,"[TilePathHashDirections] setup failed: %s\n",error.c_str());return 11;}
|
||||
std::size_t analyticForwardCovered=0,analyticReverseCovered=0;
|
||||
VoxelForgeCore::TileRegionCompileStats forwardStats,reverseStats;
|
||||
if(!analyticForward.compileTileRegion(pageTiles,static_cast<uint32>(std::clamp(workers,1,8)),
|
||||
scheduler,error,&analyticForwardCovered,&forwardStats,true)
|
||||
||!analyticReverse.compileTileRegion(pageTiles,static_cast<uint32>(std::clamp(workers,1,8)),
|
||||
scheduler,error,&analyticReverseCovered,&reverseStats,true))
|
||||
{std::fprintf(stderr,"[TilePathHashDirections] region compile failed: %s\n",error.c_str());return 11;}
|
||||
auto analyticPathForward=analyticForward.generateTile(target,options,error);
|
||||
auto analyticPathReverse=analyticReverse.generateTile(target,options,error);
|
||||
if(!analyticPathForward||!analyticPathReverse)
|
||||
{std::fprintf(stderr,"[TilePathHashDirections] tile generation failed: %s\n",error.c_str());return 11;}
|
||||
const bool analyticDirectionsSame=Same(*analyticPathForward,*analyticPathReverse);
|
||||
const bool packedAnalyticRenderSame=pathA->meshHash==analyticPathForward->meshHash
|
||||
&&pathA->payloadHash==analyticPathForward->payloadHash
|
||||
&&pathA->topologyHash==analyticPathForward->topologyHash;
|
||||
const bool packedAnalyticDensitySame=pathA->densityHash==analyticPathForward->densityHash;
|
||||
std::printf("[TilePathHashDirections] tile=(%d,%d,%d) lod=%d approach_deltas=(-%d,+%d) regions=(%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,
|
||||
TargetChunkOffset,TargetChunkOffset,analyticForwardCovered,analyticReverseCovered,
|
||||
static_cast<unsigned long long>(analyticPathForward->densityHash),
|
||||
static_cast<unsigned long long>(analyticPathReverse->densityHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->meshHash),
|
||||
static_cast<unsigned long long>(analyticPathReverse->meshHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->payloadHash),
|
||||
static_cast<unsigned long long>(analyticPathReverse->payloadHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->topologyHash),
|
||||
static_cast<unsigned long long>(analyticPathReverse->topologyHash),
|
||||
analyticDirectionsSame&&analyticForwardCovered==1&&analyticReverseCovered==1?"PASS":"FAIL");
|
||||
std::printf("[TilePathPackedAnalyticParity] tile=(%d,%d,%d) density=(0x%016llx,0x%016llx) density_representation=%s 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,
|
||||
static_cast<unsigned long long>(pathA->densityHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->densityHash),
|
||||
packedAnalyticDensitySame?"same":"different",
|
||||
static_cast<unsigned long long>(pathA->meshHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->meshHash),
|
||||
static_cast<unsigned long long>(pathA->payloadHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->payloadHash),
|
||||
static_cast<unsigned long long>(pathA->topologyHash),
|
||||
static_cast<unsigned long long>(analyticPathForward->topologyHash),
|
||||
packedAnalyticRenderSame?"PASS":"FAIL");
|
||||
return pass&&analyticDirectionsSame&&analyticForwardCovered==1
|
||||
&&analyticReverseCovered==1&&packedAnalyticRenderSame?0:12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2928,7 +3096,21 @@ int main(int Argc,char** Argv)
|
||||
else if(Arg.starts_with("--max-clip-level="))
|
||||
MaxClipLevelOverride=std::clamp(
|
||||
std::atoi(std::string(Arg.substr(17)).c_str()),0,8);
|
||||
else if(Arg=="--test-one-chunk-crossing")App.bTestOneChunkCrossing=true;
|
||||
else if(Arg=="--test-one-chunk-crossing")
|
||||
{
|
||||
App.FlightTargetCrossings=1;
|
||||
App.bShapeWorld=true;
|
||||
}
|
||||
else if(Arg.starts_with("--test-flight-crossings="))
|
||||
{
|
||||
App.FlightTargetCrossings=std::max(1,std::atoi(
|
||||
std::string(Arg.substr(std::string_view("--test-flight-crossings=").size())).c_str()));
|
||||
App.bShapeWorld=true;
|
||||
}
|
||||
else if(Arg.starts_with("--flight-speed-mps="))
|
||||
App.FlightSpeedMps=std::clamp(std::strtof(
|
||||
std::string(Arg.substr(std::string_view("--flight-speed-mps=").size())).c_str(),nullptr),
|
||||
1.0f,256.0f);
|
||||
else if(Arg=="--capture-after-clipmap")App.bCaptureAfterClipmap=true;
|
||||
else if(Arg=="--quit-after-clipmap")App.bQuitAfterClipmap=true;
|
||||
else if(Arg.starts_with("--workers="))
|
||||
|
||||
@@ -1040,9 +1040,49 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
|
||||
const int64 TileExtentVoxels = static_cast<int64>(Step) * CellsPerAxis;
|
||||
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
|
||||
const FStandaloneShapeWorld* ActiveShapeWorld=ShapeWorldContext;
|
||||
const bool bUseShapeWorldZBatch=ActiveShapeWorld!=nullptr
|
||||
&&ActiveShapeWorld->IsAnalyticOnly()
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid;
|
||||
const bool bUseShapeWorldXBatch=ActiveShapeWorld!=nullptr
|
||||
&&!bUseSharedDensityGrid&&!bCapturePreEditGrid;
|
||||
if (bUseVerticalCacheTraversal)
|
||||
if(bUseShapeWorldZBatch&&!bUseVerticalCacheTraversal)
|
||||
{
|
||||
for(const std::vector<int32>& YGroup:YGroups)
|
||||
for(const std::vector<int32>& XGroup:XGroups)
|
||||
for(const int32 gy:YGroup)
|
||||
for(const int32 gx:XGroup)
|
||||
{
|
||||
if(ShouldAbortWork())
|
||||
{
|
||||
if(bUseOperatorBlock){Generator->EndDensityBlock();}
|
||||
return FVoxelMeshData();
|
||||
}
|
||||
const int32 BatchCount=GzHi-GzLo+1;
|
||||
const int32 SampleX=OriginVoxels.X+gx*Step;
|
||||
const int32 SampleY=OriginVoxels.Y+gy*Step;
|
||||
float Densities[CHUNK_SIZE+3]{};
|
||||
ActiveShapeWorld->DensityAtLatticeZRun(SampleX,SampleY,
|
||||
OriginVoxels.Z+GzLo*Step,Step,BatchCount,Densities);
|
||||
for(int32 Lane=0;Lane<BatchCount;++Lane)
|
||||
{
|
||||
const int32 LocalGz=GzLo+Lane;
|
||||
const int32 LocalIndex=
|
||||
((LocalGz+1)*MDim+(gy+1))*MDim+(gx+1);
|
||||
DensityGrid[LocalIndex]=Densities[Lane];
|
||||
}
|
||||
const int32 ReachBlockX=FMath::Clamp(gx,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
const int32 ReachBlockY=FMath::Clamp(gy,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
const int32 ReachBlockZ=FMath::Clamp(GzHi,0,CellsPerAxis-1)
|
||||
/VoxelGenLOD::TileReachBlockCells;
|
||||
VoxelGenLOD::TileReachBlockIndex=
|
||||
((ReachBlockZ*ReachBlocksPerAxis)+ReachBlockY)
|
||||
*ReachBlocksPerAxis+ReachBlockX;
|
||||
if(OutDensitySampleCount)*OutDensitySampleCount+=BatchCount;
|
||||
}
|
||||
}
|
||||
else if (bUseVerticalCacheTraversal)
|
||||
{
|
||||
for (const std::vector<int32>& YGroup : YGroups)
|
||||
for (const std::vector<int32>& XGroup : XGroups)
|
||||
|
||||
Reference in new issue
Block a user