standalone: per-tile speed at plugin level, streaming like the game, softer lighting

Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it
constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6
(36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function.
Serial ms/tile, 64 nearest tiles per level (before -> after):
  L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145.
Hashes of all levels unchanged; parity 3,436/3,436 bit-identical.
Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is
high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial.
Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first;
one crossing = exactly 441 new tiles in 6.1 s, no stale results.
Lighting: normals already come from the density gradient; the striping was the squared headlamp;
broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-03 06:49:41 +02:00
1 parent 3f300b912a
commit 81b7d6d0ff
24 files changed
+1096 -196

No files matched your search

+349 -10
View File
@@ -1,6 +1,7 @@
#include "OwnerWorldLoader.h"
#include "StreamingWorld.h"
#include "StandaloneResearchTunnels.h"
#include "VoxelDensityProfile.h"
#ifdef TEXT
#undef TEXT
@@ -18,6 +19,8 @@
#include <windowsx.h>
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
@@ -105,9 +108,16 @@ namespace
};
struct FCrossingWork
{
uint64 SubmittedJobs=0;
uint64 PendingJobs=0;
uint64 FinishedJobs=0;
uint64 CancelledQueuedJobs=0;
uint64 StaleResults=0;
uint64 Tiles=0;
uint64 Samples=0;
double WorkerSeconds=0.0;
std::chrono::steady_clock::time_point StartTime{};
bool bReported=false;
};
FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile)
@@ -133,6 +143,9 @@ namespace
std::unique_ptr<FStandaloneOwnerWorld> World;
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
std::string DataPath;
bool bHasCameraOverride=false;
V3 CameraPositionOverride{};
float CameraYawOverride=0.0f,CameraPitchOverride=0.0f;
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
@@ -143,6 +156,7 @@ namespace
uint64 GeneratedByLevel[MaxLevels+1]{};
uint64 SamplesByLevel[MaxLevels+1]{};
double WorkerSecondsByLevel[MaxLevels+1]{};
uint64 ProfileCyclesByLevel[MaxLevels+1][VoxelDensityProfile::BucketCount]{};
std::vector<double> TileSecondsByLevel[MaxLevels+1];
uint64 CrossingIndex=0;
bool bGenerationRunning=false;
@@ -154,11 +168,23 @@ namespace
bool bAutoCaptureWritten=false;
bool bFullDetailReported=false;
bool bInitialClipmapReported=false;
bool bProfileAttribution=false;
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{};
bool bCaptureAfterClipmap=false;
bool bQuitAfterClipmap=false;
std::string CapturePath;
std::chrono::steady_clock::time_point GenerationStart{};
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
bool Keys[256]{};
int32 Seed=0;
int32 WorkerCount=6;
int32 Width=1280,Height=800;
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
V3 Position{};
@@ -190,7 +216,9 @@ namespace
StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear();
DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear();
for(int32 I=0;I<=MaxLevels;++I)
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;TileSecondsByLevel[I].clear();}
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;
std::fill(std::begin(ProfileCyclesByLevel[I]),std::end(ProfileCyclesByLevel[I]),0);
TileSecondsByLevel[I].clear();}
TotalVertices=0;TotalIndices=0;
}
@@ -335,9 +363,12 @@ O VS(I i){O o;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=normalize(i.n);return o;
float4 PS(O i):SV_TARGET{
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);Head=Head*Head*Atten;
float3 Rock=lerp(float3(0.28,0.34,0.38),float3(0.43,0.47,0.48),saturate(N.z*0.5+0.5));
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.62,0.76,0.9)*Head*1.25;
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);
float3 Rock=lerp(float3(0.30,0.22,0.16),float3(0.48,0.43,0.34),saturate(N.z*0.5+0.5));
// Keep the camera fill broad and low-amplitude. The old squared, high-intensity
// lobe made the 25 cm density-lattice normal variation read as horizontal bands.
Head=Head*Atten;
float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
float Fog=saturate((Dist-FogStart)/(FogEnd-FogStart));
Lit=lerp(Lit,float3(0.10,0.14,0.19),Fog);return float4(Lit,1);
})";
@@ -361,9 +392,9 @@ float4 PS(O i):SV_TARGET{
return Resize(Width,Height);
}
void PrintParity()
size_t PrintParity()
{
if(bParityChecked||!World)return;
if(bParityChecked||!World)return 0;
bParityChecked=true;size_t MismatchCount=0;
std::map<std::string,size_t> Kinds;
for(size_t I=0;I<World->DensitySamples.size();++I)
@@ -384,6 +415,7 @@ float4 PS(O i):SV_TARGET{
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
std::fflush(stdout);
std::fflush(stdout);
return MismatchCount;
}
bool ConfigureWorld(std::string& Error)
@@ -428,12 +460,22 @@ float4 PS(O i):SV_TARGET{
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
GenerationStart=std::chrono::steady_clock::now();
bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false;
if(bProfileAttribution)
{
VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution,128);
VoxelDensityProfile::CalibrateTimer();
VoxelDensityProfile::Reset();
std::printf("Density stage profiling enabled (Attribution, sample interval 128).\n");
}
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
DesiredTiles=Desired.Tiles;
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,6);
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount);
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
InitialWork.StartTime=GenerationStart;
bGenerationRunning=true;bRebuildPending=false;
std::printf("Streaming seed %d%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
Seed,bRoundTunnels?" with research round/winding tunnels":"",Update.DesiredTiles,
@@ -495,6 +537,15 @@ float4 PS(O i):SV_TARGET{
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
++CrossingIndex;
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex);
for(const auto& Pair:Update.CancelledByCrossing)
{
FCrossingWork& Work=CrossingWork[Pair.first];
Work.PendingJobs=Work.PendingJobs>=Pair.second?Work.PendingJobs-Pair.second:0;
Work.CancelledQueuedJobs+=Pair.second;
}
FCrossingWork& Work=CrossingWork[CrossingIndex];
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
Work.StartTime=std::chrono::steady_clock::now();
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",
static_cast<unsigned long long>(CrossingIndex),Update.CenterChunk.X,Update.CenterChunk.Y,Update.CenterChunk.Z,
@@ -502,6 +553,7 @@ float4 PS(O i):SV_TARGET{
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,Leavers.size(),
Update.CancelledQueuedJobs,Update.FullResTiles);
std::fflush(stdout);
ReportCrossingWork();
}
bool RebuildBatch(const FStandaloneBatchKey& Key)
@@ -621,10 +673,44 @@ float4 PS(O i):SV_TARGET{
std::printf("[TileTiming] LOD=%d generated=%llu target=%d samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms\n",
L,static_cast<unsigned long long>(GeneratedByLevel[L]),DesiredByLevel[L],
static_cast<double>(SamplesByLevel[L])/1.0e6,WorkerSecondsByLevel[L],Average*1000.0,P50*1000.0,P95*1000.0);
if(bProfileAttribution)
{
std::printf("[TileProfile] LOD=%d",L);
for(size_t I=0;I<VoxelDensityProfile::BucketCount;++I)
{
const double Ms=static_cast<double>(ProfileCyclesByLevel[L][I])
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
if(Ms>0.001)std::printf(" %s=%.3fms",
VoxelDensityProfile::BucketName(static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
}
std::printf("\n");
}
Times.clear();
}
}
void ReportCrossingWork()
{
for(auto& Pair:CrossingWork)
{
FCrossingWork& Work=Pair.second;
if(Work.bReported||Work.PendingJobs!=0)continue;
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
?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 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),
static_cast<unsigned long long>(Work.Tiles),
static_cast<unsigned long long>(Work.CancelledQueuedJobs),
static_cast<unsigned long long>(Work.StaleResults),
static_cast<double>(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs);
}
}
void PumpGeneration()
{
if(bRebuildPending){StartGeneration();return;}
@@ -632,11 +718,16 @@ float4 PS(O i):SV_TARGET{
std::vector<FStandaloneCpuTile> Ready=Streamer->TakeReady();
for(FStandaloneCpuTile& Tile:Ready)
{
if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))continue;
const int32 Level=Tile.Key.Level;
FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing];
++Crossing.Tiles;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds;
if(Crossing.PendingJobs>0)--Crossing.PendingJobs;
++Crossing.FinishedJobs;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds;
if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))
{++Crossing.StaleResults;continue;}
++Crossing.Tiles;
++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
if(bProfileAttribution)for(size_t I=0;I<Tile.ProfileCycles.size();++I)
ProfileCyclesByLevel[Level][I]+=Tile.ProfileCycles[I];
TileSecondsByLevel[Level].push_back(Tile.WorkerSeconds);
FResidentTileMesh Resident;Resident.Vertices=std::move(Tile.Vertices);Resident.Indices=std::move(Tile.Indices);
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
@@ -644,7 +735,20 @@ float4 PS(O i):SV_TARGET{
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
}
ReportCrossingWork();
UploadDirtyBatches();
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
&&DirtyBatchSet.empty())
{
const auto Crossing=CrossingWork.find(1);
if(Crossing!=CrossingWork.end()&&Crossing->second.bReported)
{
bCrossingClosePosted=true;
std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n");
std::fflush(stdout);
PostMessageA(Window,WM_CLOSE,0,0);
}
}
if(!bFullDetailReported&&IsFullDetailUploaded())
{
bFullDetailReported=true;
@@ -661,13 +765,15 @@ float4 PS(O i):SV_TARGET{
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
ReportTileTimings();std::fflush(stdout);
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
}
bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty();
}
void CaptureRenderedFrame()
{
if(CapturePath.empty()||bAutoCaptureWritten||!IsFullDetailUploaded())return;
const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded();
if(CapturePath.empty()||bAutoCaptureWritten||!bReadyToCapture)return;
bAutoCaptureWritten=SaveBackBufferPng(CapturePath);
if(bAutoCaptureWritten)
{
@@ -698,6 +804,21 @@ float4 PS(O i):SV_TARGET{
{
PumpGeneration();
const auto Now=std::chrono::steady_clock::now();
if(bTestOneChunkCrossing&&bFullDetailReported&&!bCrossingMoveStarted&&!bCrossingMoveFinished)
{
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");
std::fflush(stdout);
}
if(bCrossingMoveStarted&&Now>=CrossingMoveStopTime)
{
Keys[VK_SHIFT]=false;Keys['W']=false;
bCrossingMoveStarted=false;bCrossingMoveFinished=true;
std::printf("[CrossingTestMove] movement stopped; waiting for crossing work and uploads.\n");
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));
@@ -767,23 +888,241 @@ float4 PS(O i):SV_TARGET{
const std::filesystem::path Exe(Module);
return Exe.parent_path().parent_path()/L"data"/L"owner_world.json";
}
int RunTileBenchmark(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 Level,
int32 Count,bool bProfile,bool bCompareEvaluators,bool bDensityHash)
{
const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0);
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels);
std::vector<FVoxelTileKey> Tiles;Tiles.reserve(static_cast<size_t>(std::max(Count,1)));
for(const FVoxelTileKey& Tile:Desired.Tiles)
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
if(Tiles.empty())
{std::fprintf(stderr,"No LOD %d tiles are in the current desired set.\n",Level);return 4;}
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
int32 TileCacheWindow=1,TileCacheWindowMinLOD=3,RoomGraphCacheSlots=1;
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindow=",TileCacheWindow);
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindowMinLOD=",TileCacheWindowMinLOD);
FParse::Value(FCommandLine::Get(),"voxel.RoomGraphCacheSlots=",RoomGraphCacheSlots);
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d command='%s'\n",
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,FCommandLine::Get());
VoxelDensityProfile::SetMode(bProfile?VoxelDensityProfile::EMode::Attribution
:VoxelDensityProfile::EMode::Disabled,128);
if(bProfile)VoxelDensityProfile::CalibrateTimer();
VoxelDensityProfile::Reset();
const VoxelDensityProfile::FSnapshot Before=VoxelDensityProfile::SnapshotCurrentThread();
UVoxelMarchingCubesMesher Mesher;
Mesher.SetGenerator(World.Generator.get());Mesher.bGenerateSkirts=World.bGenerateSkirts;
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);Mesher.LODOctaveDrop=World.LODOctaveDrop;
const int32 BandMin=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
?Desired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
const int32 BandMax=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
?(Desired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
if(bCompareEvaluators)
{
const bool bOriginalFused=VoxelGenLOD::IsUseFusedEvaluatorEnabled();
uint64 Mismatches=0,ComparedSamples=0,MeshCountMismatches=0;
float MaxAbsoluteDifference=0.0f;
size_t FirstMismatch=SIZE_MAX;
FVoxelTileKey FirstMismatchTile{};
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
for(const FVoxelTileKey& Tile:Tiles)
{
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
std::vector<float> FusedDensities,OpStackDensities;
int64 FusedSampleCalls=0,OpStackSampleCalls=0;
FVoxelMeshData FusedMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&FusedSampleCalls,nullptr,nullptr,&FusedDensities);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(false);
FVoxelMeshData OpStackMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&OpStackSampleCalls,nullptr,nullptr,&OpStackDensities);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
if(FusedMesh.Vertices.Num()!=OpStackMesh.Vertices.Num()
|| FusedMesh.Triangles.Num()!=OpStackMesh.Triangles.Num())++MeshCountMismatches;
const size_t SharedSamples=std::min(FusedDensities.size(),OpStackDensities.size());
ComparedSamples+=SharedSamples;
for(size_t I=0;I<SharedSamples;++I)
{
if(std::bit_cast<uint32>(FusedDensities[I])!=std::bit_cast<uint32>(OpStackDensities[I]))
{
if(FirstMismatch==SIZE_MAX){FirstMismatch=I;FirstMismatchTile=Tile;}
++Mismatches;
MaxAbsoluteDifference=std::max(MaxAbsoluteDifference,
std::abs(FusedDensities[I]-OpStackDensities[I]));
}
}
Mismatches+=static_cast<uint64>(std::max(FusedDensities.size(),OpStackDensities.size())-SharedSamples);
}
std::printf("[EvaluatorCompare] LOD=%d tiles=%zu compared_samples=%llu density_mismatches=%llu first_tile=(%d,%d,%d) first_index=%llu max_abs=%.9g mesh_count_mismatch_tiles=%llu original_fused=%d\n",
Level,Tiles.size(),static_cast<unsigned long long>(ComparedSamples),
static_cast<unsigned long long>(Mismatches),
FirstMismatchTile.Coord.X,FirstMismatchTile.Coord.Y,FirstMismatchTile.Coord.Z,
static_cast<unsigned long long>(FirstMismatch),MaxAbsoluteDifference,
static_cast<unsigned long long>(MeshCountMismatches),bOriginalFused?1:0);
VoxelGenLOD::SetUseFusedEvaluatorEnabled(bOriginalFused);
std::fflush(stdout);
return Mismatches==0?0:5;
}
std::vector<double> TileSeconds;TileSeconds.reserve(Tiles.size());
uint64 Samples=0,Vertices=0,Indices=0;
uint64 DensityHash=14695981039346656037ull;
const auto TotalStart=std::chrono::steady_clock::now();
for(const FVoxelTileKey& Tile:Tiles)
{
const auto TileStart=std::chrono::steady_clock::now();
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
int64 TileSamples=0;
std::vector<float> DebugDensitySamples;
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
BandMin,BandMax,&TileSamples,nullptr,nullptr,
bDensityHash?&DebugDensitySamples:nullptr);
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
Vertices+=static_cast<uint64>(Mesh.Vertices.Num());
Indices+=static_cast<uint64>(Mesh.Triangles.Num());
if(bDensityHash)
{
for(const float Density:DebugDensitySamples)
{
DensityHash^=std::bit_cast<uint32>(Density);
DensityHash*=1099511628211ull;
}
}
TileSeconds.push_back(std::chrono::duration<double>(
std::chrono::steady_clock::now()-TileStart).count());
}
const double TotalSeconds=std::chrono::duration<double>(
std::chrono::steady_clock::now()-TotalStart).count();
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
std::sort(TileSeconds.begin(),TileSeconds.end());
const double Average=TotalSeconds/static_cast<double>(Tiles.size());
std::printf("[TileBenchmark] LOD=%d generated=%zu samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms vertices=%llu indices=%llu profiling=%s\n",
Level,Tiles.size(),static_cast<double>(Samples)/1.0e6,TotalSeconds,Average*1000.0,
TileSeconds[(TileSeconds.size()-1)*50/100]*1000.0,
TileSeconds[(TileSeconds.size()-1)*95/100]*1000.0,
static_cast<unsigned long long>(Vertices),static_cast<unsigned long long>(Indices),
bProfile?"Attribution":"off");
if(bDensityHash)
{
std::printf("[TileDensityHash] LOD=%d tiles=%zu hash=0x%016llx\n",Level,Tiles.size(),
static_cast<unsigned long long>(DensityHash));
}
if(bProfile)
{
std::printf("[TileProfile] LOD=%d",Level);
for(int32 I=0;I<VoxelDensityProfile::BucketCount;++I)
{
const uint64 CycleDelta=After.Cycles[I]>=Before.Cycles[I]
?After.Cycles[I]-Before.Cycles[I]:0;
const double Ms=static_cast<double>(CycleDelta)
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
if(Ms>0.001)std::printf(" %s=%.3fms",VoxelDensityProfile::BucketName(
static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
}
std::printf("\n");
const auto CounterDelta=[&](VoxelDensityProfile::ECounter Counter)
{
const size_t I=static_cast<size_t>(Counter);
return After.Counters[I]>=Before.Counters[I]
?After.Counters[I]-Before.Counters[I]:0;
};
std::printf("[TileCounters] sdf_builds=%llu graph_tile_builds=%llu graph_core_builds=%llu op_block_builds=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)));
std::printf("[SdfRebuildCauses] strate=%llu seed=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_policy=%llu tile_policy=%llu tile_identity=%llu bounds_x=%llu bounds_y=%llu tile_window_accepted=%llu tile_window_rejected=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildStrate)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildSeed)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFingerprint)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildLayout)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTerrainOps)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTilePolicy)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTileIdentity)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsX)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsY)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowAccepted)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowRejected)));
std::printf("[TilePolicyKeyChanges] origin=%llu step=%llu cells=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_path=%llu context=%llu\n",
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyOrigin)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyStep)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyCells)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyLayout)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyManager)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath)),
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyContext)));
}
std::fflush(stdout);
return 0;
}
}
int main(int Argc,char** Argv)
{
FApp App;App.DataPath=DefaultDataPath().string();
std::string JoinedCommandLine;
for(int32 I=1;I<Argc;++I)
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
FCommandLine::Set(JoinedCommandLine);
int32 BenchmarkLevel=-1,BenchmarkCount=64;
for(int32 I=1;I<Argc;++I)
{
const std::string_view Arg=Argv[I];
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
else if(Arg=="--profile-attribution")App.bProfileAttribution=true;
else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true;
else if(Arg=="--parity-only")App.bParityOnly=true;
else if(Arg=="--density-hash")App.bDensityHash=true;
else if(Arg=="--round-tunnels")App.bRoundTunnels=true;
else if(Arg=="--test-one-chunk-crossing")App.bTestOneChunkCrossing=true;
else if(Arg=="--capture-after-clipmap")App.bCaptureAfterClipmap=true;
else if(Arg=="--quit-after-clipmap")App.bQuitAfterClipmap=true;
else if(Arg.starts_with("--workers="))
{
const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str());
App.WorkerCount=std::clamp(Parsed,1,32);
}
else if(Arg.starts_with("--benchmark-level="))
BenchmarkLevel=std::atoi(std::string(Arg.substr(18)).c_str());
else if(Arg.starts_with("--benchmark-count="))
BenchmarkCount=std::max(1,std::atoi(std::string(Arg.substr(18)).c_str()));
else if(Arg.starts_with("--camera="))
{
const std::string Values(Arg.substr(9));
float X=0,Y=0,Z=0,YawDegrees=0,PitchDegrees=0;
if(std::sscanf(Values.c_str(),"%f,%f,%f,%f,%f",&X,&Y,&Z,&YawDegrees,&PitchDegrees)==5)
{
App.bHasCameraOverride=true;App.CameraPositionOverride={X,Y,Z};
App.CameraYawOverride=YawDegrees*0.017453292519943295f;
App.CameraPitchOverride=PitchDegrees*0.017453292519943295f;
}
else {std::fprintf(stderr,"Invalid --camera value: %s\n",Values.c_str());return 2;}
}
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
}
std::string Error;
App.World=std::make_unique<FStandaloneOwnerWorld>();
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
App.Seed=App.World->Seed;
if(App.bParityOnly)
{
VoxelGenLOD::InitializeConsoleSwitches();
return App.PrintParity()==0?0:1;
}
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
if(App.bHasCameraOverride){App.Position=App.CameraPositionOverride;App.Yaw=App.CameraYawOverride;App.Pitch=App.CameraPitchOverride;}
if(BenchmarkLevel>=0)
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash);
App.World.reset();
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
+45 -9
View File
@@ -3,6 +3,7 @@
#include "WorldMesher.h"
#include <condition_variable>
#include <array>
#include <unordered_set>
struct FStandaloneTileKeyHash
@@ -30,6 +31,7 @@ struct FStandaloneCpuTile
std::vector<uint32> Indices;
uint64 DensitySamples=0;
double WorkerSeconds=0.0;
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
};
struct FStandaloneStreamingUpdate
@@ -40,6 +42,7 @@ struct FStandaloneStreamingUpdate
int32 NewJobs=0;
int32 CancelledQueuedJobs=0;
int32 FullResTiles=0;
std::vector<std::pair<uint64,uint64>> CancelledByCrossing;
};
struct FStandaloneTileJob
@@ -48,6 +51,20 @@ struct FStandaloneTileJob
int32 BandZMin=MIN_int32;
int32 BandZMax=MAX_int32;
uint64 RequestedCrossing=0;
double PriorityDistanceSquared=0.0;
};
struct FStandaloneTileJobHeapOrder
{
bool operator()(const FStandaloneTileJob& A,const FStandaloneTileJob& B)const
{
if(A.PriorityDistanceSquared!=B.PriorityDistanceSquared)
return A.PriorityDistanceSquared>B.PriorityDistanceSquared;
if(A.Key.Level!=B.Key.Level)return A.Key.Level>B.Key.Level;
if(A.Key.Coord.Z!=B.Key.Coord.Z)return A.Key.Coord.Z>B.Key.Coord.Z;
if(A.Key.Coord.Y!=B.Key.Coord.Y)return A.Key.Coord.Y>B.Key.Coord.Y;
return A.Key.Coord.X>B.Key.Coord.X;
}
};
namespace StandaloneWorldMesher
@@ -99,6 +116,7 @@ public:
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=6)
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,32))
{
VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
}
FStandaloneStreamingGenerator(const FStandaloneStreamingGenerator&)=delete;
@@ -153,12 +171,20 @@ public:
Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1);
for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key);
FullResTileCount=Stats.FullResTiles;
std::unordered_map<uint64,uint64> CancelledByCrossing;
auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job)
{
if(Desired.contains(Job.Key))return false;
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;return true;
Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;
++CancelledByCrossing[Job.RequestedCrossing];return true;
});
Queue.erase(QueueEnd,Queue.end());
Stats.CancelledByCrossing.reserve(CancelledByCrossing.size());
for(const auto& Pair:CancelledByCrossing)Stats.CancelledByCrossing.push_back(Pair);
for(FStandaloneTileJob& Job:Queue)
Job.PriorityDistanceSquared=StandaloneWorldMesher::TileDistanceSquaredChunks(
Job.Key,NewDesired.CenterChunk);
const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate
? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
@@ -167,16 +193,12 @@ public:
for(const FVoxelTileKey& Key:NewDesired.Tiles)
{
if(Resident.contains(Key)||Outstanding.contains(Key))continue;
Queue.push_back({Key,BandMin,BandMax,CrossingIndex});
Queue.push_back({Key,BandMin,BandMax,CrossingIndex,
StandaloneWorldMesher::TileDistanceSquaredChunks(Key,NewDesired.CenterChunk)});
Outstanding.insert(Key);++Stats.NewJobs;
}
CurrentDesired=std::move(NewDesired);
std::sort(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& A,const FStandaloneTileJob& B)
{
const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A.Key,CurrentDesired.CenterChunk);
const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B.Key,CurrentDesired.CenterChunk);
return DA==DB?A.Key.Level<B.Key.Level:DA<DB;
});
std::make_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Wake.notify_all();
return Stats;
}
@@ -215,8 +237,21 @@ private:
const int32 Cells=Level<World->FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells);
const int32 Step=FMath::Max(1,Extent/Cells);
int64 SampleCount=0;
const bool bProfile=VoxelDensityProfile::GetMode()!=VoxelDensityProfile::EMode::Disabled;
const VoxelDensityProfile::FSnapshot Before=bProfile
? VoxelDensityProfile::SnapshotCurrentThread():VoxelDensityProfile::FSnapshot{};
FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr,
Job.BandZMin,Job.BandZMax,&SampleCount);
if(bProfile)
{
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
for(size_t I=0;I<Out.ProfileCycles.size();++I)
{
const size_t Bucket=I;
Out.ProfileCycles[I]=After.Cycles[Bucket]>=Before.Cycles[Bucket]
?After.Cycles[Bucket]-Before.Cycles[Bucket]:0;
}
}
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
for(int32 I=0;I<Mesh.Vertices.Num();++I)
@@ -241,7 +276,8 @@ private:
std::unique_lock<std::mutex> Lock(Mutex);
Wake.wait(Lock,[&](){return bStop||!Queue.empty();});
if(bStop)return;
Job=Queue.front();Queue.erase(Queue.begin());
std::pop_heap(Queue.begin(),Queue.end(),FStandaloneTileJobHeapOrder{});
Job=Queue.back();Queue.pop_back();
}
FStandaloneCpuTile Tile=Generate(Job);
{
@@ -7750,7 +7750,7 @@ void VoxelCaveMorphology::BuildChunkCache(
GraphLowLink.Init(INDEX_NONE, NumRooms);
GraphBridges.Init(0u, GraphEdges.Num());
int32 GraphDiscoveryTime = 0;
TFunction<void(int32, int32)> VisitGraph = [&](int32 Node, int32 ParentEdge)
const auto VisitGraph = [&](auto&& VisitGraphSelf, int32 Node, int32 ParentEdge) -> void
{
GraphDiscovery[Node] = GraphDiscoveryTime;
GraphLowLink[Node] = GraphDiscoveryTime;
@@ -7765,7 +7765,7 @@ void VoxelCaveMorphology::BuildChunkCache(
const int32 Other = Edge.RoomA == Node ? Edge.RoomB : Edge.RoomA;
if (GraphDiscovery[Other] == INDEX_NONE)
{
VisitGraph(Other, EdgeIndex);
VisitGraphSelf(VisitGraphSelf, Other, EdgeIndex);
GraphLowLink[Node] = FMath::Min(
GraphLowLink[Node], GraphLowLink[Other]);
if (GraphLowLink[Other] > GraphDiscovery[Node])
@@ -7784,7 +7784,7 @@ void VoxelCaveMorphology::BuildChunkCache(
{
if (GraphDiscovery[Node] == INDEX_NONE)
{
VisitGraph(Node, INDEX_NONE);
VisitGraph(VisitGraph, Node, INDEX_NONE);
}
}
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
@@ -7803,7 +7803,7 @@ void VoxelCaveMorphology::BuildChunkCache(
TArray<uint8, TInlineAllocator<128>> GraphForestEdges;
GraphForestVisited.Init(0u, NumRooms);
GraphForestEdges.Init(0u, GraphEdges.Num());
TFunction<void(int32)> VisitWalkForest = [&](int32 Node)
const auto VisitWalkForest = [&](auto&& VisitWalkForestSelf, int32 Node) -> void
{
GraphForestVisited[Node] = 1u;
for (const int32 EdgeIndex : GraphAdjacency[Node])
@@ -7815,14 +7815,14 @@ void VoxelCaveMorphology::BuildChunkCache(
continue;
}
GraphForestEdges[EdgeIndex] = 1u;
VisitWalkForest(Other);
VisitWalkForestSelf(VisitWalkForestSelf, Other);
}
};
for (int32 Node = 0; Node < NumRooms; ++Node)
{
if (GraphForestVisited[Node] == 0u)
{
VisitWalkForest(Node);
VisitWalkForest(VisitWalkForest, Node);
}
}
for (int32 EdgeIndex = 0; EdgeIndex < GraphEdges.Num(); ++EdgeIndex)
@@ -704,6 +704,28 @@ namespace VoxelDensityProfile
case ECounter::MesherOutputAllocatedBytes: return TEXT("MesherOutputAllocatedBytes");
case ECounter::OpBlockScratchBytes: return TEXT("OpBlockScratchBytes");
case ECounter::OpBlockCopyBytes: return TEXT("OpBlockCopyBytes");
case ECounter::SdfRebuildStrate: return TEXT("SdfRebuildStrate");
case ECounter::SdfRebuildSeed: return TEXT("SdfRebuildSeed");
case ECounter::SdfRebuildFingerprint: return TEXT("SdfRebuildFingerprint");
case ECounter::SdfRebuildLayout: return TEXT("SdfRebuildLayout");
case ECounter::SdfRebuildManagerLifetime: return TEXT("SdfRebuildManagerLifetime");
case ECounter::SdfRebuildTerrainOps: return TEXT("SdfRebuildTerrainOps");
case ECounter::SdfRebuildFusedPolicy: return TEXT("SdfRebuildFusedPolicy");
case ECounter::SdfRebuildTilePolicy: return TEXT("SdfRebuildTilePolicy");
case ECounter::SdfRebuildTileIdentity: return TEXT("SdfRebuildTileIdentity");
case ECounter::SdfRebuildBoundsX: return TEXT("SdfRebuildBoundsX");
case ECounter::SdfRebuildBoundsY: return TEXT("SdfRebuildBoundsY");
case ECounter::SdfTileWindowAccepted: return TEXT("SdfTileWindowAccepted");
case ECounter::SdfTileWindowRejected: return TEXT("SdfTileWindowRejected");
case ECounter::TilePolicyKeyOrigin: return TEXT("TilePolicyKeyOrigin");
case ECounter::TilePolicyKeyStep: return TEXT("TilePolicyKeyStep");
case ECounter::TilePolicyKeyCells: return TEXT("TilePolicyKeyCells");
case ECounter::TilePolicyKeyFingerprint: return TEXT("TilePolicyKeyFingerprint");
case ECounter::TilePolicyKeyLayout: return TEXT("TilePolicyKeyLayout");
case ECounter::TilePolicyKeyManager: return TEXT("TilePolicyKeyManager");
case ECounter::TilePolicyKeyTerrainOps: return TEXT("TilePolicyKeyTerrainOps");
case ECounter::TilePolicyKeyFusedPath: return TEXT("TilePolicyKeyFusedPath");
case ECounter::TilePolicyKeyContext: return TEXT("TilePolicyKeyContext");
case ECounter::Count: break;
}
return TEXT("Unknown");
@@ -32,6 +32,7 @@
#endif
#include <atomic>
#include <array>
namespace VoxelMath
{
@@ -139,6 +140,24 @@ namespace
TEXT("voxel.TileCacheWindowOpStackMinLOD"),
GVoxelForgeTileCacheWindowOpStackMinLOD,
TEXT("Minimum SampleStep LOD for the op-stack tile-sized room cache window."));
int32 GVoxelForgeRoomGraphCacheSlots = 1;
FAutoConsoleVariableRef CVarVoxelForgeRoomGraphCacheSlots(
TEXT("voxel.RoomGraphCacheSlots"),
GVoxelForgeRoomGraphCacheSlots,
TEXT("Per-worker exact room-graph windows to retain during density sampling (1-4)."));
bool GVoxelForgeRoomGraphCacheSlotsParsed = false;
void VF_ParseRoomGraphCacheSlots()
{
if (GVoxelForgeRoomGraphCacheSlotsParsed) return;
GVoxelForgeRoomGraphCacheSlotsParsed = true;
int32 CommandLineValue = GVoxelForgeRoomGraphCacheSlots;
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.RoomGraphCacheSlots="), CommandLineValue))
{
GVoxelForgeRoomGraphCacheSlots = CommandLineValue;
}
GVoxelForgeRoomGraphCacheSlots = FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4);
}
int32 GVoxelForgeSpatialIndex = 1;
FAutoConsoleVariableRef CVarVoxelForgeSpatialIndex(
@@ -879,10 +898,23 @@ void VoxelGenLOD::InitializeConsoleSwitches()
VF_ParseFusedEvaluatorSwitch();
VF_ParseWormBlockSkipSwitch();
VF_ParseTileCacheWindowSwitch();
VF_ParseRoomGraphCacheSlots();
VF_ParseSpatialIndexSwitch();
VF_ParseChunkRoutingCacheSwitch();
}
void VoxelGenLOD::SetUseFusedEvaluatorEnabled(bool bEnabled)
{
VF_ParseFusedEvaluatorSwitch();
GVoxelForgeUseFusedEvaluator = bEnabled ? 1 : 0;
}
bool VoxelGenLOD::IsUseFusedEvaluatorEnabled()
{
VF_ParseFusedEvaluatorSwitch();
return GVoxelForgeUseFusedEvaluator != 0;
}
bool VoxelGenLOD::IsTileCacheWindowEnabled(bool bFusedPath)
{
if (GVoxelForgeTileCacheWindow == 0)
@@ -4171,38 +4203,31 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
//
// Declared before the RoomDensity if-block so SDFCache and NearestRoomIdx
// are also visible to the terrain ops block further below.
thread_local FChunkSDFCache SDFCache;
// Cache validity is tracked by the SEARCH BOX the cache was built for, NOT by chunk
// equality. Gradient-normal sampling queries at WorldX±1 (and warp displacement) can
// step a voxel outside the chunk; with chunk-equality keying that flipped the key and
// rebuilt the (now expensive) cache every boundary cell. Since the stored rooms cover
// the search box + MaxInfluence, any query INSIDE the box is correct — so we only
// rebuild when the query actually leaves the box. Result: one build per chunk, no thrash.
thread_local float CachedSMinX = 1.0f, CachedSMaxX = -1.0f; // start invalid (min > max)
thread_local float CachedSMinY = 0.0f, CachedSMaxY = 0.0f;
thread_local int32 CachedStrate = INT32_MIN;
thread_local uint32 CachedSeed = 0;
// ⚠️ AUDIT §C2 — `CachedFingerprint` et `CachedLayout` ci-dessous sont OBLIGATOIRES dans cette clé.
// La clé ci-dessus décrit la GÉOMÉTRIE de la fenêtre (boîte, strate, seed) et rien de ce qui
// détermine les PARAMS avec lesquels les salles ont été cuites. Comme `GetGenerationParams`
// blende à l'intérieur d'une strate (`Alpha` = f(chunk Z), et f(chunk XY) aussi en
// `Interleaved`), deux chunks voisins produisent la MÊME clé avec des params DIFFÉRENTS, et le
// deuxième se sert des salles du premier. Non déterministe entre pairs, parce que l'ordre des
// workers décide lequel est « le premier » — exactement ce que §2.6.1 interdit.
//
// Pourquoi ça ne casse PAS l'invariant de perf de §8.10 : la clé reste une BOÎTE, donc les
// sondes de gradient à `WorldX ± 1` ne font toujours pas tourner le cache. Ce qui le fait
// tourner en plus, c'est un changement RÉEL de params — une fois par chunk dans une bande de
// transition, ce qui est exactement le nombre de reconstructions nécessaire.
thread_local uint32 CachedFingerprint = 0xFFFFFFFFu;
thread_local uint32 CachedLayout = 0xFFFFFFFFu;
thread_local bool CachedUsesFusedCacheWindow = false;
thread_local bool CachedUsesTileCacheWindow = false;
thread_local FIntVector CachedTileOrigin = FIntVector::ZeroValue;
thread_local int32 CachedTileStep = 0;
thread_local int32 CachedTileCells = 0;
thread_local uint64 CachedManagerLifetimeId = 0;
thread_local const TArray<FStrateTerrainOpEntry>* CachedTerrainOps = nullptr;
// Keep a small bounded set of exact room-graph windows. Tall 32-chunk tiles need two slots
// for their two strata; other tile extents start with one. Every slot uses the full cache
// identity/bounds.
struct FRoomGraphCacheSlot
{
FChunkSDFCache Cache;
float SMinX = 1.0f, SMaxX = -1.0f;
float SMinY = 0.0f, SMaxY = 0.0f;
int32 Strate = INT32_MIN;
uint32 Seed = 0;
uint32 Fingerprint = 0xFFFFFFFFu;
uint32 Layout = 0xFFFFFFFFu;
bool bUsesFusedCacheWindow = false;
bool bUsesTileCacheWindow = false;
FIntVector TileOrigin = FIntVector::ZeroValue;
int32 TileStep = 0;
int32 TileCells = 0;
uint64 ManagerLifetimeId = 0;
const TArray<FStrateTerrainOpEntry>* TerrainOps = nullptr;
uint64 LastUse = 0;
bool bValid = false;
};
static thread_local std::array<FRoomGraphCacheSlot, 4> RoomGraphCacheSlots{};
static thread_local uint64 RoomGraphCacheUseSerial = 0;
VF_ParseRoomGraphCacheSlots();
// The envelope proof is constant for a cache-policy key. Do not run the complete room/edge
// reach calculation for every density sample: that would turn a build-time guard into a
// per-sample cost on the LOD0 floor.
@@ -4267,6 +4292,32 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
|| CachedTilePolicyTerrainOps != TerrainOps;
if (bTilePolicyKeyChanged)
{
if (VoxelDensityProfile::AreCountersEnabled())
{
const auto CountPolicyKeyChange = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
{
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
};
CountPolicyKeyChange(CachedTilePolicyOrigin != RequestedTileOrigin,
VoxelDensityProfile::ECounter::TilePolicyKeyOrigin);
CountPolicyKeyChange(CachedTilePolicySampleStep != RequestedTileStep
|| CachedTilePolicyStep != RequestedTileStep,
VoxelDensityProfile::ECounter::TilePolicyKeyStep);
CountPolicyKeyChange(CachedTilePolicyCells != RequestedTileCells,
VoxelDensityProfile::ECounter::TilePolicyKeyCells);
CountPolicyKeyChange(CachedTilePolicyFingerprint != ParamsFingerprint,
VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint);
CountPolicyKeyChange(CachedTilePolicyLayout != LayoutVersion,
VoxelDensityProfile::ECounter::TilePolicyKeyLayout);
CountPolicyKeyChange(CachedTilePolicyManagerLifetimeId != CurrentManagerLifetimeId,
VoxelDensityProfile::ECounter::TilePolicyKeyManager);
CountPolicyKeyChange(CachedTilePolicyTerrainOps != TerrainOps,
VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps);
CountPolicyKeyChange(CachedTilePolicyFusedPath != bUseFusedCacheWindow,
VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath);
CountPolicyKeyChange(CachedTilePolicyHasContext != (bTileWindowEligible && bHasTileWindowContext),
VoxelDensityProfile::ECounter::TilePolicyKeyContext);
}
CachedTilePolicyValid = true;
CachedTilePolicyHasContext = bTileWindowEligible && bHasTileWindowContext;
CachedTilePolicyFusedPath = bUseFusedCacheWindow;
@@ -4280,6 +4331,12 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
CachedTilePolicyTerrainOps = TerrainOps;
CachedTilePolicyResult = CachedTilePolicyHasContext
&& VoxelCaveMorphology::IsRoomGraphWindowInvariant(Params, TerrainOps);
if (VoxelDensityProfile::AreCountersEnabled() && CachedTilePolicyHasContext)
{
VoxelDensityProfile::AddCounter(CachedTilePolicyResult
? VoxelDensityProfile::ECounter::SdfTileWindowAccepted
: VoxelDensityProfile::ECounter::SdfTileWindowRejected);
}
}
const bool bUseTileCacheWindow = CachedTilePolicyResult;
@@ -4290,36 +4347,137 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
float CaveSDF = FLT_MAX;
// Get strate index for unique caves per strate. The index changes only at chunk-Z
// boundaries and is also part of the worker-local graph-cache identity.
int32 StrateIdx = 0;
if (StrateManager)
{
thread_local int32 SI_ChunkZ = INT32_MAX;
thread_local uint32 SI_Version = 0xFFFFFFFFu;
thread_local int32 SI_Index = 0;
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
const uint32 LV = StrateManager->GetLayoutVersion();
if (QZ != SI_ChunkZ || LV != SI_Version)
{
SI_ChunkZ = QZ;
SI_Version = LV;
SI_Index = StrateManager->GetStrateIndex(
((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
}
StrateIdx = SI_Index;
}
const float ChunkMinX = CacheRegionX * static_cast<float>(CacheRegionSize);
const float ChunkMinY = CacheRegionY * static_cast<float>(CacheRegionSize);
const float ChunkMaxX = ChunkMinX + static_cast<float>(CacheRegionSize);
const float ChunkMaxY = ChunkMinY + static_cast<float>(CacheRegionSize);
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f;
float DesiredSMinX = ChunkMinX;
float DesiredSMinY = ChunkMinY;
float DesiredSMaxX = ChunkMaxX;
float DesiredSMaxY = ChunkMaxY;
if (bUseTileCacheWindow)
{
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
const float TileExtent = static_cast<float>(
static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells));
const float TileMaxX = TileMinX + TileExtent;
const float TileMaxY = TileMinY + TileExtent;
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
const float Expansion = WarpMargin + TileHalo;
DesiredSMinX = TileMinX - Expansion;
DesiredSMinY = TileMinY - Expansion;
DesiredSMaxX = TileMaxX + Expansion;
DesiredSMaxY = TileMaxY + Expansion;
}
else
{
const float Expansion = WarpMargin + 2.0f;
DesiredSMinX = ChunkMinX - Expansion;
DesiredSMinY = ChunkMinY - Expansion;
DesiredSMaxX = ChunkMaxX + Expansion;
DesiredSMaxY = ChunkMaxY + Expansion;
}
const int64 RequestedTileExtent = static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells);
const bool bNeedsTwoStrataWindows = bHasTileWindowContext
&& RequestedTileExtent == static_cast<int64>(CHUNK_SIZE) * 32;
const int32 ActiveCacheSlots = FMath::Max(
FMath::Clamp(GVoxelForgeRoomGraphCacheSlots, 1, 4),
bNeedsTwoStrataWindows ? 2 : 1);
FRoomGraphCacheSlot* CacheSlot = nullptr;
FRoomGraphCacheSlot* EvictionSlot = &RoomGraphCacheSlots[0];
for (int32 SlotIndex = 0; SlotIndex < ActiveCacheSlots; ++SlotIndex)
{
FRoomGraphCacheSlot& Candidate = RoomGraphCacheSlots[SlotIndex];
if (!Candidate.bValid)
{
EvictionSlot = &Candidate;
break;
}
if (Candidate.LastUse < EvictionSlot->LastUse)
{
EvictionSlot = &Candidate;
}
const bool bSameKey = Candidate.Strate == StrateIdx
&& Candidate.Seed == static_cast<uint32>(Seed)
&& Candidate.Fingerprint == ParamsFingerprint
&& Candidate.Layout == LayoutVersion
&& Candidate.ManagerLifetimeId == CurrentManagerLifetimeId
&& Candidate.TerrainOps == TerrainOps
&& Candidate.bUsesFusedCacheWindow == bUseFusedCacheWindow
&& Candidate.bUsesTileCacheWindow == bUseTileCacheWindow
&& (!bUseTileCacheWindow
|| (Candidate.TileOrigin == RequestedTileOrigin
&& Candidate.TileStep == RequestedTileStep
&& Candidate.TileCells == RequestedTileCells));
const bool bSameBounds = Candidate.SMinX == DesiredSMinX
&& Candidate.SMaxX == DesiredSMaxX
&& Candidate.SMinY == DesiredSMinY
&& Candidate.SMaxY == DesiredSMaxY;
const bool bContainsQuery = WarpedX >= Candidate.SMinX
&& WarpedX <= Candidate.SMaxX
&& WarpedY >= Candidate.SMinY
&& WarpedY <= Candidate.SMaxY;
if (bSameKey && bSameBounds && bContainsQuery)
{
CacheSlot = &Candidate;
break;
}
}
if (CacheSlot == nullptr)
{
CacheSlot = EvictionSlot;
}
CacheSlot->LastUse = ++RoomGraphCacheUseSerial;
FChunkSDFCache& SDFCache = CacheSlot->Cache;
float& CachedSMinX = CacheSlot->SMinX;
float& CachedSMaxX = CacheSlot->SMaxX;
float& CachedSMinY = CacheSlot->SMinY;
float& CachedSMaxY = CacheSlot->SMaxY;
int32& CachedStrate = CacheSlot->Strate;
uint32& CachedSeed = CacheSlot->Seed;
uint32& CachedFingerprint = CacheSlot->Fingerprint;
uint32& CachedLayout = CacheSlot->Layout;
bool& CachedUsesFusedCacheWindow = CacheSlot->bUsesFusedCacheWindow;
bool& CachedUsesTileCacheWindow = CacheSlot->bUsesTileCacheWindow;
FIntVector& CachedTileOrigin = CacheSlot->TileOrigin;
int32& CachedTileStep = CacheSlot->TileStep;
int32& CachedTileCells = CacheSlot->TileCells;
uint64& CachedManagerLifetimeId = CacheSlot->ManagerLifetimeId;
const TArray<FStrateTerrainOpEntry>*& CachedTerrainOps = CacheSlot->TerrainOps;
if (Params.RoomDensity > 0.0f && Params.RoomSpacing > 0.0f)
{
// Get strate index for unique caves per strate.
// MEMOISED: the index only changes at chunk-Z boundaries, yet this ran the strate layout's
// linear scan PER VOXEL. Key = (chunk-Z, layout version) — the version guards against editor
// rebuilds (RebuildStrates / live edit) serving a stale index. The lookup queries the BAND
// CENTRE so the result is a pure function of the key (order-independent, window-invariant).
int32 StrateIdx = 0;
if (StrateManager)
{
thread_local int32 SI_ChunkZ = INT32_MAX;
thread_local uint32 SI_Version = 0xFFFFFFFFu;
thread_local int32 SI_Index = 0;
const int32 QZ = FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE);
const uint32 LV = StrateManager->GetLayoutVersion();
if (QZ != SI_ChunkZ || LV != SI_Version)
{
SI_ChunkZ = QZ;
SI_Version = LV;
// GetStrateIndex expects Unreal world units (divides by VOXEL_SIZE internally);
// WorldZ here is in voxels, so multiply by VOXEL_SIZE.
SI_Index = StrateManager->GetStrateIndex(((float)QZ + 0.5f) * CHUNK_SIZE * VOXEL_SIZE);
}
StrateIdx = SI_Index;
}
// Rebuild only when the WARPED query (what EvaluateSDFCached uses) leaves the
// cached search box, or the strate/seed changed.
const bool bNeedRebuild =
StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
!CacheSlot->bValid || StrateIdx != CachedStrate || (uint32)Seed != CachedSeed ||
ParamsFingerprint != CachedFingerprint || LayoutVersion != CachedLayout ||
CurrentManagerLifetimeId != CachedManagerLifetimeId ||
TerrainOps != CachedTerrainOps ||
@@ -4334,58 +4492,44 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
if (bNeedRebuild)
{
// The lowered evaluator uses the same window-invariance contract as the prepared
// room-graph source. With the switch enabled, the search box is the exact requesting
// tile footprint plus the one-sample normal halo and deterministic warp margin. A
// tile worker can visit neighbouring chunk keys in an arbitrary order without
// rebuilding the graph at every coarse sample. Point queries without a tile context
// retain the historical fused four-chunk / ordinary one-chunk fallback.
const float ChunkMinX = CacheRegionX * (float)CacheRegionSize;
const float ChunkMinY = CacheRegionY * (float)CacheRegionSize;
const float ChunkMaxX = ChunkMinX + (float)CacheRegionSize;
const float ChunkMaxY = ChunkMinY + (float)CacheRegionSize;
const float WarpMargin = FMath::Abs(Params.CaveWarpStrength)
* VOXEL_NOISE_SCALE * 1.5f;
float SMinX = ChunkMinX;
float SMinY = ChunkMinY;
float SMaxX = ChunkMaxX;
float SMaxY = ChunkMaxY;
if (bUseTileCacheWindow)
if (VoxelDensityProfile::AreCountersEnabled())
{
const float TileMinX = static_cast<float>(RequestedTileOrigin.X);
const float TileMinY = static_cast<float>(RequestedTileOrigin.Y);
const float TileExtent = static_cast<float>(
static_cast<int64>(RequestedTileStep)
* static_cast<int64>(RequestedTileCells));
const float TileMaxX = TileMinX + TileExtent;
const float TileMaxY = TileMinY + TileExtent;
const float TileHalo = static_cast<float>(RequestedTileStep) + 2.0f;
const float Expansion = WarpMargin + TileHalo;
SMinX = TileMinX - Expansion;
SMinY = TileMinY - Expansion;
SMaxX = TileMaxX + Expansion;
SMaxY = TileMaxY + Expansion;
const auto CountIf = [](bool bCondition, VoxelDensityProfile::ECounter Counter)
{
if (bCondition) VoxelDensityProfile::AddCounter(Counter);
};
CountIf(StrateIdx != CachedStrate, VoxelDensityProfile::ECounter::SdfRebuildStrate);
CountIf(static_cast<uint32>(Seed) != CachedSeed, VoxelDensityProfile::ECounter::SdfRebuildSeed);
CountIf(ParamsFingerprint != CachedFingerprint, VoxelDensityProfile::ECounter::SdfRebuildFingerprint);
CountIf(LayoutVersion != CachedLayout, VoxelDensityProfile::ECounter::SdfRebuildLayout);
CountIf(CurrentManagerLifetimeId != CachedManagerLifetimeId,
VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime);
CountIf(TerrainOps != CachedTerrainOps, VoxelDensityProfile::ECounter::SdfRebuildTerrainOps);
CountIf(bUseFusedCacheWindow != CachedUsesFusedCacheWindow,
VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy);
CountIf(bUseTileCacheWindow != CachedUsesTileCacheWindow,
VoxelDensityProfile::ECounter::SdfRebuildTilePolicy);
CountIf(bUseTileCacheWindow
&& (RequestedTileOrigin != CachedTileOrigin
|| RequestedTileStep != CachedTileStep
|| RequestedTileCells != CachedTileCells),
VoxelDensityProfile::ECounter::SdfRebuildTileIdentity);
CountIf(WarpedX < CachedSMinX || WarpedX > CachedSMaxX,
VoxelDensityProfile::ECounter::SdfRebuildBoundsX);
CountIf(WarpedY < CachedSMinY || WarpedY > CachedSMaxY,
VoxelDensityProfile::ECounter::SdfRebuildBoundsY);
}
else
{
const float Expansion = WarpMargin + 2.0f;
SMinX = ChunkMinX - Expansion;
SMinY = ChunkMinY - Expansion;
SMaxX = ChunkMaxX + Expansion;
SMaxY = ChunkMaxY + Expansion;
}
VoxelCaveMorphology::BuildChunkCache(
SDFCache,
SMinX, SMinY, SMaxX, SMaxY,
DesiredSMinX, DesiredSMinY, DesiredSMaxX, DesiredSMaxY,
Params, (uint32)Seed, StrateIdx,
TerrainOps,
ERoomGraphBuildSite::GeneratorTile
);
UpdateTunnelCoreTileReach(SDFCache, bUseTileCacheWindow);
CachedSMinX = SMinX; CachedSMaxX = SMaxX;
CachedSMinY = SMinY; CachedSMaxY = SMaxY;
CachedSMinX = DesiredSMinX; CachedSMaxX = DesiredSMaxX;
CachedSMinY = DesiredSMinY; CachedSMaxY = DesiredSMaxY;
CachedStrate = StrateIdx;
CachedSeed = (uint32)Seed;
CachedFingerprint = ParamsFingerprint;
@@ -4398,6 +4542,7 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo
CachedTileCells = bUseTileCacheWindow ? RequestedTileCells : 0;
CachedManagerLifetimeId = CurrentManagerLifetimeId;
CachedTerrainOps = TerrainOps;
CacheSlot->bValid = true;
}
// Evaluate SDF using cached rooms and tunnels (WARPED coordinates).
@@ -103,13 +103,15 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
int32 BandZMinVox, int32 BandZMaxVox,
int64* OutDensitySampleCount,
FVoxelSharedDensityGrid* OutPreEditDensityGrid,
const FVoxelSharedDensityGrid* InSharedDensityGrid)
const FVoxelSharedDensityGrid* InSharedDensityGrid,
std::vector<float>* OutDebugDensitySamples)
{
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_MesherGenerateMesh);
FVoxelMeshData MeshData;
if (OutCaptureGrid) { OutCaptureGrid->Reset(); }
if (OutPreEditDensityGrid) { OutPreEditDensityGrid->Reset(); }
if (OutDensitySampleCount) { *OutDensitySampleCount = 0; }
if (OutDebugDensitySamples) { OutDebugDensitySamples->clear(); }
if (!Generator) return MeshData;
VoxelDensityProfile::FScopedTimer MesherProfileTimer(
VoxelDensityProfile::EBucket::MesherGenerateMesh);
@@ -284,7 +286,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
//
// CellsPerAxis = nombre de cellules par axe ; GridDim = points de grille (+1).
// Le point de grille (gx,gy,gz) correspond au voxel monde (gx,gy,gz)*Step.
// Ordre de remplissage z→y→x : garde le cache SDF (search-box) bien chaud.
// The grid fill order is selected below by tile extent to preserve room-cache locality.
// On échantillonne avec un anneau de marge de 1 point de chaque côté (indices -1..GridDim)
// pour pouvoir calculer les normales par GRADIENT DE GRILLE (T1.b) — différences centrales
// sur la grille au lieu de 6 appels densité frais par vertex. La marge utilise les mêmes
@@ -869,56 +871,114 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
}
#endif
}
else for (int32 gz = GzLo; gz <= GzHi; gz++)
else
{
if (ShouldAbortWork())
auto BuildAxisGroups = [&](int32 AxisOrigin, int32 RegionSize)
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (int32 gy = -1; gy <= GridDim; gy++)
{
if (ShouldAbortWork())
std::vector<std::vector<int32>> Groups;
int32 PreviousRegion = INT32_MIN;
for (int32 Index = -1; Index <= GridDim; ++Index)
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (int32 gx = -1; gx <= GridDim; gx++)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
if (bUseSharedDensityGrid)
const int64 WorldCoordinate = static_cast<int64>(AxisOrigin)
+ static_cast<int64>(Index) * Step;
const int32 Region = FMath::FloorToInt(
static_cast<float>(WorldCoordinate) / static_cast<float>(RegionSize));
if (Groups.empty() || Region != PreviousRegion)
{
const int32 SharedX = SharedDeltaX + gx;
const int32 SharedY = SharedDeltaY + gy;
const int32 SharedZ = SharedDeltaZ + gz;
DensityGrid[LocalIndex] = ReuseGrid->Samples[
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
Groups.emplace_back();
PreviousRegion = Region;
}
else
Groups.back().push_back(Index);
}
return Groups;
};
const int32 RoomCacheRegionSize = CHUNK_SIZE
* (VoxelGenLOD::IsUseFusedEvaluatorEnabled() ? 4 : 1);
const auto XGroups = BuildAxisGroups(OriginVoxels.X,RoomCacheRegionSize);
const auto YGroups = BuildAxisGroups(OriginVoxels.Y,RoomCacheRegionSize);
auto SampleGridPoint = [&](int32 gx, int32 gy, int32 gz)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
if (bUseSharedDensityGrid)
{
const int32 SharedX = SharedDeltaX + gx;
const int32 SharedY = SharedDeltaY + gy;
const int32 SharedZ = SharedDeltaZ + gz;
DensityGrid[LocalIndex] = ReuseGrid->Samples[
(SharedZ * ReuseGrid->Dim + SharedY) * ReuseGrid->Dim + SharedX];
}
else
{
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(gz, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
VoxelGenLOD::TileReachBlockIndex =
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
* ReachBlocksPerAxis + ReachBlockX;
float PreEditDensity = 0.0f;
DensityGrid[LocalIndex] = Generator->GetDensityAt(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
bCapturePreEditGrid ? &PreEditDensity : nullptr);
if (bCapturePreEditGrid)
{
// World voxel = tile origin + grid offset scaled by the cell size (Step).
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(gz, 0, CellsPerAxis - 1)
/ VoxelGenLOD::TileReachBlockCells;
VoxelGenLOD::TileReachBlockIndex =
((ReachBlockZ * ReachBlocksPerAxis) + ReachBlockY)
* ReachBlocksPerAxis + ReachBlockX;
float PreEditDensity = 0.0f;
DensityGrid[LocalIndex] = Generator->GetDensityAt(
OriginVoxels.X + gx * Step,
OriginVoxels.Y + gy * Step,
OriginVoxels.Z + gz * Step,
bCapturePreEditGrid ? &PreEditDensity : nullptr);
if (bCapturePreEditGrid)
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
}
if (OutDensitySampleCount)
{
++(*OutDensitySampleCount);
}
}
};
// Very coarse tiles revisit the same XY cache window across a tall Z run. Keep that
// vertical run contiguous; SampleGridPoint writes by lattice index, so output bits do
// not depend on this traversal. Finer tiles retain plane-first traversal, which has
// lower overhead when a tile spans only one room-graph region.
const int64 TileExtentVoxels = static_cast<int64>(Step) * CellsPerAxis;
const bool bUseVerticalCacheTraversal = TileExtentVoxels > CHUNK_SIZE * 16;
if (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)
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
OutPreEditDensityGrid->Samples[LocalIndex] = PreEditDensity;
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
if (OutDensitySampleCount)
SampleGridPoint(gx,gy,gz);
}
}
}
else
{
for (int32 gz = GzLo; gz <= GzHi; ++gz)
{
if (ShouldAbortWork())
{
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (const std::vector<int32>& YGroup : YGroups)
for (const std::vector<int32>& XGroup : XGroups)
for (const int32 gy : YGroup)
{
if (ShouldAbortWork())
{
++(*OutDensitySampleCount);
if (bUseOperatorBlock) { Generator->EndDensityBlock(); }
return FVoxelMeshData();
}
for (const int32 gx : XGroup)
{
SampleGridPoint(gx,gy,gz);
}
}
}
@@ -943,6 +1003,19 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
{
Generator->EndDensityBlock();
}
if (OutDebugDensitySamples)
{
const size_t SampleCount = static_cast<size_t>(GzHi - GzLo + 1)
* static_cast<size_t>(MDim) * static_cast<size_t>(MDim);
OutDebugDensitySamples->reserve(SampleCount);
for (int32 gz = GzLo; gz <= GzHi; ++gz)
for (int32 gy = -1; gy <= GridDim; ++gy)
for (int32 gx = -1; gx <= GridDim; ++gx)
{
const int32 LocalIndex = ((gz + 1) * MDim + (gy + 1)) * MDim + (gx + 1);
OutDebugDensitySamples->push_back(DensityGrid[LocalIndex]);
}
}
MesherDensityGridTimer.End();
if (bUseProofBlocks)
@@ -172,6 +172,28 @@ namespace VoxelDensityProfile
MesherOutputAllocatedBytes,
OpBlockScratchBytes,
OpBlockCopyBytes,
SdfRebuildStrate,
SdfRebuildSeed,
SdfRebuildFingerprint,
SdfRebuildLayout,
SdfRebuildManagerLifetime,
SdfRebuildTerrainOps,
SdfRebuildFusedPolicy,
SdfRebuildTilePolicy,
SdfRebuildTileIdentity,
SdfRebuildBoundsX,
SdfRebuildBoundsY,
SdfTileWindowAccepted,
SdfTileWindowRejected,
TilePolicyKeyOrigin,
TilePolicyKeyStep,
TilePolicyKeyCells,
TilePolicyKeyFingerprint,
TilePolicyKeyLayout,
TilePolicyKeyManager,
TilePolicyKeyTerrainOps,
TilePolicyKeyFusedPath,
TilePolicyKeyContext,
Count
};
@@ -433,6 +433,8 @@ private:
namespace VoxelGenLOD
{
VOXELFORGE_API void SetUseFusedEvaluatorEnabled(bool bEnabled);
VOXELFORGE_API bool IsUseFusedEvaluatorEnabled();
// -1 uses the measured path/LOD policy; 0/1 force the spatial broad phase off/on for an A/B
// run. The policy affects candidate enumeration only, never cache contents or ordering.
VOXELFORGE_API bool ShouldUseSpatialIndex(bool bFusedPath);
@@ -13,6 +13,7 @@
#include "CoreMinimal.h"
#include <atomic>
#include <vector>
#include "VoxelTypes.h" // Pour FVoxelMeshData, CHUNK_SIZE, VOXEL_SIZE, etc.
#include "VoxelGenerator.h"
#include "VoxelMarchingCubesMesher.generated.h"
@@ -78,7 +79,8 @@ public:
int32 BandZMinVox = INT32_MIN, int32 BandZMaxVox = INT32_MAX,
int64* OutDensitySampleCount = nullptr,
FVoxelSharedDensityGrid* OutPreEditDensityGrid = nullptr,
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr);
const FVoxelSharedDensityGrid* InSharedDensityGrid = nullptr,
std::vector<float>* OutDebugDensitySamples = nullptr);
//=========================================================================
// SERVICES (injectés par AVoxelWorld)