Timeline showed the post-export window was frame-paced: the 4-batch / 2 ms per-frame upload budget held CPU-fallback tiles up to 755 ms after they finished, and 35 GPU batches drained one per frame. Before the first whole view, GPU work now drains as one batch and CPU fallback starts with GPU input preparation (--gpu-far-no-cold-drain = paced control). Luna's race: CPU 1.393 s vs GPU 1.677 s. My interleaved runs (Step15Timeline, RunGpuFarViewer): GPU 2.902 (first launch) / 1.252 / 1.209 s; CPU 2.196 / 2.250 / 2.061 s. The CPU viewer still uses the paced upload budget at cold start (stream 674 -> 2250 ms in my runs vs 741 -> 1422 in Luna's), so the race is not yet fair. Audits: hashes, walk 5,642/5,642, 0 diffs vs CPU/WARP. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
6247 lines
367 KiB
C++
6247 lines
367 KiB
C++
#include "OwnerWorldLoader.h"
|
|
#include "StreamingWorld.h"
|
|
#include "StandaloneCoreScheduler.h"
|
|
#include "../Core/src/StandaloneSnapshotAdapter.h"
|
|
#include "../Research/OwnerShapeWorld/gpu_field_port/GpuFarViewer.h"
|
|
#include "VoxelDensityProfile.h"
|
|
|
|
#ifdef TEXT
|
|
#undef TEXT
|
|
#endif
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
|
|
#include <d3d11.h>
|
|
#include <d3dcompiler.h>
|
|
#include <dxgi.h>
|
|
#include <windowsx.h>
|
|
|
|
#include <atomic>
|
|
#include <bit>
|
|
#include <complex>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <deque>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
#include <unordered_set>
|
|
|
|
#pragma comment(lib,"d3d11.lib")
|
|
#pragma comment(lib,"dxgi.lib")
|
|
#pragma comment(lib,"d3dcompiler.lib")
|
|
#pragma comment(lib,"user32.lib")
|
|
#pragma comment(lib,"gdi32.lib")
|
|
|
|
namespace
|
|
{
|
|
struct V3 { float X=0.0f,Y=0.0f,Z=0.0f; };
|
|
V3 Add(V3 A,V3 B){return {A.X+B.X,A.Y+B.Y,A.Z+B.Z};}
|
|
V3 Sub(V3 A,V3 B){return {A.X-B.X,A.Y-B.Y,A.Z-B.Z};}
|
|
V3 Mul(V3 A,float S){return {A.X*S,A.Y*S,A.Z*S};}
|
|
float Dot(V3 A,V3 B){return A.X*B.X+A.Y*B.Y+A.Z*B.Z;}
|
|
V3 Cross(V3 A,V3 B){return {A.Y*B.Z-A.Z*B.Y,A.Z*B.X-A.X*B.Z,A.X*B.Y-A.Y*B.X};}
|
|
V3 Normalize(V3 A){return Mul(A,1.0f/std::sqrt(std::max(1.0e-20f,Dot(A,A))));}
|
|
|
|
#include "FarSurvivalAudit.h"
|
|
|
|
struct FMeshSheetAudit
|
|
{
|
|
uint64 Components=0;
|
|
uint64 SmallComponents=0;
|
|
uint64 SmallComponentsInView=0;
|
|
uint64 BoundarySmallComponentsInView=0;
|
|
uint64 ThinSheets=0;
|
|
uint64 TrianglesInThinSheets=0;
|
|
double MinimumThicknessVoxels=std::numeric_limits<double>::max();
|
|
};
|
|
|
|
struct FMeshArtifactAudit
|
|
{
|
|
uint64 Tiles=0,SurfaceVertices=0,EdgeVertices=0,MidpointVertices=0;
|
|
uint64 SkirtTriangles=0,HiddenSameLodSkirtTriangles=0,ExposedSkirtTriangles=0,
|
|
ReverseSideSolidSkirtTriangles=0,RepeatedOffsetPatches=0,FieldMismatchPatches=0,
|
|
NormalMismatchPatches=0,GradientFallbackPatches=0,GradientFallbackSamples=0,
|
|
DetailDisplacementSamples=0,HighDetailDisplacementSamples=0,DetailDisplacementPatches=0,
|
|
CurvedEdgeChecks=0,CurvedEdgeRootPatches=0,CurvedEdgeRootBadVertices=0,
|
|
DetailGradientSamples=0,DetailGradientOverLimit=0;
|
|
double MaximumAirSkirtDensity=0.0,MaximumPatchDominance=0.0;
|
|
double MaximumDetailDisplacement=0.0;
|
|
double MaximumCurvedEdgeRootError=0.0;
|
|
double MaximumDetailGradient=0.0;
|
|
uint64 FieldEdgeChecks=0,AnalyticCrossingMisses=0,OldInt8RangeEdges=0,NormalChecks=0;
|
|
double SumAbsSourceResidual=0.0,MaximumAbsSourceResidual=0.0,
|
|
SumAnalyticEdgeShiftVoxels=0.0,MaximumAnalyticEdgeShiftVoxels=0.0,
|
|
SumNormalAgreement=0.0,MinimumNormalAgreement=1.0;
|
|
std::array<double,3> WorstShiftPosition{},WorstResidualPosition{};
|
|
double WorstShiftFraction=0.0,WorstShiftD0=0.0,WorstShiftD1=0.0,
|
|
WorstShiftExactD0=0.0,WorstShiftExactD1=0.0;
|
|
int32 WorstShiftAxis=-1;
|
|
std::array<uint64,16> OffsetHistogram{};
|
|
};
|
|
|
|
uint8 ComputeVisibleSkirtFaceMask(const FVoxelTileKey& Tile,
|
|
const std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash>& Desired)
|
|
{
|
|
const FIntVector Origin=Tile.OriginVoxels();
|
|
const int32 Extent=Tile.ExtentVoxels();
|
|
const auto FloorDiv=[](int64 Value,int64 Divisor)
|
|
{
|
|
int64 Quotient=Value/Divisor;
|
|
if(Value%Divisor<0)--Quotient;
|
|
return static_cast<int32>(Quotient);
|
|
};
|
|
uint8 Mask=0;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
for(int32 Side=0;Side<2;++Side)
|
|
{
|
|
int64 Probe[3]={Origin.X+Extent/2,Origin.Y+Extent/2,Origin.Z+Extent/2};
|
|
Probe[Axis]=Side?static_cast<int64>((&Origin.X)[Axis])+Extent
|
|
:static_cast<int64>((&Origin.X)[Axis])-1;
|
|
int32 NeighborLevel=-1;
|
|
for(int32 Level=0;Level<=8;++Level)
|
|
{
|
|
const int64 NeighborExtent=static_cast<int64>(CHUNK_SIZE)<<Level;
|
|
const FIntVector Coord(FloorDiv(Probe[0],NeighborExtent),
|
|
FloorDiv(Probe[1],NeighborExtent),FloorDiv(Probe[2],NeighborExtent));
|
|
if(Desired.contains(FVoxelTileKey(Coord,Level)))
|
|
{NeighborLevel=Level;break;}
|
|
}
|
|
if(NeighborLevel<0||NeighborLevel!=Tile.Level)
|
|
Mask|=static_cast<uint8>(1u<<(Axis*2+Side));
|
|
}
|
|
return Mask;
|
|
}
|
|
|
|
FMeshArtifactAudit AuditMeshArtifacts(const FStandaloneCpuTile& Tile,
|
|
const VoxelForgeCore::World& Shape,double SkirtDepthMeters,uint8 VisibleSkirtFaceMask,
|
|
bool bPrintSkirts,bool bPrintPatches)
|
|
{
|
|
FMeshArtifactAudit Result;
|
|
if(Tile.Key.Level<0||Tile.Key.Level>1||Tile.Vertices.empty()||Tile.Indices.size()<3)
|
|
return Result;
|
|
++Result.Tiles;
|
|
const size_t VertexCount=Tile.Vertices.size();
|
|
const size_t SurfaceVertices=std::min<size_t>(Tile.SurfaceVertexCount,VertexCount);
|
|
Result.SurfaceVertices=SurfaceVertices;
|
|
const int32 Step=1<<Tile.Key.Level;
|
|
const FIntVector Origin=Tile.Key.OriginVoxels();
|
|
|
|
using FBlockKey=std::array<int32,4>; // interpolated axis, then the 8-voxel block xyz
|
|
struct FFieldBlock
|
|
{uint64 Vertices=0,BadVertices=0;double MaximumShift=0.0,MaximumResidual=0.0;
|
|
std::array<double,3> Position{};};
|
|
struct FNormalBlock
|
|
{uint64 Vertices=0,BadVertices=0;double MinimumAgreement=1.0;
|
|
std::array<double,3> Position{},MeshNormal{};};
|
|
struct FGradientFallbackBlock
|
|
{uint64 Vertices=0,BadVertices=0,BadSamples=0;double MaximumError=0.0;
|
|
std::array<double,3> Position{};};
|
|
struct FCurvedEdgeRootBlock
|
|
{uint64 Vertices=0,BadVertices=0;double MaximumError=0.0;
|
|
std::array<double,3> Position{};};
|
|
std::map<FBlockKey,std::array<uint64,256>> OffsetBlocks;
|
|
std::map<FBlockKey,FFieldBlock> FieldBlocks;
|
|
std::map<FBlockKey,FNormalBlock> NormalBlocks;
|
|
std::map<FBlockKey,FGradientFallbackBlock> GradientFallbackBlocks;
|
|
std::map<FBlockKey,FCurvedEdgeRootBlock> CurvedEdgeRootBlocks;
|
|
struct FDetailDisplacementBlock
|
|
{uint64 Vertices=0,HighVertices=0;double Maximum=0.0,MaximumGradient=0.0;std::array<double,3> Position{};};
|
|
std::map<std::array<int32,3>,FDetailDisplacementBlock> DetailDisplacementBlocks;
|
|
std::map<FBlockKey,std::array<double,3>> FirstPositionByBlock;
|
|
std::map<FBlockKey,std::array<double,3>> FirstNormalByBlock;
|
|
uint64 RayTileResidualSamples=0,RayTileResidualGt01=0,
|
|
RayTileResidualGt025=0,RayTileResidualGt05=0;
|
|
double RayTileResidualSum=0.0,RayTileResidualMaximum=0.0;
|
|
std::array<double,3> RayTileResidualMaximumPosition{};
|
|
uint64 RayTileInteriorSamples=0,RayTileInteriorGt0125=0,
|
|
RayTileInteriorGt025=0,RayTileInteriorGt05=0,RayTileInteriorGt1=0;
|
|
double RayTileInteriorResidualSum=0.0,RayTileInteriorResidualMaximum=0.0;
|
|
std::array<double,3> RayTileInteriorMaximumPosition{};
|
|
for(size_t I=0;I<SurfaceVertices;++I)
|
|
{
|
|
const auto& Position=Tile.Vertices[I].Position;
|
|
const double P[3]={Position[0]*4.0,Position[1]*4.0,Position[2]*4.0};
|
|
double DetailGradientAtVertex=0.0;
|
|
if(Tile.Key.Level==0)
|
|
{
|
|
const VoxelForgeCore::DetailFieldSample DetailSample=Shape.detailSampleAt(
|
|
{P[0],P[1],P[2]});
|
|
if(DetailSample.caveInfluence>=0.60f&&DetailSample.floorInfluence>=0.60f)
|
|
{
|
|
constexpr double GradientProbeOffset=0.5;
|
|
double DetailGradientSquared=0.0;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
double Minus[3]={P[0],P[1],P[2]};
|
|
double Plus[3]={P[0],P[1],P[2]};
|
|
Minus[Axis]-=GradientProbeOffset;
|
|
Plus[Axis]+=GradientProbeOffset;
|
|
const double DMinus=Shape.detailSampleAt(
|
|
{Minus[0],Minus[1],Minus[2]}).displacement;
|
|
const double DPlus=Shape.detailSampleAt(
|
|
{Plus[0],Plus[1],Plus[2]}).displacement;
|
|
const double Gradient=(DPlus-DMinus)/(2.0*GradientProbeOffset);
|
|
DetailGradientSquared+=Gradient*Gradient;
|
|
}
|
|
const double DetailGradient=std::sqrt(DetailGradientSquared);
|
|
DetailGradientAtVertex=DetailGradient;
|
|
++Result.DetailGradientSamples;
|
|
Result.MaximumDetailGradient=std::max(
|
|
Result.MaximumDetailGradient,DetailGradient);
|
|
if(DetailGradient>0.55)++Result.DetailGradientOverLimit;
|
|
const double DetailDisplacement=std::abs(DetailSample.displacement);
|
|
++Result.DetailDisplacementSamples;
|
|
Result.MaximumDetailDisplacement=std::max(
|
|
Result.MaximumDetailDisplacement,DetailDisplacement);
|
|
const std::array<int32,3> DetailKey{
|
|
static_cast<int32>(std::floor((P[0]-Origin.X)/8.0)),
|
|
static_cast<int32>(std::floor((P[1]-Origin.Y)/8.0)),
|
|
static_cast<int32>(std::floor((P[2]-Origin.Z)/8.0))};
|
|
FDetailDisplacementBlock& DetailBlock=DetailDisplacementBlocks[DetailKey];
|
|
++DetailBlock.Vertices;
|
|
DetailBlock.MaximumGradient=std::max(DetailBlock.MaximumGradient,DetailGradientAtVertex);
|
|
if(DetailDisplacement>0.65)
|
|
{
|
|
++DetailBlock.HighVertices;
|
|
if(DetailDisplacement>DetailBlock.Maximum)
|
|
{
|
|
DetailBlock.Maximum=DetailDisplacement;
|
|
DetailBlock.Position={P[0],P[1],P[2]};
|
|
}
|
|
++Result.HighDetailDisplacementSamples;
|
|
}
|
|
}
|
|
}
|
|
double Grid[3];int32 VariableAxis=-1;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
Grid[Axis]=(P[Axis]-(&Origin.X)[Axis])/Step;
|
|
if(std::abs(Grid[Axis]-std::round(Grid[Axis]))>0.003)
|
|
{
|
|
if(VariableAxis>=0){VariableAxis=-2;break;}
|
|
VariableAxis=Axis;
|
|
}
|
|
}
|
|
if(VariableAxis<0)continue;
|
|
double Fraction=Grid[VariableAxis]-std::floor(Grid[VariableAxis]);
|
|
if(Fraction<0.003||Fraction>0.997)continue;
|
|
++Result.EdgeVertices;
|
|
const int32 CoarseBin=std::clamp(static_cast<int32>(Fraction*16.0),0,15);
|
|
const int32 PreciseBin=std::clamp(static_cast<int32>(std::lround(Fraction*256.0)),0,255);
|
|
++Result.OffsetHistogram[CoarseBin];
|
|
if(CoarseBin==7||CoarseBin==8)++Result.MidpointVertices;
|
|
double EdgeA[3],EdgeB[3];
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
const int32 Cell=static_cast<int32>(std::floor(Grid[Axis]));
|
|
EdgeA[Axis]=(&Origin.X)[Axis]+Cell*Step;
|
|
EdgeB[Axis]=EdgeA[Axis];
|
|
}
|
|
EdgeB[VariableAxis]+=Step;
|
|
const double D0=Shape.densityAt({EdgeA[0],EdgeA[1],EdgeA[2]});
|
|
const double D1=Shape.densityAt({EdgeB[0],EdgeB[1],EdgeB[2]});
|
|
const double ExactD0=Shape.unclampedDensityAt(
|
|
{EdgeA[0]+0.013,EdgeA[1]+0.013,EdgeA[2]+0.013});
|
|
const double ExactD1=Shape.unclampedDensityAt(
|
|
{EdgeB[0]+0.013,EdgeB[1]+0.013,EdgeB[2]+0.013});
|
|
const double ExactDenominator=ExactD1-ExactD0;
|
|
++Result.FieldEdgeChecks;
|
|
if(std::abs(D0)>127.0/256.0||std::abs(D1)>127.0/256.0)
|
|
++Result.OldInt8RangeEdges;
|
|
const bool bExactCrossing=(ExactD0<0.0&&ExactD1>0.0)
|
|
||(ExactD0>0.0&&ExactD1<0.0);
|
|
double AnalyticShift=0.0;
|
|
if(bExactCrossing&&std::abs(ExactDenominator)>1.0e-9)
|
|
{
|
|
const double ExactT=std::clamp(-ExactD0/ExactDenominator,0.0,1.0);
|
|
AnalyticShift=std::abs(Fraction-ExactT)*Step;
|
|
Result.SumAnalyticEdgeShiftVoxels+=AnalyticShift;
|
|
if(AnalyticShift>Result.MaximumAnalyticEdgeShiftVoxels)
|
|
{
|
|
Result.MaximumAnalyticEdgeShiftVoxels=AnalyticShift;
|
|
Result.WorstShiftPosition={P[0],P[1],P[2]};
|
|
Result.WorstShiftFraction=Fraction;Result.WorstShiftD0=D0;
|
|
Result.WorstShiftD1=D1;Result.WorstShiftExactD0=ExactD0;
|
|
Result.WorstShiftExactD1=ExactD1;Result.WorstShiftAxis=VariableAxis;
|
|
}
|
|
}
|
|
else ++Result.AnalyticCrossingMisses;
|
|
if(Tile.Key.Level==0&&Step==1&&bExactCrossing
|
|
&&Fraction>=0.375&&Fraction<=0.625)
|
|
{
|
|
const double MidExact=Shape.unclampedDensityAt({
|
|
(EdgeA[0]+EdgeB[0])*0.5+0.013,
|
|
(EdgeA[1]+EdgeB[1])*0.5+0.013,
|
|
(EdgeA[2]+EdgeB[2])*0.5+0.013});
|
|
double CurveRootT=0.5;
|
|
if(std::abs(MidExact)>1.0e-8)
|
|
{
|
|
if((MidExact<0.0)==(ExactD0<0.0))
|
|
{
|
|
const double Denominator=ExactD1-MidExact;
|
|
if(std::abs(Denominator)>1.0e-9)
|
|
CurveRootT=0.5+0.5*std::clamp(-MidExact/Denominator,0.0,1.0);
|
|
}
|
|
else
|
|
{
|
|
const double Denominator=MidExact-ExactD0;
|
|
if(std::abs(Denominator)>1.0e-9)
|
|
CurveRootT=0.5*std::clamp(-ExactD0/Denominator,0.0,1.0);
|
|
}
|
|
}
|
|
const double CurveError=std::abs(Fraction-CurveRootT);
|
|
++Result.CurvedEdgeChecks;
|
|
Result.MaximumCurvedEdgeRootError=std::max(
|
|
Result.MaximumCurvedEdgeRootError,CurveError);
|
|
FBlockKey RootKey{};RootKey[0]=VariableAxis;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
RootKey[Axis+1]=static_cast<int32>(std::floor(
|
|
(P[Axis]-(&Origin.X)[Axis])/8.0));
|
|
FCurvedEdgeRootBlock& RootBlock=CurvedEdgeRootBlocks[RootKey];
|
|
++RootBlock.Vertices;
|
|
if(CurveError>0.08)
|
|
{
|
|
++RootBlock.BadVertices;
|
|
if(CurveError>RootBlock.MaximumError)
|
|
{
|
|
RootBlock.MaximumError=CurveError;
|
|
RootBlock.Position={P[0],P[1],P[2]};
|
|
}
|
|
++Result.CurvedEdgeRootBadVertices;
|
|
}
|
|
}
|
|
const double SourceResidual=std::abs(Shape.unclampedDensityAt({P[0],P[1],P[2]}));
|
|
if(Tile.Key.Level==0&&Tile.Key.Coord.X==1
|
|
&&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3)
|
|
{
|
|
++RayTileResidualSamples;
|
|
RayTileResidualSum+=SourceResidual;
|
|
if(SourceResidual>0.1)++RayTileResidualGt01;
|
|
if(SourceResidual>0.25)++RayTileResidualGt025;
|
|
if(SourceResidual>0.5)++RayTileResidualGt05;
|
|
if(SourceResidual>RayTileResidualMaximum)
|
|
{
|
|
RayTileResidualMaximum=SourceResidual;
|
|
RayTileResidualMaximumPosition={P[0],P[1],P[2]};
|
|
}
|
|
}
|
|
Result.SumAbsSourceResidual+=SourceResidual;
|
|
if(SourceResidual>Result.MaximumAbsSourceResidual)
|
|
{
|
|
Result.MaximumAbsSourceResidual=SourceResidual;
|
|
Result.WorstResidualPosition={P[0],P[1],P[2]};
|
|
}
|
|
const double H=0.5;
|
|
const double GX=Shape.unclampedDensityAt({P[0]+H+0.013,P[1]+0.013,P[2]+0.013})
|
|
-Shape.unclampedDensityAt({P[0]-H+0.013,P[1]+0.013,P[2]+0.013});
|
|
const double GY=Shape.unclampedDensityAt({P[0]+0.013,P[1]+H+0.013,P[2]+0.013})
|
|
-Shape.unclampedDensityAt({P[0]+0.013,P[1]-H+0.013,P[2]+0.013});
|
|
const double GZ=Shape.unclampedDensityAt({P[0]+0.013,P[1]+0.013,P[2]+H+0.013})
|
|
-Shape.unclampedDensityAt({P[0]+0.013,P[1]+0.013,P[2]-H+0.013});
|
|
const double GL=std::sqrt(GX*GX+GY*GY+GZ*GZ);
|
|
double NormalAgreement=1.0;
|
|
if(Tile.Key.Level==0&&GL>1.0e-8)
|
|
{
|
|
const auto& N=Tile.Vertices[I].Normal;
|
|
NormalAgreement=std::clamp((N[0]*GX+N[1]*GY+N[2]*GZ)/GL,-1.0,1.0);
|
|
++Result.NormalChecks;Result.SumNormalAgreement+=NormalAgreement;
|
|
Result.MinimumNormalAgreement=std::min(Result.MinimumNormalAgreement,NormalAgreement);
|
|
}
|
|
FBlockKey Key{};Key[0]=VariableAxis;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
Key[Axis+1]=static_cast<int32>(std::floor((P[Axis]-(&Origin.X)[Axis])/8.0));
|
|
if(Tile.Key.Level==0&&GL>1.0e-8)
|
|
{
|
|
FNormalBlock& NormalBlock=NormalBlocks[Key];
|
|
++NormalBlock.Vertices;
|
|
if(NormalAgreement<NormalBlock.MinimumAgreement)
|
|
{
|
|
NormalBlock.MinimumAgreement=NormalAgreement;
|
|
NormalBlock.Position={P[0],P[1],P[2]};
|
|
NormalBlock.MeshNormal={Tile.Vertices[I].Normal[0],
|
|
Tile.Vertices[I].Normal[1],Tile.Vertices[I].Normal[2]};
|
|
}
|
|
if(NormalAgreement<0.90)++NormalBlock.BadVertices;
|
|
}
|
|
if(Tile.Key.Level==0)
|
|
{
|
|
FFieldBlock& FieldBlock=FieldBlocks[Key];
|
|
++FieldBlock.Vertices;
|
|
FieldBlock.MaximumShift=std::max(FieldBlock.MaximumShift,AnalyticShift);
|
|
FieldBlock.MaximumResidual=std::max(FieldBlock.MaximumResidual,SourceResidual);
|
|
const bool bBadFieldEdge=SourceResidual>0.5;
|
|
if(bBadFieldEdge)
|
|
{
|
|
++FieldBlock.BadVertices;
|
|
if(FieldBlock.BadVertices==1)FieldBlock.Position={P[0],P[1],P[2]};
|
|
}
|
|
|
|
FGradientFallbackBlock& FallbackBlock=GradientFallbackBlocks[Key];
|
|
++FallbackBlock.Vertices;
|
|
uint64 VertexFallbackSamples=0;
|
|
for(int32 Endpoint=0;Endpoint<2;++Endpoint)
|
|
{
|
|
const double* EdgePoint=Endpoint?EdgeB:EdgeA;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
for(int32 Side=-1;Side<=1;Side+=2)
|
|
{
|
|
double Probe[3]={EdgePoint[0],EdgePoint[1],EdgePoint[2]};
|
|
Probe[Axis]+=Side;
|
|
const double Packed=Shape.densityAt({Probe[0],Probe[1],Probe[2]});
|
|
if(std::abs(std::abs(Packed)-4.125)>0.001)continue;
|
|
double ExactProbe[3]={Probe[0]+0.013,Probe[1]+0.013,Probe[2]+0.013};
|
|
const double Exact=Shape.unclampedDensityAt(
|
|
{ExactProbe[0],ExactProbe[1],ExactProbe[2]});
|
|
const double Error=std::abs(Packed-Exact);
|
|
if(Error<=0.25)continue;
|
|
++VertexFallbackSamples;
|
|
FallbackBlock.MaximumError=std::max(FallbackBlock.MaximumError,Error);
|
|
}
|
|
}
|
|
if(VertexFallbackSamples>0&&NormalAgreement<0.98)
|
|
{
|
|
++FallbackBlock.BadVertices;
|
|
FallbackBlock.BadSamples+=VertexFallbackSamples;
|
|
if(FallbackBlock.BadVertices==1)FallbackBlock.Position={P[0],P[1],P[2]};
|
|
}
|
|
}
|
|
FirstPositionByBlock.try_emplace(Key,std::array<double,3>{P[0],P[1],P[2]});
|
|
const auto& Normal=Tile.Vertices[I].Normal;
|
|
FirstNormalByBlock.try_emplace(Key,std::array<double,3>{Normal[0],Normal[1],Normal[2]});
|
|
++OffsetBlocks[Key][PreciseBin];
|
|
}
|
|
|
|
struct FHighDetailBlock
|
|
{std::array<int32,3> Key{};uint64 Vertices=0;double Maximum=0.0,MaximumGradient=0.0;
|
|
std::array<double,3> Position{};};
|
|
std::vector<FHighDetailBlock> HighDetailBlocks;
|
|
for(const auto& [Key,Block]:DetailDisplacementBlocks)
|
|
{
|
|
if(Block.Vertices<12||Block.HighVertices<6
|
|
||static_cast<double>(Block.HighVertices)/Block.Vertices<0.25)continue;
|
|
HighDetailBlocks.push_back({Key,Block.HighVertices,Block.Maximum,Block.MaximumGradient,Block.Position});
|
|
}
|
|
std::vector<uint8> HighDetailVisited(HighDetailBlocks.size(),0);
|
|
for(size_t I=0;I<HighDetailBlocks.size();++I)
|
|
{
|
|
if(HighDetailVisited[I])continue;
|
|
std::vector<size_t> Pending{I};HighDetailVisited[I]=1;
|
|
uint64 BlockCount=0,VertexCountInPatch=0;double Maximum=0.0,MaximumGradient=0.0;
|
|
size_t Representative=I;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FHighDetailBlock& A=HighDetailBlocks[Current];
|
|
++BlockCount;VertexCountInPatch+=A.Vertices;
|
|
MaximumGradient=std::max(MaximumGradient,A.MaximumGradient);
|
|
if(A.Maximum>Maximum){Maximum=A.Maximum;Representative=Current;}
|
|
for(size_t J=0;J<HighDetailBlocks.size();++J)
|
|
{
|
|
if(HighDetailVisited[J])continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-HighDetailBlocks[J].Key[Axis]);
|
|
if(Manhattan==1){HighDetailVisited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
// Large relief is valid when it changes smoothly over multiple cells. Flag
|
|
// only connected high-amplitude patches whose measured slope is alias-prone.
|
|
if(BlockCount<3||VertexCountInPatch<48||MaximumGradient<=0.55)continue;
|
|
++Result.DetailDisplacementPatches;
|
|
if(bPrintPatches&&Result.DetailDisplacementPatches<=8)
|
|
{
|
|
const auto& Sample=HighDetailBlocks[Representative];
|
|
std::printf("[MeshDetailDisplacementPatch] level=0 tile=(%d,%d,%d) connected_8vox_blocks=%llu vertices=%llu max_displacement=%.3f vox max_gradient=%.4f vox_per_vox sample=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(VertexCountInPatch),Maximum,MaximumGradient,
|
|
Sample.Position[0],Sample.Position[1],Sample.Position[2]);
|
|
}
|
|
}
|
|
|
|
struct FBadFieldBlock
|
|
{FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double Share=0.0,MaximumShift=0.0,
|
|
MaximumResidual=0.0;std::array<double,3> Position{};};
|
|
std::vector<FBadFieldBlock> BadFieldBlocks;
|
|
for(const auto& [Key,Block]:FieldBlocks)
|
|
{
|
|
if(Block.Vertices<12||Block.BadVertices<6)continue;
|
|
const double Share=static_cast<double>(Block.BadVertices)/Block.Vertices;
|
|
if(Share<0.25)continue;
|
|
BadFieldBlocks.push_back({Key,Block.Vertices,Block.BadVertices,Share,
|
|
Block.MaximumShift,Block.MaximumResidual,Block.Position});
|
|
}
|
|
std::vector<uint8> BadFieldVisited(BadFieldBlocks.size(),0);
|
|
for(size_t I=0;I<BadFieldBlocks.size();++I)
|
|
{
|
|
if(BadFieldVisited[I])continue;
|
|
std::vector<size_t> Pending{I};BadFieldVisited[I]=1;
|
|
uint64 BlockCount=0,BadVertexCount=0;double MaxShift=0.0,MaxResidual=0.0;
|
|
size_t Representative=I;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FBadFieldBlock& A=BadFieldBlocks[Current];
|
|
++BlockCount;BadVertexCount+=A.BadVertices;
|
|
MaxShift=std::max(MaxShift,A.MaximumShift);
|
|
MaxResidual=std::max(MaxResidual,A.MaximumResidual);
|
|
if(A.MaximumShift>=MaxShift||A.MaximumResidual>=MaxResidual)Representative=Current;
|
|
for(size_t J=0;J<BadFieldBlocks.size();++J)
|
|
{
|
|
if(BadFieldVisited[J]||A.Key[0]!=BadFieldBlocks[J].Key[0])continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=1;Axis<4;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-BadFieldBlocks[J].Key[Axis]);
|
|
if(Manhattan==1){BadFieldVisited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
if(BlockCount<2||BadVertexCount<24)continue;
|
|
++Result.FieldMismatchPatches;
|
|
if(bPrintPatches&&Result.FieldMismatchPatches<=8)
|
|
{
|
|
const FBadFieldBlock& Sample=BadFieldBlocks[Representative];
|
|
std::printf("[MeshFieldMismatchPatch] level=%d tile=(%d,%d,%d) connected_8vox_blocks=%llu bad_edges=%llu max_analytic_shift=%.3f vox max_source_residual=%.3f vox sample=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(BadVertexCount),MaxShift,MaxResidual,
|
|
Sample.Position[0],Sample.Position[1],Sample.Position[2]);
|
|
}
|
|
}
|
|
|
|
struct FBadGradientFallbackBlock
|
|
{FBlockKey Key{};uint64 Vertices=0,BadVertices=0,BadSamples=0;
|
|
double MaximumError=0.0;std::array<double,3> Position{};};
|
|
std::vector<FBadGradientFallbackBlock> BadGradientFallbackBlocks;
|
|
for(const auto& [Key,Block]:GradientFallbackBlocks)
|
|
{
|
|
if(Block.Vertices<12||Block.BadVertices<6)continue;
|
|
if(static_cast<double>(Block.BadVertices)/Block.Vertices<0.25)continue;
|
|
BadGradientFallbackBlocks.push_back({Key,Block.Vertices,Block.BadVertices,
|
|
Block.BadSamples,Block.MaximumError,Block.Position});
|
|
}
|
|
std::vector<uint8> GradientFallbackVisited(BadGradientFallbackBlocks.size(),0);
|
|
for(size_t I=0;I<BadGradientFallbackBlocks.size();++I)
|
|
{
|
|
if(GradientFallbackVisited[I])continue;
|
|
std::vector<size_t> Pending{I};GradientFallbackVisited[I]=1;
|
|
uint64 BlockCount=0,BadVertexCount=0,BadSamples=0;double MaximumError=0.0;
|
|
size_t Representative=I;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FBadGradientFallbackBlock& A=BadGradientFallbackBlocks[Current];
|
|
++BlockCount;BadVertexCount+=A.BadVertices;BadSamples+=A.BadSamples;
|
|
MaximumError=std::max(MaximumError,A.MaximumError);
|
|
if(A.MaximumError>=MaximumError)Representative=Current;
|
|
for(size_t J=0;J<BadGradientFallbackBlocks.size();++J)
|
|
{
|
|
if(GradientFallbackVisited[J]
|
|
||A.Key[0]!=BadGradientFallbackBlocks[J].Key[0])continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=1;Axis<4;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-BadGradientFallbackBlocks[J].Key[Axis]);
|
|
if(Manhattan==1)
|
|
{GradientFallbackVisited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
if(BlockCount<2||BadVertexCount<24)continue;
|
|
++Result.GradientFallbackPatches;Result.GradientFallbackSamples+=BadSamples;
|
|
if(bPrintPatches&&Result.GradientFallbackPatches<=8)
|
|
{
|
|
const auto& Sample=BadGradientFallbackBlocks[Representative];
|
|
std::printf("[MeshGradientFallbackPatch] level=%d tile=(%d,%d,%d) connected_8vox_blocks=%llu vertices=%llu fallback_gradient_samples=%llu max_distance_error=%.3f vox sample=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(BadVertexCount),
|
|
static_cast<unsigned long long>(BadSamples),MaximumError,
|
|
Sample.Position[0],Sample.Position[1],Sample.Position[2]);
|
|
}
|
|
}
|
|
|
|
struct FBadNormalBlock
|
|
{FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double MinimumAgreement=1.0;
|
|
std::array<double,3> Position{},MeshNormal{};};
|
|
std::vector<FBadNormalBlock> BadNormalBlocks;
|
|
for(const auto& [Key,Block]:NormalBlocks)
|
|
{
|
|
if(Block.Vertices<12||Block.BadVertices<6)continue;
|
|
if(static_cast<double>(Block.BadVertices)/Block.Vertices<0.25)continue;
|
|
BadNormalBlocks.push_back({Key,Block.Vertices,Block.BadVertices,
|
|
Block.MinimumAgreement,Block.Position,Block.MeshNormal});
|
|
}
|
|
std::vector<uint8> BadNormalVisited(BadNormalBlocks.size(),0);
|
|
for(size_t I=0;I<BadNormalBlocks.size();++I)
|
|
{
|
|
if(BadNormalVisited[I])continue;
|
|
std::vector<size_t> Pending{I};BadNormalVisited[I]=1;
|
|
uint64 BlockCount=0,BadVertexCount=0;double MinAgreement=1.0;size_t Representative=I;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FBadNormalBlock& A=BadNormalBlocks[Current];
|
|
++BlockCount;BadVertexCount+=A.BadVertices;
|
|
MinAgreement=std::min(MinAgreement,A.MinimumAgreement);
|
|
if(A.MinimumAgreement<BadNormalBlocks[Representative].MinimumAgreement)
|
|
Representative=Current;
|
|
for(size_t J=0;J<BadNormalBlocks.size();++J)
|
|
{
|
|
if(BadNormalVisited[J]||A.Key[0]!=BadNormalBlocks[J].Key[0])continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=1;Axis<4;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-BadNormalBlocks[J].Key[Axis]);
|
|
if(Manhattan==1){BadNormalVisited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
if(BlockCount<2||BadVertexCount<24)continue;
|
|
++Result.NormalMismatchPatches;
|
|
if(bPrintPatches&&Result.NormalMismatchPatches<=8)
|
|
{
|
|
const auto& Sample=BadNormalBlocks[Representative];
|
|
std::printf("[MeshNormalMismatchPatch] level=%d tile=(%d,%d,%d) connected_8vox_blocks=%llu bad_normals=%llu min_field_normal_dot=%.3f sample=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(BadVertexCount),MinAgreement,
|
|
Sample.Position[0],Sample.Position[1],Sample.Position[2]);
|
|
double EdgeA[3],EdgeB[3];
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
EdgeA[Axis]=(&Origin.X)[Axis]
|
|
+std::floor((Sample.Position[Axis]-(&Origin.X)[Axis])/Step)*Step;
|
|
EdgeB[Axis]=EdgeA[Axis];
|
|
}
|
|
EdgeB[Sample.Key[0]]+=Step;
|
|
double GradPacked[2][3]{},GradExact[2][3]{};
|
|
for(int32 Endpoint=0;Endpoint<2;++Endpoint)
|
|
{
|
|
const double* P=Endpoint?EdgeB:EdgeA;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
double Minus[3]={P[0],P[1],P[2]},Plus[3]={P[0],P[1],P[2]};
|
|
Minus[Axis]-=1.0;Plus[Axis]+=1.0;
|
|
GradPacked[Endpoint][Axis]=Shape.densityAt({Plus[0],Plus[1],Plus[2]})
|
|
-Shape.densityAt({Minus[0],Minus[1],Minus[2]});
|
|
for(int32 A=0;A<3;++A){Minus[A]+=0.013;Plus[A]+=0.013;}
|
|
GradExact[Endpoint][Axis]=Shape.unclampedDensityAt(
|
|
{Plus[0],Plus[1],Plus[2]})
|
|
-Shape.unclampedDensityAt({Minus[0],Minus[1],Minus[2]});
|
|
}
|
|
}
|
|
const double T=std::clamp(Sample.Position[Sample.Key[0]]-EdgeA[Sample.Key[0]],0.0,1.0);
|
|
double PackedInterpolated[3]{},ExactInterpolated[3]{};
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
PackedInterpolated[Axis]=GradPacked[0][Axis]+T*(GradPacked[1][Axis]-GradPacked[0][Axis]);
|
|
ExactInterpolated[Axis]=GradExact[0][Axis]+T*(GradExact[1][Axis]-GradExact[0][Axis]);
|
|
}
|
|
const auto NormalDot=[](const double A[3],const double B[3])
|
|
{
|
|
const double AL=std::sqrt(A[0]*A[0]+A[1]*A[1]+A[2]*A[2]);
|
|
const double BL=std::sqrt(B[0]*B[0]+B[1]*B[1]+B[2]*B[2]);
|
|
return AL>1.0e-10&&BL>1.0e-10
|
|
?(A[0]*B[0]+A[1]*B[1]+A[2]*B[2])/(AL*BL):0.0;
|
|
};
|
|
std::printf("[MeshNormalProbe] t=%.3f edge_axis=%d mesh=(%.3f,%.3f,%.3f) packed_grid=(%.3f,%.3f,%.3f) exact_grid=(%.3f,%.3f,%.3f) packed_dot=%.3f exact_dot=%.3f endpoints=(%.0f,%.0f,%.0f)->(%.0f,%.0f,%.0f)\n",
|
|
T,Sample.Key[0],Sample.MeshNormal[0],Sample.MeshNormal[1],Sample.MeshNormal[2],
|
|
PackedInterpolated[0],PackedInterpolated[1],PackedInterpolated[2],
|
|
ExactInterpolated[0],ExactInterpolated[1],ExactInterpolated[2],
|
|
NormalDot(Sample.MeshNormal.data(),PackedInterpolated),
|
|
NormalDot(Sample.MeshNormal.data(),ExactInterpolated),
|
|
EdgeA[0],EdgeA[1],EdgeA[2],EdgeB[0],EdgeB[1],EdgeB[2]);
|
|
for(int32 Endpoint=0;Endpoint<2;++Endpoint)
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double* P=Endpoint?EdgeB:EdgeA;
|
|
for(int32 Side=-1;Side<=1;Side+=2)
|
|
{
|
|
double Q[3]={P[0],P[1],P[2]};Q[Axis]+=Side;
|
|
const double Packed=Shape.densityAt({Q[0],Q[1],Q[2]});
|
|
Q[0]+=0.013;Q[1]+=0.013;Q[2]+=0.013;
|
|
const double Exact=Shape.unclampedDensityAt({Q[0],Q[1],Q[2]});
|
|
if(std::abs(Packed-Exact)>0.02)
|
|
std::printf("[MeshNormalProbeNode] endpoint=%d axis=%d side=%d packed=%.4f exact=%.4f delta=%.4f\n",
|
|
Endpoint,Axis,Side,Packed,Exact,Packed-Exact);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(Tile.Key.Level==0&&Tile.Key.Coord.X==1
|
|
&&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3)
|
|
{
|
|
struct FConcentratedBlock
|
|
{FBlockKey Key{};uint64 Count=0;double TopOne=0.0,TopThree=0.0;
|
|
std::array<int32,3> Bins{};std::array<double,3> Position{};};
|
|
std::vector<FConcentratedBlock> ConcentratedBlocks;
|
|
for(const auto& [Key,Bins]:OffsetBlocks)
|
|
{
|
|
const uint64 Count=std::accumulate(Bins.begin(),Bins.end(),uint64{0});
|
|
if(Count<12)continue;
|
|
std::array<int32,256> Order{};
|
|
std::iota(Order.begin(),Order.end(),0);
|
|
std::partial_sort(Order.begin(),Order.begin()+3,Order.end(),
|
|
[&](int32 A,int32 B){return Bins[A]>Bins[B];});
|
|
const uint64 TopThree=Bins[Order[0]]+Bins[Order[1]]+Bins[Order[2]];
|
|
ConcentratedBlocks.push_back({Key,Count,
|
|
static_cast<double>(Bins[Order[0]])/Count,
|
|
static_cast<double>(TopThree)/Count,
|
|
{Order[0],Order[1],Order[2]},FirstPositionByBlock[Key]});
|
|
}
|
|
std::sort(ConcentratedBlocks.begin(),ConcentratedBlocks.end(),
|
|
[](const auto& A,const auto& B){return A.TopThree>B.TopThree;});
|
|
for(size_t I=0;I<std::min<size_t>(8,ConcentratedBlocks.size());++I)
|
|
{
|
|
const auto& B=ConcentratedBlocks[I];
|
|
std::printf("[MeshOffsetConcentration] tile=(1,-16,-3) axis=%d block=(%d,%d,%d) vertices=%llu top1=%.3f top3=%.3f bins=(%d,%d,%d) sample=(%.3f,%.3f,%.3f)\n",
|
|
B.Key[0],B.Key[1],B.Key[2],B.Key[3],
|
|
static_cast<unsigned long long>(B.Count),B.TopOne,B.TopThree,
|
|
B.Bins[0],B.Bins[1],B.Bins[2],
|
|
B.Position[0],B.Position[1],B.Position[2]);
|
|
}
|
|
}
|
|
|
|
struct FRepeatBlock
|
|
{FBlockKey Key{};int32 Bin=0;uint64 Count=0;double Share=0.0;
|
|
std::array<double,3> Position{},Normal{};};
|
|
std::vector<FRepeatBlock> RepeatBlocks;
|
|
for(const auto& [Key,Bins]:OffsetBlocks)
|
|
{
|
|
const uint64 Count=std::accumulate(Bins.begin(),Bins.end(),uint64{0});
|
|
if(Count<12)continue;
|
|
const auto Best=std::max_element(Bins.begin(),Bins.end());
|
|
const int32 Bin=static_cast<int32>(Best-Bins.begin());
|
|
const double Share=static_cast<double>(*Best)/Count;
|
|
if(Bin<80||Bin>176||Share<0.75)continue;
|
|
RepeatBlocks.push_back({Key,Bin,Count,Share,FirstPositionByBlock[Key],FirstNormalByBlock[Key]});
|
|
}
|
|
std::vector<uint8> Visited(RepeatBlocks.size(),0);
|
|
for(size_t I=0;I<RepeatBlocks.size();++I)
|
|
{
|
|
if(Visited[I])continue;
|
|
std::vector<size_t> Pending{I};Visited[I]=1;
|
|
uint64 BlockCount=0,VertexCountInPatch=0;double Dominance=0.0;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FRepeatBlock& A=RepeatBlocks[Current];
|
|
++BlockCount;VertexCountInPatch+=A.Count;Dominance=std::max(Dominance,A.Share);
|
|
for(size_t J=0;J<RepeatBlocks.size();++J)
|
|
{
|
|
if(Visited[J]||A.Key[0]!=RepeatBlocks[J].Key[0]
|
|
||std::abs(A.Bin-RepeatBlocks[J].Bin)>1)continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=1;Axis<4;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-RepeatBlocks[J].Key[Axis]);
|
|
if(Manhattan==1){Visited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
if(BlockCount<3||VertexCountInPatch<48)continue;
|
|
++Result.RepeatedOffsetPatches;
|
|
Result.MaximumPatchDominance=std::max(Result.MaximumPatchDominance,Dominance);
|
|
if(bPrintPatches&&Result.RepeatedOffsetPatches<=8)
|
|
{
|
|
const auto& P=RepeatBlocks[I].Position;const int32 Axis=RepeatBlocks[I].Key[0];
|
|
double EdgeA[3],EdgeB[3];
|
|
for(int32 A=0;A<3;++A)
|
|
{
|
|
const int32 Cell=static_cast<int32>(std::floor((P[A]-(&Origin.X)[A])/Step));
|
|
EdgeA[A]=(&Origin.X)[A]+Cell*Step;EdgeB[A]=EdgeA[A];
|
|
}
|
|
EdgeB[Axis]+=Step;
|
|
const double D0=Shape.densityAt({EdgeA[0],EdgeA[1],EdgeA[2]});
|
|
const double D1=Shape.densityAt({EdgeB[0],EdgeB[1],EdgeB[2]});
|
|
const double ExactD0=Shape.densityAt({EdgeA[0]+0.013,EdgeA[1]+0.013,EdgeA[2]+0.013});
|
|
const double ExactD1=Shape.densityAt({EdgeB[0]+0.013,EdgeB[1]+0.013,EdgeB[2]+0.013});
|
|
const double InterpolatedT=std::abs(D1-D0)>1.0e-9
|
|
?std::clamp(-D0/(D1-D0),0.0,1.0):0.5;
|
|
const double SourceDensity=Shape.densityAt({P[0],P[1],P[2]});
|
|
const auto& N=RepeatBlocks[I].Normal;
|
|
const double PlusNormal=Shape.densityAt({P[0]+N[0],P[1]+N[1],P[2]+N[2]});
|
|
const double MinusNormal=Shape.densityAt({P[0]-N[0],P[1]-N[1],P[2]-N[2]});
|
|
std::printf("[MeshLatticePatch] level=%d tile=(%d,%d,%d) connected_8vox_blocks=%llu edge_vertices=%llu dominant_share=%.3f midpoint_bin_cluster=1 axis=%d sample=(%.3f,%.3f,%.3f) offset=%.4f normal=(%.3f,%.3f,%.3f) endpoint_density=(%.4f,%.4f) analytic_probe=(%.4f,%.4f) predicted_t=%.4f source_density=%.4f normal_probe=(plus1=%.4f,minus1=%.4f)\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(VertexCountInPatch),Dominance,Axis,
|
|
P[0],P[1],P[2],(P[Axis]-EdgeA[Axis])/Step,N[0],N[1],N[2],D0,D1,
|
|
ExactD0,ExactD1,InterpolatedT,SourceDensity,PlusNormal,MinusNormal);
|
|
}
|
|
}
|
|
|
|
const size_t TriangleCount=Tile.Indices.size()/3;
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
if(A<SurfaceVertices&&B<SurfaceVertices&&C<SurfaceVertices)
|
|
{
|
|
if(Tile.Key.Level==0&&Tile.Key.Coord.X==1
|
|
&&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3)
|
|
{
|
|
const auto& PA=Tile.Vertices[A].Position;
|
|
const auto& PB=Tile.Vertices[B].Position;
|
|
const auto& PC=Tile.Vertices[C].Position;
|
|
const double V[3][3]={{PA[0]*4.0,PA[1]*4.0,PA[2]*4.0},
|
|
{PB[0]*4.0,PB[1]*4.0,PB[2]*4.0},
|
|
{PC[0]*4.0,PC[1]*4.0,PC[2]*4.0}};
|
|
const double Probe[4][3]={
|
|
{(V[0][0]+V[1][0]+V[2][0])/3.0,
|
|
(V[0][1]+V[1][1]+V[2][1])/3.0,
|
|
(V[0][2]+V[1][2]+V[2][2])/3.0},
|
|
{(V[0][0]+V[1][0])/2.0,(V[0][1]+V[1][1])/2.0,(V[0][2]+V[1][2])/2.0},
|
|
{(V[1][0]+V[2][0])/2.0,(V[1][1]+V[2][1])/2.0,(V[1][2]+V[2][2])/2.0},
|
|
{(V[2][0]+V[0][0])/2.0,(V[2][1]+V[0][1])/2.0,(V[2][2]+V[0][2])/2.0}};
|
|
for(const auto& Sample:Probe)
|
|
{
|
|
const double Residual=std::abs(Shape.unclampedDensityAt(
|
|
{Sample[0]+0.013,Sample[1]+0.013,Sample[2]+0.013}));
|
|
++RayTileInteriorSamples;RayTileInteriorResidualSum+=Residual;
|
|
if(Residual>0.125)++RayTileInteriorGt0125;
|
|
if(Residual>0.25)++RayTileInteriorGt025;
|
|
if(Residual>0.5)++RayTileInteriorGt05;
|
|
if(Residual>1.0)++RayTileInteriorGt1;
|
|
if(Residual>RayTileInteriorResidualMaximum)
|
|
{
|
|
RayTileInteriorResidualMaximum=Residual;
|
|
RayTileInteriorMaximumPosition={Sample[0],Sample[1],Sample[2]};
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
const uint8 SkirtFaces=T<Tile.TriangleSkirtFaceMasks.size()
|
|
?Tile.TriangleSkirtFaceMasks[T]:0x3f;
|
|
if((SkirtFaces&VisibleSkirtFaceMask)==0)
|
|
{++Result.HiddenSameLodSkirtTriangles;continue;}
|
|
++Result.SkirtTriangles;
|
|
const auto& PA=Tile.Vertices[A].Position;const auto& PB=Tile.Vertices[B].Position;
|
|
const auto& PC=Tile.Vertices[C].Position;
|
|
const VoxelForgeCore::Vec3d Center{
|
|
(static_cast<double>(PA[0])+PB[0]+PC[0])*4.0/3.0,
|
|
(static_cast<double>(PA[1])+PB[1]+PC[1])*4.0/3.0,
|
|
(static_cast<double>(PA[2])+PB[2]+PC[2])*4.0/3.0};
|
|
const double Density=Shape.densityAt(Center);
|
|
double AlternativeCenter[3]={0.0,0.0,0.0};
|
|
const uint32 TriangleIndices[3]={A,B,C};
|
|
for(uint32 Index:TriangleIndices)
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
double Position=Tile.Vertices[Index].Position[Axis];
|
|
if(Index>=SurfaceVertices)
|
|
Position+=2.0*SkirtDepthMeters*Tile.Vertices[Index].Normal[Axis];
|
|
AlternativeCenter[Axis]+=Position*4.0/3.0;
|
|
}
|
|
const double AlternativeDensity=Shape.densityAt({
|
|
AlternativeCenter[0],AlternativeCenter[1],AlternativeCenter[2]});
|
|
double Normal[3]={0.0,0.0,0.0};
|
|
for(uint32 Index:TriangleIndices)if(Index>=SurfaceVertices)
|
|
for(int32 Axis=0;Axis<3;++Axis)Normal[Axis]+=Tile.Vertices[Index].Normal[Axis];
|
|
const double NormalLength=std::sqrt(Normal[0]*Normal[0]+Normal[1]*Normal[1]+Normal[2]*Normal[2]);
|
|
if(NormalLength>1.0e-8)for(double& Value:Normal)Value/=NormalLength;
|
|
const double Inward1=Shape.densityAt({Center.x-Normal[0],Center.y-Normal[1],Center.z-Normal[2]});
|
|
const double Inward2=Shape.densityAt({Center.x-Normal[0]*2.0,Center.y-Normal[1]*2.0,Center.z-Normal[2]*2.0});
|
|
double MaximumProbeDensity=Density;
|
|
const double ProbeWeights[10][3]={
|
|
{1.0,0.0,0.0},{0.0,1.0,0.0},{0.0,0.0,1.0},
|
|
{1.0/3.0,1.0/3.0,1.0/3.0},
|
|
{0.5,0.5,0.0},{0.0,0.5,0.5},{0.5,0.0,0.5},
|
|
{0.75,0.125,0.125},{0.125,0.75,0.125},{0.125,0.125,0.75}};
|
|
for(const auto& Weights:ProbeWeights)
|
|
{
|
|
const VoxelForgeCore::Vec3d Probe{
|
|
PA[0]*Weights[0]+PB[0]*Weights[1]+PC[0]*Weights[2],
|
|
PA[1]*Weights[0]+PB[1]*Weights[1]+PC[1]*Weights[2],
|
|
PA[2]*Weights[0]+PB[2]*Weights[1]+PC[2]*Weights[2]};
|
|
MaximumProbeDensity=std::max(MaximumProbeDensity,static_cast<double>(
|
|
Shape.densityAt({Probe.x*4.0,Probe.y*4.0,Probe.z*4.0})));
|
|
}
|
|
if(MaximumProbeDensity>0.25)
|
|
{
|
|
++Result.ExposedSkirtTriangles;
|
|
Result.MaximumAirSkirtDensity=std::max(
|
|
Result.MaximumAirSkirtDensity,MaximumProbeDensity);
|
|
if(AlternativeDensity<-0.25)++Result.ReverseSideSolidSkirtTriangles;
|
|
if(bPrintSkirts&&Result.ExposedSkirtTriangles<=8)
|
|
std::printf("[ExposedTileSkirt] level=%d tile=(%d,%d,%d) center=(%.2f,%.2f,%.2f) center_density=%.3f max_probe_density=%.3f reverse_density=%.3f inward=(%.3f,%.3f) normal=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
Center.x,Center.y,Center.z,Density,MaximumProbeDensity,
|
|
AlternativeDensity,Inward1,Inward2,
|
|
Normal[0],Normal[1],Normal[2]);
|
|
}
|
|
}
|
|
if(Tile.Key.Level==0&&Tile.Key.Coord.X==1
|
|
&&Tile.Key.Coord.Y==-16&&Tile.Key.Coord.Z==-3)
|
|
{
|
|
std::printf("[MeshRayTileResidual] tile=(1,-16,-3) vertices=%llu mean=%.4f max=%.4f gt0.1=%llu gt0.25=%llu gt0.5=%llu max_vertex=(%.3f,%.3f,%.3f)\n",
|
|
static_cast<unsigned long long>(RayTileResidualSamples),
|
|
RayTileResidualSamples?RayTileResidualSum/RayTileResidualSamples:0.0,
|
|
RayTileResidualMaximum,
|
|
static_cast<unsigned long long>(RayTileResidualGt01),
|
|
static_cast<unsigned long long>(RayTileResidualGt025),
|
|
static_cast<unsigned long long>(RayTileResidualGt05),
|
|
RayTileResidualMaximumPosition[0],RayTileResidualMaximumPosition[1],
|
|
RayTileResidualMaximumPosition[2]);
|
|
std::printf("[MeshRayTileInteriorResidual] tile=(1,-16,-3) samples=%llu mean=%.4f max=%.4f gt0.125=%llu gt0.25=%llu gt0.5=%llu gt1=%llu max_probe=(%.3f,%.3f,%.3f)\n",
|
|
static_cast<unsigned long long>(RayTileInteriorSamples),
|
|
RayTileInteriorSamples?RayTileInteriorResidualSum/RayTileInteriorSamples:0.0,
|
|
RayTileInteriorResidualMaximum,
|
|
static_cast<unsigned long long>(RayTileInteriorGt0125),
|
|
static_cast<unsigned long long>(RayTileInteriorGt025),
|
|
static_cast<unsigned long long>(RayTileInteriorGt05),
|
|
static_cast<unsigned long long>(RayTileInteriorGt1),
|
|
RayTileInteriorMaximumPosition[0],RayTileInteriorMaximumPosition[1],
|
|
RayTileInteriorMaximumPosition[2]);
|
|
}
|
|
struct FBadCurvedEdgeRootBlock
|
|
{FBlockKey Key{};uint64 Vertices=0,BadVertices=0;double Share=0.0,MaximumError=0.0;
|
|
std::array<double,3> Position{};};
|
|
std::vector<FBadCurvedEdgeRootBlock> BadCurvedEdgeRootBlocks;
|
|
for(const auto& [Key,Block]:CurvedEdgeRootBlocks)
|
|
{
|
|
if(Block.Vertices<12||Block.BadVertices<6)continue;
|
|
const double Share=static_cast<double>(Block.BadVertices)/Block.Vertices;
|
|
if(Share<0.25)continue;
|
|
BadCurvedEdgeRootBlocks.push_back({Key,Block.Vertices,Block.BadVertices,Share,
|
|
Block.MaximumError,Block.Position});
|
|
}
|
|
std::vector<uint8> CurvedEdgeRootVisited(BadCurvedEdgeRootBlocks.size(),0);
|
|
for(size_t I=0;I<BadCurvedEdgeRootBlocks.size();++I)
|
|
{
|
|
if(CurvedEdgeRootVisited[I])continue;
|
|
std::vector<size_t> Pending{I};CurvedEdgeRootVisited[I]=1;
|
|
uint64 BlockCount=0,BadVertexCount=0;double MaximumError=0.0;size_t Representative=I;
|
|
while(!Pending.empty())
|
|
{
|
|
const size_t Current=Pending.back();Pending.pop_back();
|
|
const FBadCurvedEdgeRootBlock& A=BadCurvedEdgeRootBlocks[Current];
|
|
++BlockCount;BadVertexCount+=A.BadVertices;
|
|
if(A.MaximumError>MaximumError)
|
|
{MaximumError=A.MaximumError;Representative=Current;}
|
|
for(size_t J=0;J<BadCurvedEdgeRootBlocks.size();++J)
|
|
{
|
|
if(CurvedEdgeRootVisited[J]
|
|
||A.Key[0]!=BadCurvedEdgeRootBlocks[J].Key[0])continue;
|
|
int32 Manhattan=0;
|
|
for(int32 Axis=1;Axis<4;++Axis)
|
|
Manhattan+=std::abs(A.Key[Axis]-BadCurvedEdgeRootBlocks[J].Key[Axis]);
|
|
if(Manhattan==1)
|
|
{CurvedEdgeRootVisited[J]=1;Pending.push_back(J);}
|
|
}
|
|
}
|
|
if(BlockCount<2||BadVertexCount<24)continue;
|
|
++Result.CurvedEdgeRootPatches;
|
|
if(bPrintPatches&&Result.CurvedEdgeRootPatches<=8)
|
|
{
|
|
const FBadCurvedEdgeRootBlock& Sample=BadCurvedEdgeRootBlocks[Representative];
|
|
std::printf("[MeshCurvedEdgeRootPatch] level=0 tile=(%d,%d,%d) connected_8vox_blocks=%llu bad_vertices=%llu max_root_error=%.4f vox sample=(%.3f,%.3f,%.3f)\n",
|
|
Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(BlockCount),
|
|
static_cast<unsigned long long>(BadVertexCount),MaximumError,
|
|
Sample.Position[0],Sample.Position[1],Sample.Position[2]);
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
FMeshSheetAudit AuditThinMeshSheets(const FStandaloneCpuTile& Tile,
|
|
const FVector& CameraVoxels,float Yaw,float Pitch,bool bPrintCandidates)
|
|
{
|
|
FMeshSheetAudit Result;
|
|
const size_t VertexCount=Tile.Vertices.size();
|
|
if(VertexCount==0||Tile.Indices.size()<3)return Result;
|
|
std::vector<uint32> Parent(VertexCount);
|
|
std::vector<uint8> Rank(VertexCount,0);
|
|
std::iota(Parent.begin(),Parent.end(),0u);
|
|
auto Find=[&](uint32 Value)
|
|
{
|
|
uint32 Root=Value;
|
|
while(Parent[Root]!=Root)Root=Parent[Root];
|
|
while(Parent[Value]!=Value)
|
|
{const uint32 Next=Parent[Value];Parent[Value]=Root;Value=Next;}
|
|
return Root;
|
|
};
|
|
auto Union=[&](uint32 A,uint32 B)
|
|
{
|
|
A=Find(A);B=Find(B);if(A==B)return;
|
|
if(Rank[A]<Rank[B])std::swap(A,B);
|
|
Parent[B]=A;if(Rank[A]==Rank[B])++Rank[A];
|
|
};
|
|
const size_t TriangleCount=Tile.Indices.size()/3;
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
Union(A,B);Union(B,C);
|
|
}
|
|
struct FComponent
|
|
{
|
|
uint64 Triangles=0,Vertices=0;
|
|
double Min[3]={std::numeric_limits<double>::max(),std::numeric_limits<double>::max(),std::numeric_limits<double>::max()};
|
|
double Max[3]={-std::numeric_limits<double>::max(),-std::numeric_limits<double>::max(),-std::numeric_limits<double>::max()};
|
|
double Sum[3]={0.0,0.0,0.0};
|
|
double Cov[6]={0.0,0.0,0.0,0.0,0.0,0.0};
|
|
};
|
|
std::vector<FComponent> Components(VertexCount);
|
|
for(size_t I=0;I<VertexCount;++I)
|
|
{
|
|
const uint32 Root=Find(static_cast<uint32>(I));
|
|
FComponent& C=Components[Root];++C.Vertices;
|
|
for(int32 A=0;A<3;++A)
|
|
{
|
|
const double P=Tile.Vertices[I].Position[A];
|
|
C.Min[A]=std::min(C.Min[A],P);C.Max[A]=std::max(C.Max[A],P);C.Sum[A]+=P;
|
|
}
|
|
}
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
++Components[Find(A)].Triangles;
|
|
}
|
|
for(size_t I=0;I<VertexCount;++I)
|
|
{
|
|
const uint32 Root=Find(static_cast<uint32>(I));
|
|
FComponent& C=Components[Root];
|
|
if(C.Vertices==0)continue;
|
|
const double P[3]={Tile.Vertices[I].Position[0],Tile.Vertices[I].Position[1],Tile.Vertices[I].Position[2]};
|
|
const double D[3]={P[0]-C.Sum[0]/C.Vertices,P[1]-C.Sum[1]/C.Vertices,P[2]-C.Sum[2]/C.Vertices};
|
|
C.Cov[0]+=D[0]*D[0];C.Cov[1]+=D[1]*D[1];C.Cov[2]+=D[2]*D[2];
|
|
C.Cov[3]+=D[0]*D[1];C.Cov[4]+=D[0]*D[2];C.Cov[5]+=D[1]*D[2];
|
|
}
|
|
const double Camera[3]={CameraVoxels.X*0.25,CameraVoxels.Y*0.25,CameraVoxels.Z*0.25};
|
|
const double CP=std::cos(Pitch),Forward[3]={std::sin(Yaw)*CP,std::cos(Yaw)*CP,std::sin(Pitch)};
|
|
const double Right[3]={std::cos(Yaw),-std::sin(Yaw),0.0};
|
|
const double Up[3]={-std::sin(Yaw)*std::sin(Pitch),-std::cos(Yaw)*std::sin(Pitch),std::cos(Pitch)};
|
|
for(size_t Root=0;Root<VertexCount;++Root)
|
|
{
|
|
FComponent& C=Components[Root];if(C.Triangles==0||C.Vertices==0)continue;
|
|
++Result.Components;
|
|
const double Extent[3]={C.Max[0]-C.Min[0],C.Max[1]-C.Min[1],C.Max[2]-C.Min[2]};
|
|
const double MaxExtent=std::max({Extent[0],Extent[1],Extent[2]})/0.25;
|
|
const bool bSmall=C.Triangles<=256&&MaxExtent<=20.0;
|
|
if(bSmall)++Result.SmallComponents;
|
|
if(!bSmall||C.Vertices<4)continue;
|
|
double A[3][3]={{C.Cov[0],C.Cov[3],C.Cov[4]},
|
|
{C.Cov[3],C.Cov[1],C.Cov[5]},{C.Cov[4],C.Cov[5],C.Cov[2]}};
|
|
for(int32 Sweep=0;Sweep<12;++Sweep)
|
|
{
|
|
int32 P=0,Q=1;
|
|
if(std::abs(A[0][2])>std::abs(A[P][Q])){P=0;Q=2;}
|
|
if(std::abs(A[1][2])>std::abs(A[P][Q])){P=1;Q=2;}
|
|
if(std::abs(A[P][Q])<1.0e-16)break;
|
|
const double Tau=(A[Q][Q]-A[P][P])/(2.0*A[P][Q]);
|
|
const double T=std::copysign(1.0,Tau)/(std::abs(Tau)+std::sqrt(1.0+Tau*Tau));
|
|
const double Cos=1.0/std::sqrt(1.0+T*T),Sin=T*Cos;
|
|
const double App=A[P][P],Aqq=A[Q][Q],Apq=A[P][Q];
|
|
A[P][P]=App-T*Apq;A[Q][Q]=Aqq+T*Apq;A[P][Q]=A[Q][P]=0.0;
|
|
for(int32 K=0;K<3;++K)if(K!=P&&K!=Q)
|
|
{
|
|
const double Akp=A[K][P],Akq=A[K][Q];
|
|
A[K][P]=A[P][K]=Cos*Akp-Sin*Akq;
|
|
A[K][Q]=A[Q][K]=Sin*Akp+Cos*Akq;
|
|
}
|
|
}
|
|
const double Variance=std::max(0.0,std::min({A[0][0],A[1][1],A[2][2]})/C.Vertices);
|
|
const double ThicknessVoxels=3.5*std::sqrt(Variance)/0.25;
|
|
const double Center[3]={C.Sum[0]/C.Vertices,C.Sum[1]/C.Vertices,C.Sum[2]/C.Vertices};
|
|
const double Relative[3]={Center[0]-Camera[0],Center[1]-Camera[1],Center[2]-Camera[2]};
|
|
const double Depth=Relative[0]*Forward[0]+Relative[1]*Forward[1]+Relative[2]*Forward[2];
|
|
const double ScreenX=Relative[0]*Right[0]+Relative[1]*Right[1]+Relative[2]*Right[2];
|
|
const double ScreenY=Relative[0]*Up[0]+Relative[1]*Up[1]+Relative[2]*Up[2];
|
|
const bool bInView=Depth>0.0&&Depth<=64.0&&std::abs(ScreenX)<0.924*Depth
|
|
&&std::abs(ScreenY)<0.5774*Depth;
|
|
if(!bInView)continue;
|
|
const FIntVector TileOrigin=Tile.Key.OriginVoxels();
|
|
const int32 TileMin[3]={TileOrigin.X,TileOrigin.Y,TileOrigin.Z};
|
|
const int32 TileMax[3]={TileOrigin.X+Tile.Key.ExtentVoxels(),
|
|
TileOrigin.Y+Tile.Key.ExtentVoxels(),TileOrigin.Z+Tile.Key.ExtentVoxels()};
|
|
bool bTouchesTileBoundary=false;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double MinVoxels=C.Min[Axis]*4.0,MaxVoxels=C.Max[Axis]*4.0;
|
|
bTouchesTileBoundary|=MinVoxels<=TileMin[Axis]+0.5
|
|
||MaxVoxels>=TileMax[Axis]-0.5;
|
|
}
|
|
if(bTouchesTileBoundary)++Result.BoundarySmallComponentsInView;
|
|
else ++Result.SmallComponentsInView;
|
|
if(bPrintCandidates&&!bTouchesTileBoundary&&Result.SmallComponentsInView<=8)
|
|
std::printf("[SmallMeshComponent] level=%d tile=(%d,%d,%d) tris=%llu size=(%.2f,%.2f,%.2f) vox pca_thickness=%.3f vox center=(%.2f,%.2f,%.2f)m\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(C.Triangles),Extent[0]/0.25,Extent[1]/0.25,
|
|
Extent[2]/0.25,ThicknessVoxels,Center[0],Center[1],Center[2]);
|
|
else if(bPrintCandidates&&bTouchesTileBoundary
|
|
&&Result.BoundarySmallComponentsInView<=8)
|
|
std::printf("[BoundaryMeshComponent] level=%d tile=(%d,%d,%d) tris=%llu size=(%.2f,%.2f,%.2f) vox pca_thickness=%.3f vox center=(%.2f,%.2f,%.2f)m\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(C.Triangles),Extent[0]/0.25,Extent[1]/0.25,
|
|
Extent[2]/0.25,ThicknessVoxels,Center[0],Center[1],Center[2]);
|
|
if(ThicknessVoxels>=1.0)continue;
|
|
++Result.ThinSheets;Result.MinimumThicknessVoxels=std::min(Result.MinimumThicknessVoxels,ThicknessVoxels);
|
|
Result.TrianglesInThinSheets+=C.Triangles;
|
|
if(bPrintCandidates&&Result.ThinSheets==1)
|
|
std::printf("[ThinMeshSheetCandidate] level=%d tile=(%d,%d,%d) tris=%llu size=(%.2f,%.2f,%.2f) vox pca_thickness=%.3f vox center=(%.2f,%.2f,%.2f)m\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(C.Triangles),Extent[0]/0.25,Extent[1]/0.25,Extent[2]/0.25,
|
|
ThicknessVoxels,Center[0],Center[1],Center[2]);
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
struct FCoarsePanelAudit
|
|
{
|
|
uint64 Tiles=0,PlanarOpenComponents=0,OffSurfacePanels=0,Triangles=0;
|
|
double MaximumSourceErrorVoxels=0.0;
|
|
};
|
|
|
|
FCoarsePanelAudit AuditCoarseSurfacePanels(const FStandaloneCpuTile& Tile,
|
|
const VoxelForgeCore::World& Shape,const VoxelForgeCore::WorldConfiguration& Config,
|
|
bool bPrintCandidates)
|
|
{
|
|
FCoarsePanelAudit Result;
|
|
const size_t VertexCount=Tile.Vertices.size();
|
|
const size_t TriangleCount=Tile.Indices.size()/3;
|
|
if(Tile.Key.Level<1||VertexCount==0||TriangleCount==0)return Result;
|
|
std::vector<uint32> Parent(VertexCount);
|
|
std::vector<uint8> Rank(VertexCount,0);
|
|
std::iota(Parent.begin(),Parent.end(),0u);
|
|
const auto Find=[&](uint32 Value)
|
|
{
|
|
uint32 Root=Value;
|
|
while(Parent[Root]!=Root)Root=Parent[Root];
|
|
while(Parent[Value]!=Value)
|
|
{const uint32 Next=Parent[Value];Parent[Value]=Root;Value=Next;}
|
|
return Root;
|
|
};
|
|
const auto Union=[&](uint32 A,uint32 B)
|
|
{
|
|
A=Find(A);B=Find(B);if(A==B)return;
|
|
if(Rank[A]<Rank[B])std::swap(A,B);
|
|
Parent[B]=A;if(Rank[A]==Rank[B])++Rank[A];
|
|
};
|
|
const auto EdgeKey=[](uint32 A,uint32 B)
|
|
{if(A>B)std::swap(A,B);return (static_cast<uint64>(A)<<32)|B;};
|
|
std::unordered_map<uint64,uint8> EdgeUse;
|
|
EdgeUse.reserve(TriangleCount*3/2+1);
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
Union(A,B);Union(B,C);
|
|
for(const auto [U,V]:std::array<std::pair<uint32,uint32>,3>{{{A,B},{B,C},{C,A}}})
|
|
{
|
|
uint8& Count=EdgeUse[EdgeKey(U,V)];
|
|
if(Count<255)++Count;
|
|
}
|
|
}
|
|
struct FComponent
|
|
{
|
|
uint64 Triangles=0,InteriorFreeEdges=0,SourceSamples=0,OffSourceSamples=0;
|
|
double Normal[3]={0,0,0},NormalWeight=0,MaximumSourceError=0;
|
|
double WorstPosition[3]={0,0,0};
|
|
bool bWorstTriangleCentroid=false;
|
|
};
|
|
std::vector<FComponent> Components(VertexCount);
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
const uint32 Root=Find(A);FComponent& Stats=Components[Root];++Stats.Triangles;
|
|
const auto& PA=Tile.Vertices[A].Position;const auto& PB=Tile.Vertices[B].Position;
|
|
const auto& PC=Tile.Vertices[C].Position;
|
|
const double U[3]={PB[0]-PA[0],PB[1]-PA[1],PB[2]-PA[2]};
|
|
const double V[3]={PC[0]-PA[0],PC[1]-PA[1],PC[2]-PA[2]};
|
|
double N[3]={U[1]*V[2]-U[2]*V[1],U[2]*V[0]-U[0]*V[2],U[0]*V[1]-U[1]*V[0]};
|
|
const double Area2=std::sqrt(N[0]*N[0]+N[1]*N[1]+N[2]*N[2]);
|
|
if(Area2>1.0e-12)
|
|
{
|
|
for(int32 Axis=0;Axis<3;++Axis)Stats.Normal[Axis]+=N[Axis]/Area2*Area2;
|
|
Stats.NormalWeight+=Area2;
|
|
}
|
|
}
|
|
const FIntVector Origin=Tile.Key.OriginVoxels();
|
|
const double TileMin[3]={static_cast<double>(Origin.X),static_cast<double>(Origin.Y),static_cast<double>(Origin.Z)};
|
|
const double TileMax[3]={TileMin[0]+Tile.Key.ExtentVoxels(),TileMin[1]+Tile.Key.ExtentVoxels(),TileMin[2]+Tile.Key.ExtentVoxels()};
|
|
const auto IsTileBoundaryEdge=[&](uint32 A,uint32 B)
|
|
{
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double PA=Tile.Vertices[A].Position[Axis]*4.0;
|
|
const double PB=Tile.Vertices[B].Position[Axis]*4.0;
|
|
if((std::abs(PA-TileMin[Axis])<0.75&&std::abs(PB-TileMin[Axis])<0.75)
|
|
||(std::abs(PA-TileMax[Axis])<0.75&&std::abs(PB-TileMax[Axis])<0.75))return true;
|
|
}
|
|
return false;
|
|
};
|
|
for(const auto& [Key,Count]:EdgeUse)
|
|
{
|
|
if(Count!=1)continue;
|
|
const uint32 A=static_cast<uint32>(Key>>32),B=static_cast<uint32>(Key);
|
|
if(A>=VertexCount||B>=VertexCount||IsTileBoundaryEdge(A,B))continue;
|
|
++Components[Find(A)].InteriorFreeEdges;
|
|
}
|
|
const int32 Extent=Tile.Key.ExtentVoxels();
|
|
const int32 Cells=Tile.Key.Level<Config.fullResolutionLodCount
|
|
?CHUNK_SIZE:std::max(1,Config.coarseTileCells);
|
|
const double Step=std::max(1,Extent/Cells);
|
|
const double ResidualLimit=std::max(2.0,Step*0.10);
|
|
std::vector<uint8> Candidate(VertexCount,0);
|
|
for(size_t Root=0;Root<VertexCount;++Root)
|
|
{
|
|
FComponent& Stats=Components[Root];
|
|
if(Stats.Triangles==0||Stats.Triangles>256||Stats.InteriorFreeEdges==0||Stats.NormalWeight<=0.0)continue;
|
|
const double NormalLength=std::sqrt(Stats.Normal[0]*Stats.Normal[0]
|
|
+Stats.Normal[1]*Stats.Normal[1]+Stats.Normal[2]*Stats.Normal[2]);
|
|
const double Coherence=NormalLength/Stats.NormalWeight;
|
|
if(Coherence<0.995)continue;
|
|
Candidate[Root]=1;++Result.PlanarOpenComponents;
|
|
}
|
|
if(Result.PlanarOpenComponents==0)return Result;
|
|
for(size_t Vertex=0;Vertex<VertexCount;++Vertex)
|
|
{
|
|
const uint32 Root=Find(static_cast<uint32>(Vertex));
|
|
if(!Candidate[Root])continue;
|
|
const auto& P=Tile.Vertices[Vertex].Position;
|
|
const double Error=std::abs(Shape.densityAt({P[0]*4.0,P[1]*4.0,P[2]*4.0}));
|
|
FComponent& Stats=Components[Root];++Stats.SourceSamples;
|
|
if(Error>Stats.MaximumSourceError)
|
|
{
|
|
Stats.MaximumSourceError=Error;
|
|
std::copy_n(P,3,Stats.WorstPosition);
|
|
Stats.bWorstTriangleCentroid=false;
|
|
}
|
|
if(Error>ResidualLimit)++Stats.OffSourceSamples;
|
|
}
|
|
for(size_t T=0;T<TriangleCount;++T)
|
|
{
|
|
const uint32 A=Tile.Indices[T*3],B=Tile.Indices[T*3+1],C=Tile.Indices[T*3+2];
|
|
if(A>=VertexCount||B>=VertexCount||C>=VertexCount)continue;
|
|
const uint32 Root=Find(A);if(!Candidate[Root])continue;
|
|
const auto& PA=Tile.Vertices[A].Position;const auto& PB=Tile.Vertices[B].Position;
|
|
const auto& PC=Tile.Vertices[C].Position;
|
|
const double Error=std::abs(Shape.densityAt({(PA[0]+PB[0]+PC[0])*4.0/3.0,
|
|
(PA[1]+PB[1]+PC[1])*4.0/3.0,(PA[2]+PB[2]+PC[2])*4.0/3.0}));
|
|
FComponent& Stats=Components[Root];++Stats.SourceSamples;
|
|
if(Error>Stats.MaximumSourceError)
|
|
{
|
|
Stats.MaximumSourceError=Error;
|
|
Stats.WorstPosition[0]=(PA[0]+PB[0]+PC[0])/3.0;
|
|
Stats.WorstPosition[1]=(PA[1]+PB[1]+PC[1])/3.0;
|
|
Stats.WorstPosition[2]=(PA[2]+PB[2]+PC[2])/3.0;
|
|
Stats.bWorstTriangleCentroid=true;
|
|
}
|
|
if(Error>ResidualLimit)++Stats.OffSourceSamples;
|
|
}
|
|
for(size_t Root=0;Root<VertexCount;++Root)
|
|
{
|
|
const FComponent& Stats=Components[Root];if(!Candidate[Root]||Stats.SourceSamples==0)continue;
|
|
const double OffShare=static_cast<double>(Stats.OffSourceSamples)/Stats.SourceSamples;
|
|
Result.MaximumSourceErrorVoxels=std::max(Result.MaximumSourceErrorVoxels,Stats.MaximumSourceError);
|
|
if(OffShare<0.25)continue;
|
|
++Result.OffSurfacePanels;Result.Triangles+=Stats.Triangles;
|
|
if(bPrintCandidates&&Result.OffSurfacePanels<=8)
|
|
std::printf("[CoarsePanelCandidate] lod=%d tile=(%d,%d,%d) triangles=%llu interior_free_edges=%llu source_error=%.3f vox off_share=%.1f%% limit=%.3f vox worst=(%.2f,%.2f,%.2f)m sample=%s\n",
|
|
Tile.Key.Level,Tile.Key.Coord.X,Tile.Key.Coord.Y,Tile.Key.Coord.Z,
|
|
static_cast<unsigned long long>(Stats.Triangles),
|
|
static_cast<unsigned long long>(Stats.InteriorFreeEdges),Stats.MaximumSourceError,
|
|
OffShare*100.0,ResidualLimit,
|
|
Stats.WorstPosition[0],Stats.WorstPosition[1],Stats.WorstPosition[2],
|
|
Stats.bWorstTriangleCentroid?"centroid":"vertex");
|
|
}
|
|
Result.Tiles=1;
|
|
return Result;
|
|
}
|
|
|
|
struct M4 { float M[4][4]{}; };
|
|
M4 Identity(){M4 R{};for(int I=0;I<4;++I)R.M[I][I]=1.0f;return R;}
|
|
M4 Transpose(M4 A){M4 B{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)B.M[I][J]=A.M[J][I];return B;}
|
|
M4 Perspective(float Fov,float Aspect,float NearZ,float FarZ)
|
|
{
|
|
M4 R{};const float Y=1.0f/std::tan(Fov*0.5f);
|
|
R.M[0][0]=Y/Aspect;R.M[1][1]=Y;R.M[2][2]=FarZ/(FarZ-NearZ);
|
|
R.M[2][3]=1.0f;R.M[3][2]=-NearZ*FarZ/(FarZ-NearZ);return R;
|
|
}
|
|
M4 View(V3 Position,V3 Forward,V3 Right,V3 Up)
|
|
{
|
|
M4 R=Identity();
|
|
R.M[0][0]=Right.X;R.M[1][0]=Right.Y;R.M[2][0]=Right.Z;R.M[3][0]=-Dot(Right,Position);
|
|
R.M[0][1]=Up.X;R.M[1][1]=Up.Y;R.M[2][1]=Up.Z;R.M[3][1]=-Dot(Up,Position);
|
|
R.M[0][2]=Forward.X;R.M[1][2]=Forward.Y;R.M[2][2]=Forward.Z;R.M[3][2]=-Dot(Forward,Position);
|
|
return R;
|
|
}
|
|
|
|
struct FGpuVertex { float Position[3]; float Normal[3]; };
|
|
struct FConstants
|
|
{
|
|
M4 ViewProjection;
|
|
float Camera[3]; float FogStart;
|
|
float Forward[3]; float FogEnd;
|
|
float LightDirection[3]; float Ambient;
|
|
float Padding[4];
|
|
};
|
|
|
|
constexpr int32 MaxLevels=8;
|
|
struct FStandaloneBatchKey
|
|
{
|
|
int32 Level=0,X=0,Y=0,Z=0;
|
|
bool operator==(const FStandaloneBatchKey& O)const
|
|
{return Level==O.Level&&X==O.X&&Y==O.Y&&Z==O.Z;}
|
|
};
|
|
struct FStandaloneBatchKeyHash
|
|
{
|
|
size_t operator()(const FStandaloneBatchKey& K)const noexcept
|
|
{
|
|
uint64 H=static_cast<uint32>(K.Level);
|
|
H=(H*0x9e3779b185ebca87ull)^static_cast<uint32>(K.X);
|
|
H=(H*0x9e3779b185ebca87ull)^static_cast<uint32>(K.Y);
|
|
H=(H*0x9e3779b185ebca87ull)^static_cast<uint32>(K.Z);
|
|
return static_cast<size_t>(H^(H>>32));
|
|
}
|
|
};
|
|
struct FResidentTileMesh
|
|
{
|
|
std::vector<FStandaloneCpuVertex> Vertices;
|
|
std::vector<uint32> Indices;
|
|
std::vector<uint8> TriangleSkirtFaceMasks;
|
|
uint8 ActiveSkirtFaceMask=0x3f;
|
|
uint64 Samples=0;
|
|
double WorkerSeconds=0.0;
|
|
std::chrono::steady_clock::time_point CpuQueuedAt{};
|
|
std::chrono::steady_clock::time_point CpuStartedAt{};
|
|
std::chrono::steady_clock::time_point CpuFinishedAt{};
|
|
bool bColdTimelineResidentReported=false;
|
|
};
|
|
struct FStreamingGpuBatch
|
|
{
|
|
ID3D11Buffer* VertexBuffer=nullptr;
|
|
ID3D11Buffer* IndexBuffer=nullptr;
|
|
UINT IndexCount=0;
|
|
UINT DrawOnlyIndexStart=0,DrawOnlyIndexCount=0;
|
|
uint64 VertexCount=0;
|
|
};
|
|
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;
|
|
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;
|
|
};
|
|
struct FGpuFarTile
|
|
{
|
|
ID3D11Buffer* VertexBuffer=nullptr;
|
|
ID3D11Buffer* IndexBuffer=nullptr;
|
|
ID3D11Buffer* IndirectArgs=nullptr;
|
|
ID3D11Buffer* ConnectorVertexBuffer=nullptr;
|
|
ID3D11Buffer* ConnectorIndexBuffer=nullptr;
|
|
ID3D11Query* CompletionEvent=nullptr;
|
|
VoxelForgeGpuFarViewer::GpuTimingQueries TimingQueries{};
|
|
uint64 VertexCapacityBytes=0;
|
|
uint32 OutputSlot=0,IndirectArgsByteOffset=0,BatchIndex=UINT32_MAX;
|
|
uint8 VisibleSkirtFaceMask=0;
|
|
bool bSkirtDrawArgsAudited=false;
|
|
double GpuDispatchMs=0.0;
|
|
std::chrono::steady_clock::time_point PreparedAt{};
|
|
std::chrono::steady_clock::time_point SubmittedAt{};
|
|
std::chrono::steady_clock::time_point CompletedAt{};
|
|
bool bGpuComplete=false;
|
|
bool bTimingResolved=false;
|
|
bool bColdTimelineResidentReported=false;
|
|
};
|
|
struct FGpuFarBatch
|
|
{
|
|
ID3D11Query* CompletionEvent=nullptr;
|
|
VoxelForgeGpuFarViewer::GpuTimingQueries TimingQueries{};
|
|
uint32 TileCount=0;
|
|
uint32 ResidentCount=0;
|
|
std::vector<FVoxelTileKey> Keys;
|
|
uint64 TimestampFrequency=0;
|
|
double GpuDispatchMs=0.0;
|
|
std::chrono::steady_clock::time_point PreparedAt{};
|
|
std::chrono::steady_clock::time_point SubmittedAt{};
|
|
std::chrono::steady_clock::time_point CompletedAt{};
|
|
bool bGpuComplete=false,bTimingResolved=false;
|
|
};
|
|
|
|
uint64 HashStandaloneFile(const std::string& Path)
|
|
{
|
|
std::ifstream File(Path,std::ios::binary);
|
|
if(!File)return 0;
|
|
uint64 Hash=14695981039346656037ull;
|
|
char Buffer[65536];
|
|
while(File)
|
|
{
|
|
File.read(Buffer,sizeof(Buffer));
|
|
const std::streamsize Count=File.gcount();
|
|
for(std::streamsize I=0;I<Count;++I)
|
|
{
|
|
Hash^=static_cast<uint8>(Buffer[I]);
|
|
Hash*=1099511628211ull;
|
|
}
|
|
}
|
|
return Hash;
|
|
}
|
|
|
|
FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile)
|
|
{
|
|
return {Tile.Level,VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord.X,8),
|
|
VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord.Y,8),Tile.Coord.Z};
|
|
}
|
|
|
|
bool PrepareStandaloneDensityWorld(FStandaloneOwnerWorld& World,int32 Seed,
|
|
bool bRoundTunnels,bool bEnhancedWalls,std::string& Error)
|
|
{
|
|
World.Generator.reset();World.Manager.reset();
|
|
World.Settings->Seed=Seed;World.Seed=Seed;
|
|
// Keep the owner's walkable rooms, tunnels, and strate passages intact. The research
|
|
// round field composes as additional air after the owner field, so it can widen/connect
|
|
// spaces but cannot remove the authored entrance-to-exit route.
|
|
World.Manager=std::make_unique<UVoxelStrateManager>();
|
|
if(!World.Manager->Initialize(World.Settings.get(),Seed))
|
|
{Error="standalone copied strate manager failed while applying viewer settings";return false;}
|
|
World.Generator=std::make_unique<UVoxelGenerator>();
|
|
World.Generator->InitializeSettings(World.Settings.get());
|
|
World.Generator->SetStrateManager(World.Manager.get());
|
|
World.Generator->bStandaloneResearchRoundTunnels=bRoundTunnels;
|
|
World.Generator->bStandaloneResearchEnhancedWalls=bEnhancedWalls;
|
|
return true;
|
|
}
|
|
|
|
struct FApp
|
|
{
|
|
HWND Window=nullptr;
|
|
ID3D11Device* Device=nullptr;
|
|
ID3D11DeviceContext* Context=nullptr;
|
|
IDXGISwapChain* SwapChain=nullptr;
|
|
ID3D11RenderTargetView* RenderTarget=nullptr;
|
|
ID3D11DepthStencilView* DepthView=nullptr;
|
|
ID3D11VertexShader* VertexShader=nullptr;
|
|
ID3D11PixelShader* PixelShader=nullptr;
|
|
ID3D11InputLayout* InputLayout=nullptr;
|
|
ID3D11Buffer* Constants=nullptr;
|
|
ID3D11RasterizerState* SolidRasterizer=nullptr;
|
|
ID3D11RasterizerState* WireRasterizer=nullptr;
|
|
std::shared_ptr<FStandaloneOwnerWorld> World;
|
|
VoxelForgeCore::ResolvedWorldSnapshot Snapshot;
|
|
std::unique_ptr<VoxelForgeCore::World> ShapeWorld;
|
|
std::unique_ptr<VoxelForgeCore::World> PendingFullShapeWorld;
|
|
FStandaloneCoreScheduler CoreScheduler;
|
|
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
|
std::thread FullShapeBuildThread;
|
|
std::atomic<bool> bFullShapeBuildFinished{false};
|
|
bool bFullShapeBuildSucceeded=false;
|
|
std::string FullShapeBuildError;
|
|
std::chrono::steady_clock::time_point FullShapeBuildStart{};
|
|
FStandaloneMeshCache MeshCache;
|
|
std::string DataPath;
|
|
VFJson::Value OwnerJsonRoot;
|
|
bool bHasCameraOverride=false;
|
|
V3 CameraPositionOverride{};
|
|
float CameraYawOverride=0.0f,CameraPitchOverride=0.0f;
|
|
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> DeferredLeaverTiles;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> DesiredTileSet;
|
|
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
|
|
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
|
|
std::unordered_map<FVoxelTileKey,FGpuFarTile,FStandaloneTileKeyHash> GpuFarTiles;
|
|
std::vector<FGpuFarBatch> GpuFarBatches;
|
|
std::deque<FVoxelTileKey> GpuFarQueue;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> GpuFarQueued;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> GpuFarEligibleTiles;
|
|
std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet;
|
|
std::unordered_map<uint64,FCrossingWork> CrossingWork;
|
|
std::vector<FVoxelTileKey> DesiredTiles;
|
|
std::vector<FVoxelTileKey> InitialNeighborhoodTiles;
|
|
int32 DesiredByLevel[MaxLevels+1]{};
|
|
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;
|
|
bool bRebuildPending=false;
|
|
bool bParityChecked=false;
|
|
bool bRoundTunnels=false;
|
|
bool bRoundTunnelEnhanced=true;
|
|
bool bShapeWorld=false;
|
|
bool bShapePipelineActive=false;
|
|
bool bNeighborhoodReadyReported=false;
|
|
bool bShapeAuditOnly=false;
|
|
bool bShapeCompare=false;
|
|
bool bFarShapeStagePending=false;
|
|
bool bGpuFarEnabled=false;
|
|
bool bDisableGpuColdStartDrain=false;
|
|
bool bGpuFarAuditEnabled=false;
|
|
bool bGpuFarShaderPrewarm=false,bDisableGpuWarmupPresentHold=false;
|
|
bool bGpuFarWarmupResourcesOnly=false;
|
|
bool bGpuFarWarmupNormalPriority=false,bGpuFarWarmupBeforeWorkers=false;
|
|
bool bGpuFarWarmupKernelFences=false;
|
|
bool bGpuFarWarmupOnly=false;
|
|
std::string GpuWarmupEntryFilter;
|
|
std::thread GpuShaderWarmupThread;
|
|
std::thread GpuFarPreparationThread;
|
|
std::atomic<bool> bGpuFarPreparationFinished{false};
|
|
bool bGpuFarPreparationStarted=false,bGpuFarInputsFinalized=false;
|
|
bool bGpuWarmupCompleted=false,bGpuFarTimelineEnabled=false;
|
|
bool bGpuShaderWarmupQueueSucceeded=false;
|
|
std::string GpuShaderWarmupQueueError;
|
|
bool bGpuFarAuditReported=false;
|
|
bool bGpuSkirtEmitEnabled=true,bGpuConnectorRepairEnabled=true;
|
|
bool bDisableVSync=false;
|
|
bool bForceBlockingStartupPresent=false;
|
|
bool bPresentCpuFrameBeforeGpu=false;
|
|
bool bCpuReadyFramePresentedBeforeGpu=false;
|
|
bool bHoldCpuFrameDuringGpuWarmup=false,bGpuWarmupHoldReported=false;
|
|
bool bGpuWarmupReleaseReported=false;
|
|
uint64 GpuWarmupHeldPresents=0;
|
|
bool bAuditGpuSkirtDrawArgs=false,bGpuSkirtDrawAuditReported=false;
|
|
ID3D11Buffer* GpuSkirtArgsStaging=nullptr;
|
|
uint64 GpuSkirtDrawAuditTiles=0,GpuSkirtDrawAuditZeroCounts=0,
|
|
GpuSkirtDrawAuditOverflow=0,GpuSkirtDrawAuditBadStartIndex=0;
|
|
uint64 GpuSkirtDrawAuditSumIndices=0,GpuSkirtDrawAuditMaxIndices=0;
|
|
std::unique_ptr<FFarSurvivalAudit> GpuFarAudit;
|
|
bool bGpuFarFirstTileReported=false;
|
|
std::filesystem::path GpuFarShaderPath;
|
|
double GpuFarEnqueueWallSeconds=0.0;
|
|
uint64 GpuFarTilesSubmitted=0;
|
|
uint64 PresentCallCount=0;
|
|
uint64 StartupNonBlockingPresentCalls=0,StartupNonBlockingPresentSkips=0;
|
|
double GpuFarGpuDispatchMs=0.0;
|
|
double GpuFarGpuDispatchMaxMs=0.0;
|
|
double GpuFarSubmissionSpanMs=0.0,GpuFarSubmissionSpanMaxMs=0.0;
|
|
uint64 GpuFarLocalMemoryPeakBytes=0,GpuFarLocalMemoryBudgetBytes=0;
|
|
double GpuFarPoolReserveMs=0.0;
|
|
uint64 GpuFarSubmissionBatches=0;
|
|
uint32 GpuFarMaxTilesPerSubmissionBatch=0;
|
|
uint32 GpuFarOutputCapacitySlots=0,GpuFarNextOutputSlot=0;
|
|
std::vector<uint32> GpuFarFreeOutputSlots;
|
|
std::unordered_map<FVoxelTileKey,VoxelForgeCore::FarFieldTileInput,FStandaloneTileKeyHash> GpuFarPreparedInputs;
|
|
std::unordered_map<FVoxelTileKey,std::string,FStandaloneTileKeyHash> GpuFarPreparationErrors;
|
|
std::unordered_map<FVoxelTileKey,std::chrono::steady_clock::time_point,FStandaloneTileKeyHash> GpuFarPreparedAt;
|
|
std::vector<FVoxelTileKey> GpuFarCandidateTiles,GpuFarCpuFallbackTiles,GpuFarNeedPreparation;
|
|
double GpuFarPreparationWallMs=0.0,GpuFarPreparationWorkerMs=0.0;
|
|
uint64 GpuFarPumpCalls=0,GpuFarCompletionPolls=0;
|
|
double GpuFarFirstPumpMs=-1.0,GpuFarLastPumpMs=-1.0;
|
|
double GpuFarFirstSubmitMs=-1.0,GpuFarLastSubmitMs=-1.0;
|
|
double GpuFarFirstCompletionPollMs=-1.0,GpuFarLastCompletionPollMs=-1.0;
|
|
double GpuFarPumpWallMs=0.0,GpuFarPumpMaxWallMs=0.0;
|
|
double GpuFarPumpGapMs=0.0,GpuFarPumpGapMaxMs=0.0;
|
|
std::chrono::steady_clock::time_point LastGpuFarPumpAt{};
|
|
std::chrono::steady_clock::time_point LastDrawEndAt{};
|
|
VoxelForgeCore::DetailVariant ShapeDetailVariant=VoxelForgeCore::DetailVariant::coherent;
|
|
bool bWireframe=false;
|
|
bool bCaptured=false;
|
|
bool bAutoCaptureWritten=false;
|
|
bool bFullDetailReported=false;
|
|
bool bInitialClipmapReported=false;
|
|
bool bWholeViewPresented=false;
|
|
bool bGpuFirstBackedPresentReported=false;
|
|
bool bProfileAttribution=false;
|
|
bool bProfileDetailBand=false;
|
|
bool bCompareEvaluators=false;
|
|
bool bParityOnly=false;
|
|
bool bDensityHash=false;
|
|
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;
|
|
bool bForceGenerate=false;
|
|
std::string CapturePath;
|
|
std::string MeshCachePath;
|
|
std::chrono::steady_clock::time_point ColdProcessStart{};
|
|
std::atomic<uint64> ColdTimelineFrameOrdinal{0};
|
|
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=8;
|
|
int32 ClipRadius=3;
|
|
int32 MaxClipLevelOverride=-1;
|
|
int32 CaptureTileLimit=0;
|
|
int32 CaptureOnlyLod=-1;
|
|
bool bCaptureOnlyTile=false;
|
|
int32 CaptureOnlyTileX=0,CaptureOnlyTileY=0,CaptureOnlyTileZ=0,CaptureOnlyTileLod=0;
|
|
bool bDrawOnlyTile=false;
|
|
int32 DrawOnlyTileX=0,DrawOnlyTileY=0,DrawOnlyTileZ=0,DrawOnlyTileLod=0;
|
|
int32 CaptureOutputSlot=-1;
|
|
int32 Width=1280,Height=800;
|
|
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
|
|
V3 Position{};
|
|
POINT LastMouse{};
|
|
std::chrono::steady_clock::time_point PreviousFrame=std::chrono::steady_clock::now();
|
|
double FrameMs=0.0;
|
|
uint64 TotalVertices=0;
|
|
uint64 TotalIndices=0;
|
|
FIntVector BuildTargetChunk=FIntVector::ZeroValue;
|
|
int32 MaxBatchUploadsPerFrame=4;
|
|
double BatchUploadBudgetMilliseconds=2.0;
|
|
|
|
~FApp(){
|
|
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
|
|
if(GpuShaderWarmupThread.joinable())GpuShaderWarmupThread.join();
|
|
StopGeneration(true);ReleaseMeshes();
|
|
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
|
|
if(Constants)Constants->Release();if(InputLayout)InputLayout->Release();
|
|
if(GpuSkirtArgsStaging)GpuSkirtArgsStaging->Release();
|
|
if(VertexShader)VertexShader->Release();if(PixelShader)PixelShader->Release();
|
|
if(RenderTarget)RenderTarget->Release();if(DepthView)DepthView->Release();
|
|
if(SwapChain)SwapChain->Release();if(Context)Context->Release();if(Device)Device->Release();
|
|
}
|
|
|
|
static void ReleaseGpuFarTile(FGpuFarTile& Tile)
|
|
{
|
|
if(Tile.VertexBuffer)Tile.VertexBuffer->Release();
|
|
if(Tile.IndexBuffer)Tile.IndexBuffer->Release();
|
|
if(Tile.IndirectArgs)Tile.IndirectArgs->Release();
|
|
if(Tile.ConnectorVertexBuffer)Tile.ConnectorVertexBuffer->Release();
|
|
if(Tile.ConnectorIndexBuffer)Tile.ConnectorIndexBuffer->Release();
|
|
if(Tile.CompletionEvent)Tile.CompletionEvent->Release();
|
|
if(Tile.TimingQueries.start)Tile.TimingQueries.start->Release();
|
|
if(Tile.TimingQueries.end)Tile.TimingQueries.end->Release();
|
|
if(Tile.TimingQueries.disjoint)Tile.TimingQueries.disjoint->Release();
|
|
Tile={};
|
|
}
|
|
|
|
double ColdTimelineMilliseconds(std::chrono::steady_clock::time_point At)const
|
|
{
|
|
return At.time_since_epoch().count()==0?-1.0:
|
|
std::chrono::duration<double,std::milli>(At-ColdProcessStart).count();
|
|
}
|
|
|
|
uint64 ColdTimelineFrame()const
|
|
{return ColdTimelineFrameOrdinal.load(std::memory_order_relaxed);}
|
|
|
|
static void ReleaseGpuFarBatch(FGpuFarBatch& Batch)
|
|
{
|
|
if(Batch.CompletionEvent)Batch.CompletionEvent->Release();
|
|
if(Batch.TimingQueries.start)Batch.TimingQueries.start->Release();
|
|
if(Batch.TimingQueries.end)Batch.TimingQueries.end->Release();
|
|
if(Batch.TimingQueries.disjoint)Batch.TimingQueries.disjoint->Release();
|
|
Batch={};
|
|
}
|
|
|
|
void SampleGpuFarMemory()
|
|
{
|
|
if(!bGpuFarEnabled||!Device||!Context)return;
|
|
uint64 Usage=0,Budget=0;std::string Error;
|
|
if(VoxelForgeGpuFarViewer::QueryFarGpuMemory(Device,Context,GpuFarShaderPath,
|
|
Usage,Budget,Error))
|
|
{
|
|
GpuFarLocalMemoryPeakBytes=std::max(GpuFarLocalMemoryPeakBytes,Usage);
|
|
GpuFarLocalMemoryBudgetBytes=Budget;
|
|
}
|
|
}
|
|
|
|
void ReleaseMeshes()
|
|
{
|
|
for(auto& Pair:StreamingBatches)
|
|
{
|
|
if(Pair.second.VertexBuffer)Pair.second.VertexBuffer->Release();
|
|
if(Pair.second.IndexBuffer)Pair.second.IndexBuffer->Release();
|
|
}
|
|
StreamingBatches.clear();BatchMembers.clear();ResidentTiles.clear();
|
|
for(auto& Pair:GpuFarTiles)
|
|
ReleaseGpuFarTile(Pair.second);
|
|
GpuFarTiles.clear();
|
|
for(FGpuFarBatch& Batch:GpuFarBatches)ReleaseGpuFarBatch(Batch);
|
|
GpuFarBatches.clear();GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
|
|
GpuFarFreeOutputSlots.clear();GpuFarNextOutputSlot=0;
|
|
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();GpuFarPreparedAt.clear();
|
|
GpuFarCandidateTiles.clear();GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
|
|
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
bGpuWarmupCompleted=false;
|
|
GpuFarPreparationWallMs=0.0;GpuFarPreparationWorkerMs=0.0;
|
|
GpuFarPumpCalls=0;GpuFarCompletionPolls=0;
|
|
GpuFarFirstPumpMs=GpuFarLastPumpMs=GpuFarFirstSubmitMs=GpuFarLastSubmitMs=-1.0;
|
|
GpuFarFirstCompletionPollMs=GpuFarLastCompletionPollMs=-1.0;
|
|
GpuFarPumpWallMs=GpuFarPumpMaxWallMs=GpuFarPumpGapMs=GpuFarPumpGapMaxMs=0.0;
|
|
LastGpuFarPumpAt={};
|
|
LastDrawEndAt={};
|
|
GpuFarAudit.reset();bGpuFarAuditReported=false;
|
|
GpuFarTilesSubmitted=0;GpuFarEnqueueWallSeconds=0.0;GpuFarGpuDispatchMs=0.0;
|
|
GpuFarGpuDispatchMaxMs=0.0;GpuFarSubmissionSpanMs=0.0;GpuFarSubmissionSpanMaxMs=0.0;
|
|
GpuFarLocalMemoryPeakBytes=0;GpuFarLocalMemoryBudgetBytes=0;
|
|
bGpuFarFirstTileReported=false;
|
|
GpuFarPoolReserveMs=0.0;
|
|
GpuFarSubmissionBatches=0;GpuFarMaxTilesPerSubmissionBatch=0;
|
|
DirtyBatchSet.clear();DesiredTiles.clear();DesiredTileSet.clear();CrossingWork.clear();
|
|
DeferredLeaverTiles.clear();
|
|
for(int32 I=0;I<=MaxLevels;++I)
|
|
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;
|
|
std::fill(std::begin(ProfileCyclesByLevel[I]),std::end(ProfileCyclesByLevel[I]),0);
|
|
TileSecondsByLevel[I].clear();}
|
|
TotalVertices=0;TotalIndices=0;
|
|
}
|
|
|
|
void StopGeneration(bool bCancel=false)
|
|
{
|
|
(void)bCancel;
|
|
if(Streamer)Streamer->Stop();
|
|
Streamer.reset();
|
|
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
|
|
bGpuFarPreparationStarted=false;
|
|
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
|
|
PendingFullShapeWorld.reset();
|
|
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
|
|
bFullShapeBuildSucceeded=false;
|
|
FullShapeBuildError.clear();
|
|
InitialNeighborhoodTiles.clear();
|
|
bShapePipelineActive=false;
|
|
bNeighborhoodReadyReported=false;
|
|
bFarShapeStagePending=false;
|
|
bGenerationRunning=false;
|
|
}
|
|
|
|
V3 Forward() const
|
|
{return Normalize({std::sin(Yaw)*std::cos(Pitch),std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)});}
|
|
|
|
bool IsDrawOnlyTile(const FVoxelTileKey& Key) const
|
|
{
|
|
return !bDrawOnlyTile||(Key.Level==DrawOnlyTileLod&&Key.Coord.X==DrawOnlyTileX
|
|
&&Key.Coord.Y==DrawOnlyTileY&&Key.Coord.Z==DrawOnlyTileZ);
|
|
}
|
|
|
|
FIntVector CameraChunk() const
|
|
{return StandaloneWorldMesher::CameraChunk(FVector(Position.X*4.0,Position.Y*4.0,Position.Z*4.0));}
|
|
|
|
bool NeedsRecenter(const FIntVector& Center) const
|
|
{
|
|
return CameraChunk()!=Center;
|
|
}
|
|
|
|
bool MakeBuffer(ID3D11Buffer** Out,const void* Data,size_t Bytes,UINT Bind)
|
|
{
|
|
if(Bytes==0||Bytes>std::numeric_limits<UINT>::max())return false;
|
|
D3D11_BUFFER_DESC Desc{};Desc.ByteWidth=static_cast<UINT>(Bytes);
|
|
Desc.Usage=D3D11_USAGE_DEFAULT;Desc.BindFlags=Bind;
|
|
D3D11_SUBRESOURCE_DATA Initial{};Initial.pSysMem=Data;
|
|
return SUCCEEDED(Device->CreateBuffer(&Desc,&Initial,Out));
|
|
}
|
|
|
|
static void AppendPngU32(std::vector<uint8>& Out,uint32 Value)
|
|
{
|
|
Out.push_back(static_cast<uint8>(Value >> 24));
|
|
Out.push_back(static_cast<uint8>(Value >> 16));
|
|
Out.push_back(static_cast<uint8>(Value >> 8));
|
|
Out.push_back(static_cast<uint8>(Value));
|
|
}
|
|
|
|
static uint32 PngCrc32(const uint8* Data,size_t Count)
|
|
{
|
|
uint32 Crc=0xffffffffu;
|
|
for(size_t I=0;I<Count;++I)
|
|
{
|
|
Crc^=Data[I];
|
|
for(int32 Bit=0;Bit<8;++Bit)Crc=(Crc>>1)^((Crc&1)?0xedb88320u:0u);
|
|
}
|
|
return Crc^0xffffffffu;
|
|
}
|
|
|
|
static void AppendPngChunk(std::vector<uint8>& Png,const char Type[4],const std::vector<uint8>& Data)
|
|
{
|
|
AppendPngU32(Png,static_cast<uint32>(Data.size()));
|
|
const size_t CrcStart=Png.size();
|
|
Png.insert(Png.end(),Type,Type+4);
|
|
Png.insert(Png.end(),Data.begin(),Data.end());
|
|
AppendPngU32(Png,PngCrc32(Png.data()+CrcStart,Png.size()-CrcStart));
|
|
}
|
|
|
|
bool SaveBackBufferPng(const std::string& Path)
|
|
{
|
|
ID3D11Texture2D* BackBuffer=nullptr;
|
|
if(FAILED(SwapChain->GetBuffer(0,IID_PPV_ARGS(&BackBuffer))))return false;
|
|
D3D11_TEXTURE2D_DESC Desc{};BackBuffer->GetDesc(&Desc);
|
|
Desc.Usage=D3D11_USAGE_STAGING;Desc.BindFlags=0;Desc.CPUAccessFlags=D3D11_CPU_ACCESS_READ;Desc.MiscFlags=0;
|
|
ID3D11Texture2D* Staging=nullptr;
|
|
const HRESULT CreateResult=Device->CreateTexture2D(&Desc,nullptr,&Staging);
|
|
if(FAILED(CreateResult)){BackBuffer->Release();return false;}
|
|
Context->CopyResource(Staging,BackBuffer);BackBuffer->Release();
|
|
D3D11_MAPPED_SUBRESOURCE Mapped{};
|
|
if(FAILED(Context->Map(Staging,0,D3D11_MAP_READ,0,&Mapped))){Staging->Release();return false;}
|
|
|
|
const size_t RowBytes=static_cast<size_t>(Desc.Width)*4;
|
|
std::vector<uint8> Raw((RowBytes+1)*Desc.Height);
|
|
for(UINT Y=0;Y<Desc.Height;++Y)
|
|
{
|
|
uint8* Row=Raw.data()+static_cast<size_t>(Y)*(RowBytes+1);
|
|
Row[0]=0;
|
|
std::memcpy(Row+1,static_cast<const uint8*>(Mapped.pData)+static_cast<size_t>(Y)*Mapped.RowPitch,RowBytes);
|
|
}
|
|
Context->Unmap(Staging,0);Staging->Release();
|
|
|
|
std::vector<uint8> Zlib{0x78,0x01};
|
|
uint32 AdlerA=1,AdlerB=0;
|
|
for(const uint8 Byte:Raw){AdlerA=(AdlerA+Byte)%65521u;AdlerB=(AdlerB+AdlerA)%65521u;}
|
|
for(size_t Offset=0;Offset<Raw.size();)
|
|
{
|
|
const uint16 Block=static_cast<uint16>(std::min<size_t>(65535,Raw.size()-Offset));
|
|
const bool bFinal=Offset+Block==Raw.size();
|
|
Zlib.push_back(bFinal?1:0);
|
|
Zlib.push_back(static_cast<uint8>(Block));Zlib.push_back(static_cast<uint8>(Block>>8));
|
|
const uint16 NotBlock=static_cast<uint16>(~Block);
|
|
Zlib.push_back(static_cast<uint8>(NotBlock));Zlib.push_back(static_cast<uint8>(NotBlock>>8));
|
|
Zlib.insert(Zlib.end(),Raw.begin()+Offset,Raw.begin()+Offset+Block);
|
|
Offset+=Block;
|
|
}
|
|
AppendPngU32(Zlib,(AdlerB<<16)|AdlerA);
|
|
|
|
std::vector<uint8> Png{137,80,78,71,13,10,26,10};
|
|
std::vector<uint8> Ihdr;AppendPngU32(Ihdr,Desc.Width);AppendPngU32(Ihdr,Desc.Height);
|
|
Ihdr.insert(Ihdr.end(),{8,6,0,0,0});AppendPngChunk(Png,"IHDR",Ihdr);
|
|
AppendPngChunk(Png,"IDAT",Zlib);AppendPngChunk(Png,"IEND",{});
|
|
std::ofstream File(Path,std::ios::binary);
|
|
if(!File)return false;
|
|
File.write(reinterpret_cast<const char*>(Png.data()),static_cast<std::streamsize>(Png.size()));
|
|
return File.good();
|
|
}
|
|
|
|
bool Resize(UINT NewWidth,UINT NewHeight)
|
|
{
|
|
if(!Device||NewWidth==0||NewHeight==0)return false;
|
|
Width=static_cast<int32>(NewWidth);Height=static_cast<int32>(NewHeight);
|
|
if(RenderTarget){RenderTarget->Release();RenderTarget=nullptr;}
|
|
if(DepthView){DepthView->Release();DepthView=nullptr;}
|
|
if(SwapChain->ResizeBuffers(0,NewWidth,NewHeight,DXGI_FORMAT_UNKNOWN,0)!=S_OK)return false;
|
|
ID3D11Texture2D* Back=nullptr;
|
|
if(FAILED(SwapChain->GetBuffer(0,IID_PPV_ARGS(&Back))))return false;
|
|
const HRESULT RtvResult=Device->CreateRenderTargetView(Back,nullptr,&RenderTarget);Back->Release();
|
|
if(FAILED(RtvResult))return false;
|
|
D3D11_TEXTURE2D_DESC DepthDesc{};DepthDesc.Width=NewWidth;DepthDesc.Height=NewHeight;
|
|
DepthDesc.MipLevels=1;DepthDesc.ArraySize=1;DepthDesc.Format=DXGI_FORMAT_D24_UNORM_S8_UINT;
|
|
DepthDesc.SampleDesc.Count=1;DepthDesc.Usage=D3D11_USAGE_DEFAULT;DepthDesc.BindFlags=D3D11_BIND_DEPTH_STENCIL;
|
|
ID3D11Texture2D* Depth=nullptr;
|
|
if(FAILED(Device->CreateTexture2D(&DepthDesc,nullptr,&Depth)))return false;
|
|
const HRESULT DsvResult=Device->CreateDepthStencilView(Depth,nullptr,&DepthView);Depth->Release();
|
|
return SUCCEEDED(DsvResult);
|
|
}
|
|
|
|
bool InitializeDevice()
|
|
{
|
|
DXGI_SWAP_CHAIN_DESC Desc{};Desc.BufferCount=1;Desc.BufferDesc.Format=DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
Desc.BufferUsage=DXGI_USAGE_RENDER_TARGET_OUTPUT;Desc.OutputWindow=Window;Desc.SampleDesc.Count=1;
|
|
Desc.Windowed=TRUE;Desc.SwapEffect=DXGI_SWAP_EFFECT_DISCARD;
|
|
D3D_FEATURE_LEVEL Feature{};
|
|
if(FAILED(D3D11CreateDeviceAndSwapChain(nullptr,D3D_DRIVER_TYPE_HARDWARE,nullptr,0,nullptr,0,
|
|
D3D11_SDK_VERSION,&Desc,&SwapChain,&Device,&Feature,&Context)))return false;
|
|
const char* ShaderSource=R"(
|
|
cbuffer C:register(b0){float4x4 VP;float3 Camera;float FogStart;float3 Forward;float FogEnd;float3 LightDir;float Ambient;float4 Pad;}
|
|
struct I{float3 p:POSITION;float3 n:NORMAL;};
|
|
struct O{float4 p:SV_POSITION;float3 world:TEXCOORD0;float3 n:NORMAL;};
|
|
O VS(I i){O o;float3 p=i.p*Pad.x;o.p=mul(float4(p,1),VP);o.world=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);
|
|
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);
|
|
})";
|
|
ID3DBlob* VsCode=nullptr;ID3DBlob* PsCode=nullptr;ID3DBlob* Errors=nullptr;
|
|
HRESULT Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"VS","vs_5_0",0,0,&VsCode,&Errors);
|
|
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Vertex shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();return false;}
|
|
Hr=D3DCompile(ShaderSource,std::strlen(ShaderSource),nullptr,nullptr,nullptr,"PS","ps_5_0",0,0,&PsCode,&Errors);
|
|
if(FAILED(Hr)){if(Errors)std::fprintf(stderr,"Pixel shader compile: %s\n",static_cast<const char*>(Errors->GetBufferPointer()));if(Errors)Errors->Release();VsCode->Release();return false;}
|
|
Hr=Device->CreateVertexShader(VsCode->GetBufferPointer(),VsCode->GetBufferSize(),nullptr,&VertexShader);
|
|
if(SUCCEEDED(Hr))Hr=Device->CreatePixelShader(PsCode->GetBufferPointer(),PsCode->GetBufferSize(),nullptr,&PixelShader);
|
|
D3D11_INPUT_ELEMENT_DESC Elements[]={{"POSITION",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0},
|
|
{"NORMAL",0,DXGI_FORMAT_R32G32B32_FLOAT,0,12,D3D11_INPUT_PER_VERTEX_DATA,0}};
|
|
if(SUCCEEDED(Hr))Hr=Device->CreateInputLayout(Elements,2,VsCode->GetBufferPointer(),VsCode->GetBufferSize(),&InputLayout);
|
|
VsCode->Release();PsCode->Release();if(FAILED(Hr))return false;
|
|
D3D11_BUFFER_DESC ConstantDesc{};ConstantDesc.ByteWidth=sizeof(FConstants);ConstantDesc.Usage=D3D11_USAGE_DEFAULT;
|
|
ConstantDesc.BindFlags=D3D11_BIND_CONSTANT_BUFFER;
|
|
if(FAILED(Device->CreateBuffer(&ConstantDesc,nullptr,&Constants)))return false;
|
|
D3D11_RASTERIZER_DESC Raster{};Raster.FillMode=D3D11_FILL_SOLID;Raster.CullMode=D3D11_CULL_NONE;
|
|
Raster.DepthClipEnable=TRUE;if(FAILED(Device->CreateRasterizerState(&Raster,&SolidRasterizer)))return false;
|
|
Raster.FillMode=D3D11_FILL_WIREFRAME;if(FAILED(Device->CreateRasterizerState(&Raster,&WireRasterizer)))return false;
|
|
if(!Resize(Width,Height))return false;
|
|
if(bGpuFarEnabled)
|
|
{
|
|
std::string GpuInitError;
|
|
const auto GpuPipelineStart=std::chrono::steady_clock::now();
|
|
if(!VoxelForgeGpuFarViewer::InitializeFarPipeline(Device,Context,GpuFarShaderPath,
|
|
GpuInitError))
|
|
{
|
|
std::fprintf(stderr,"[GpuPipelineInitError] %s\n",GpuInitError.c_str());
|
|
return false;
|
|
}
|
|
std::printf("[ColdTimeline] stage=gpu_pipeline_init end_ms=%.3f duration_ms=%.3f prewarm=%s\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-GpuPipelineStart).count(),
|
|
bGpuFarShaderPrewarm?(bGpuFarWarmupResourcesOnly?"async_resources_only":"async_dispatch")
|
|
:"disabled");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void StartGpuShaderWarmupAsync()
|
|
{
|
|
if(!bGpuFarShaderPrewarm||GpuShaderWarmupThread.joinable())return;
|
|
bGpuShaderWarmupQueueSucceeded=false;GpuShaderWarmupQueueError.clear();
|
|
GpuShaderWarmupThread=std::thread([this]()
|
|
{
|
|
bool bNormalBasePriority=false;
|
|
if(bGpuFarWarmupNormalPriority)
|
|
{
|
|
// The process stays BelowNormal; HIGHEST relative priority raises this
|
|
// worker thread to a normal base priority on Windows.
|
|
bNormalBasePriority=SetThreadPriority(GetCurrentThread(),THREAD_PRIORITY_HIGHEST)!=0;
|
|
std::printf("[GpuShaderWarmupThread] priority=%s process=BelowNormal thread_relative=Highest\n",
|
|
bNormalBasePriority?"Normal":"set_failed");
|
|
std::fflush(stdout);
|
|
}
|
|
bGpuShaderWarmupQueueSucceeded=VoxelForgeGpuFarViewer::QueueFarShaderWarmup(
|
|
Device,Context,GpuFarShaderPath,!bGpuFarWarmupResourcesOnly,GpuWarmupEntryFilter,
|
|
bGpuFarWarmupKernelFences,
|
|
GpuShaderWarmupQueueError);
|
|
});
|
|
std::printf("[GpuShaderWarmup] queue_thread=started overlap=%s\n",
|
|
bGpuFarWarmupBeforeWorkers?"none_before_generation_workers":"CPU_shape_compile");
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
size_t PrintParity()
|
|
{
|
|
if(bParityChecked||!World)return 0;
|
|
const auto ParityStart=std::chrono::steady_clock::now();
|
|
bParityChecked=true;size_t MismatchCount=0;
|
|
std::map<std::string,size_t> Kinds;
|
|
for(size_t I=0;I<World->DensitySamples.size();++I)
|
|
{
|
|
const FStandaloneParitySample& S=World->DensitySamples[I];
|
|
const float Actual=World->Generator->GetDensityAt(static_cast<float>(S.PositionVoxels.X),
|
|
static_cast<float>(S.PositionVoxels.Y),static_cast<float>(S.PositionVoxels.Z));
|
|
++Kinds[S.Kind];
|
|
if(std::bit_cast<uint32>(Actual)!=std::bit_cast<uint32>(S.PluginDensity))
|
|
{
|
|
++MismatchCount;
|
|
std::fprintf(stderr,"PARITY MISMATCH %s strate=%d voxels=(%.6f,%.6f,%.6f) plugin=%.9g standalone=%.9g\n",
|
|
S.Kind.c_str(),S.StrateIndex,S.PositionVoxels.X,S.PositionVoxels.Y,S.PositionVoxels.Z,S.PluginDensity,Actual);
|
|
}
|
|
}
|
|
std::printf("Density parity: %zu/%zu bit-identical; mismatches=%zu\n",
|
|
World->DensitySamples.size()-MismatchCount,World->DensitySamples.size(),MismatchCount);
|
|
for(const auto& [Kind,Count]:Kinds)std::printf(" %-24s %zu\n",Kind.c_str(),Count);
|
|
std::printf("[DiagnosticProfile] stage=density_parity samples=%zu duration_ms=%.3f\n",
|
|
World->DensitySamples.size(),std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ParityStart).count());
|
|
std::fflush(stdout);
|
|
return MismatchCount;
|
|
}
|
|
|
|
bool ConfigureWorld(std::string& Error)
|
|
{
|
|
const auto ConfigureStart=std::chrono::steady_clock::now();
|
|
ShapeWorld.reset();
|
|
Snapshot={};
|
|
World=std::make_shared<FStandaloneOwnerWorld>();
|
|
const auto ResolveStart=std::chrono::steady_clock::now();
|
|
if(!StandaloneOwnerLoader::Resolve(OwnerJsonRoot,*World,Error))return false;
|
|
const auto ResolveEnd=std::chrono::steady_clock::now();
|
|
std::printf("[ConfigureProfile] stage=owner_resolve duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(ResolveEnd-ResolveStart).count());
|
|
if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)
|
|
std::printf("[DiagnosticDeferred] density_parity=--parity-only samples=%zu\n",
|
|
World->DensitySamples.size());
|
|
const auto ClipOverrideStart=std::chrono::steady_clock::now();
|
|
const int32 ExportClipRadius=World->ClipRadius;
|
|
World->ClipRadius=ClipRadius;
|
|
World->Settings->ClipRadius=ClipRadius;
|
|
if(!bPrintedClipOverride)
|
|
{
|
|
bPrintedClipOverride=true;
|
|
const double FullDetailWidth=static_cast<double>(2*ClipRadius+2)
|
|
*CHUNK_SIZE*VOXEL_SIZE/100.0;
|
|
std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n",
|
|
ExportClipRadius,ClipRadius,FullDetailWidth);
|
|
}
|
|
std::printf("[ConfigureProfile] stage=clip_override duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ClipOverrideStart).count());
|
|
const auto DensitySetupStart=std::chrono::steady_clock::now();
|
|
if(!PrepareStandaloneDensityWorld(*World,Seed,bRoundTunnels,
|
|
bRoundTunnelEnhanced,Error))return false;
|
|
std::printf("[ConfigureProfile] stage=density_world_setup duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-DensitySetupStart).count());
|
|
if(MaxClipLevelOverride>=0)
|
|
{
|
|
World->MaxClipLevel=MaxClipLevelOverride;
|
|
World->Settings->MaxClipLevel=MaxClipLevelOverride;
|
|
std::printf("Standalone max clip level override applied after world reload: max_level=%d.\n",
|
|
MaxClipLevelOverride);
|
|
}
|
|
// Compile one world field before streaming. The streamer already prioritizes the
|
|
// player's neighborhood, so a second near-only field would only duplicate work.
|
|
const auto ShapeSetupStart=std::chrono::steady_clock::now();
|
|
if(bShapeWorld&&!BuildShapeWorldForCamera(Error,true,false,true))return false;
|
|
std::printf("[ConfigureProfile] stage=shape_world_request duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ShapeSetupStart).count());
|
|
const auto CacheStart=std::chrono::steady_clock::now();
|
|
LoadMeshCache();
|
|
std::printf("[ConfigureProfile] stage=mesh_cache duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-CacheStart).count());
|
|
std::printf("[ConfigureProfile] stage=configure_total duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ConfigureStart).count());
|
|
return true;
|
|
}
|
|
|
|
bool BuildShapeWorldForCamera(std::string& Error,bool bIncludeFarWorld=false,
|
|
bool bNeighborhoodOnly=false,
|
|
bool bDeferWalkabilityValidation=false)
|
|
{
|
|
const auto RequestSetupStart=std::chrono::steady_clock::now();
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
|
FIntVector Min(INT32_MAX,INT32_MAX,INT32_MAX),Max(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
uint64 FineTiles=0;
|
|
InitialNeighborhoodTiles.clear();
|
|
const FIntVector CenterChunk=Desired.CenterChunk;
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
{
|
|
const FIntVector Origin=Tile.OriginVoxels();
|
|
const int32 Extent=Tile.ExtentVoxels();
|
|
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
|
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
|
if(bNeighborhoodOnly)
|
|
{
|
|
if(Tile.Level!=0||std::abs(Tile.Coord.X-CenterChunk.X)>1
|
|
||std::abs(Tile.Coord.Y-CenterChunk.Y)>1
|
|
||std::abs(Tile.Coord.Z-CenterChunk.Z)>1)continue;
|
|
InitialNeighborhoodTiles.push_back(Tile);
|
|
}
|
|
else if(Tile.Level>=World->FullResClipLevels)continue;
|
|
Min.X=std::min(Min.X,Origin.X);Min.Y=std::min(Min.Y,Origin.Y);Min.Z=std::min(Min.Z,Origin.Z);
|
|
Max.X=std::max(Max.X,Origin.X+Extent);Max.Y=std::max(Max.Y,Origin.Y+Extent);Max.Z=std::max(Max.Z,Origin.Z+Extent);
|
|
++FineTiles;
|
|
}
|
|
if(!FineTiles){Error="no full-detail tiles are available for shape compilation";return false;}
|
|
if(bNeighborhoodOnly)
|
|
{
|
|
if(InitialNeighborhoodTiles.size()!=27)
|
|
{Error="the 3x3x3 player neighborhood does not contain 27 LOD0 tiles";return false;}
|
|
constexpr int32 NearBuildPad=64;
|
|
Min.X-=NearBuildPad;Min.Y-=NearBuildPad;Min.Z-=NearBuildPad;
|
|
Max.X+=NearBuildPad;Max.Y+=NearBuildPad;Max.Z+=NearBuildPad;
|
|
}
|
|
const auto DesiredSetupEnd=std::chrono::steady_clock::now();
|
|
std::printf("[ConfigureProfile] stage=desired_tiles_and_bounds tiles=%llu duration_ms=%.3f\n",
|
|
static_cast<unsigned long long>(FineTiles),std::chrono::duration<double,std::milli>(
|
|
DesiredSetupEnd-RequestSetupStart).count());
|
|
const auto SnapshotStart=std::chrono::steady_clock::now();
|
|
Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(OwnerJsonRoot,Seed,ClipRadius,
|
|
MaxClipLevelOverride,bRoundTunnels,bRoundTunnelEnhanced,Error);
|
|
if(!Snapshot)
|
|
{if(Error.empty())Error="the standalone adapter could not make a resolved core snapshot";return false;}
|
|
ShapeWorld=std::make_unique<VoxelForgeCore::World>(Snapshot);
|
|
const int32 ShapeWorkers=bNeighborhoodOnly?(WorkerCount>=8?2:1):WorkerCount;
|
|
VoxelForgeCore::ShapeBuildRequest Request;
|
|
Request.minVoxel={Min.X,Min.Y,Min.Z};Request.maxVoxel={Max.X,Max.Y,Max.Z};
|
|
Request.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
|
Request.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
|
Request.workerLimit=static_cast<uint32>(ShapeWorkers);Request.detail=ShapeDetailVariant;
|
|
Request.includeFarWorld=bIncludeFarWorld;Request.roundTunnels=bRoundTunnels;
|
|
Request.enhancedTunnelWalls=bRoundTunnelEnhanced;
|
|
Request.profileDetailBand=bProfileDetailBand;
|
|
Request.validatePassageRoutes=!bDeferWalkabilityValidation;
|
|
if(bDeferWalkabilityValidation)
|
|
std::printf("[DiagnosticDeferred] walkability=full_headless_or_route_audit workers=8 samples=5642\n");
|
|
const auto ShapeCompileStart=std::chrono::steady_clock::now();
|
|
std::printf("[ConfigureProfile] stage=snapshot_and_build_request duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(ShapeCompileStart-SnapshotStart).count());
|
|
std::printf("[ColdTimeline] stage=shape_compile begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(ShapeCompileStart-ColdProcessStart).count());
|
|
if(!ShapeWorld->compile(Request,CoreScheduler,Error))
|
|
{ShapeWorld.reset();return false;}
|
|
const auto ShapeCompileEnd=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=shape_compile end_ms=%.3f duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(ShapeCompileEnd-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(ShapeCompileEnd-ShapeCompileStart).count());
|
|
bShapePipelineActive=bNeighborhoodOnly;
|
|
bNeighborhoodReadyReported=false;
|
|
bFarShapeStagePending=bShapeWorld&&!bNeighborhoodOnly&&!ShapeWorld->hasFarWorld();
|
|
std::printf("[ShapeRegion] fine_tiles=%llu bounds=(%d,%d,%d)..(%d,%d,%d) clip=(%d,%d,%d)..(%d,%d,%d)\n",
|
|
static_cast<unsigned long long>(FineTiles),Min.X,Min.Y,Min.Z,Max.X,Max.Y,Max.Z,
|
|
ClipMin.X,ClipMin.Y,ClipMin.Z,ClipMax.X,ClipMax.Y,ClipMax.Z);
|
|
return true;
|
|
}
|
|
|
|
bool StartFullShapeBuild(const std::vector<FVoxelTileKey>& FullDesired,std::string& Error)
|
|
{
|
|
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
for(const FVoxelTileKey& Tile:FullDesired)
|
|
{
|
|
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
|
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
|
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
|
if(Tile.Level>=World->FullResClipLevels)continue;
|
|
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
|
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
|
}
|
|
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
|
{Error="the full shape build has no desired detail or clipmap tiles";return false;}
|
|
PendingFullShapeWorld=std::make_unique<VoxelForgeCore::World>(Snapshot);
|
|
FullShapeBuildError.clear();bFullShapeBuildSucceeded=false;
|
|
bFullShapeBuildFinished.store(false,std::memory_order_relaxed);
|
|
const int32 FullWorkers=WorkerCount>=8?6:WorkerCount;
|
|
FullShapeBuildStart=std::chrono::steady_clock::now();
|
|
FullShapeBuildThread=std::thread([this,FineMin,FineMax,ClipMin,ClipMax,FullWorkers]()
|
|
{
|
|
VoxelForgeCore::ShapeBuildRequest Request;
|
|
Request.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
|
Request.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
|
|
Request.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
|
Request.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
|
Request.workerLimit=static_cast<uint32>(FullWorkers);Request.detail=ShapeDetailVariant;
|
|
Request.includeFarWorld=true;Request.roundTunnels=bRoundTunnels;
|
|
Request.enhancedTunnelWalls=bRoundTunnelEnhanced;
|
|
Request.profileDetailBand=bProfileDetailBand;
|
|
Request.validatePassageRoutes=false;
|
|
bFullShapeBuildSucceeded=PendingFullShapeWorld->compile(
|
|
Request,CoreScheduler,FullShapeBuildError);
|
|
bFullShapeBuildFinished.store(true,std::memory_order_release);
|
|
});
|
|
std::printf("[ShapePipelineBuildStart] neighborhood_tiles=%zu near_mesh_workers=%d full_shape_workers=%d\n",
|
|
InitialNeighborhoodTiles.size(),WorkerCount>=8?2:1,FullWorkers);
|
|
std::fflush(stdout);
|
|
return true;
|
|
}
|
|
|
|
FStandaloneMeshCacheMetadata MakeMeshCacheMetadata()const
|
|
{
|
|
FStandaloneMeshCacheMetadata Metadata;
|
|
Metadata.SourceDataHash=HashStandaloneFile(DataPath);
|
|
Metadata.Seed=Seed;Metadata.ClipRadius=ClipRadius;
|
|
Metadata.FullResClipLevels=World->FullResClipLevels;
|
|
Metadata.CoarseTileCells=World->CoarseTileCells;
|
|
Metadata.StrateContentCutMinLevel=World->StrateContentCutMinLevel;
|
|
Metadata.SkirtCells=World->SkirtCells;
|
|
Metadata.LODOctaveDrop=World->LODOctaveDrop;
|
|
Metadata.bGenerateSkirts=World->bGenerateSkirts?1:0;
|
|
Metadata.AlgorithmVersion=FStandaloneMeshCache::CurrentVersion;
|
|
Metadata.TunnelMode=bRoundTunnels?(bRoundTunnelEnhanced?2u:1u):0u;
|
|
return Metadata;
|
|
}
|
|
|
|
void LoadMeshCache()
|
|
{
|
|
MeshCache.Clear();
|
|
if(bForceGenerate||bShapeWorld||!World)return;
|
|
const char* ModeSuffix=!bRoundTunnels?"_base":
|
|
(bRoundTunnelEnhanced?"_round_detailed":"_round_legacy");
|
|
const std::filesystem::path CachePath=std::filesystem::path(DataPath).parent_path()
|
|
/(std::string("VoxelForge_seed")+std::to_string(Seed)+"_r"
|
|
+std::to_string(ClipRadius)+ModeSuffix+".vfmesh");
|
|
MeshCachePath=CachePath.string();
|
|
const auto Begin=std::chrono::steady_clock::now();
|
|
std::string CacheError;
|
|
if(MeshCache.Load(MeshCachePath,MakeMeshCacheMetadata(),CacheError))
|
|
{
|
|
const double Seconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-Begin).count();
|
|
std::printf("[MeshCache] loaded %zu tiles, %.2f MiB in %.3f s from %s\n",
|
|
MeshCache.Num(),static_cast<double>(MeshCache.GetBytesLoaded())/1048576.0,
|
|
Seconds,MeshCachePath.c_str());
|
|
std::fflush(stdout);
|
|
}
|
|
else if(CacheError!="cache file is missing or unreadable")
|
|
std::printf("[MeshCache] ignored %s: %s\n",MeshCachePath.c_str(),CacheError.c_str());
|
|
}
|
|
|
|
void StartGeneration()
|
|
{
|
|
StopGeneration();
|
|
ReleaseMeshes();
|
|
GenerationStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=generation_setup begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(GenerationStart-ColdProcessStart).count());
|
|
std::string Error;
|
|
if(!ConfigureWorld(Error))
|
|
{std::fprintf(stderr,"World load failed: %s\n",Error.c_str());MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;}
|
|
std::printf("[ColdTimeline] stage=generation_setup configured_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
|
|
bFullDetailReported=false;bInitialClipmapReported=false;bWholeViewPresented=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;
|
|
if(ShapeWorld)
|
|
{
|
|
const auto CoreTiles=ShapeWorld->desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z});
|
|
DesiredTiles.clear();DesiredTiles.reserve(CoreTiles.size());
|
|
for(const auto& Key:CoreTiles)
|
|
DesiredTiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
}
|
|
if(bCaptureOnlyTile)
|
|
{
|
|
const size_t Before=DesiredTiles.size();
|
|
DesiredTiles.erase(std::remove_if(DesiredTiles.begin(),DesiredTiles.end(),[&](const FVoxelTileKey& Key)
|
|
{
|
|
return Key.Level!=CaptureOnlyTileLod||Key.Coord.X!=CaptureOnlyTileX
|
|
||Key.Coord.Y!=CaptureOnlyTileY||Key.Coord.Z!=CaptureOnlyTileZ;
|
|
}),DesiredTiles.end());
|
|
std::printf("[CaptureOnlyTile] key=(%d,%d,%d,lod%d) before=%zu selected=%zu\n",
|
|
CaptureOnlyTileX,CaptureOnlyTileY,CaptureOnlyTileZ,CaptureOnlyTileLod,
|
|
Before,DesiredTiles.size());
|
|
if(DesiredTiles.empty())
|
|
{
|
|
Error="the requested capture-only tile is not in the desired clipmap";
|
|
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
|
|
return;
|
|
}
|
|
}
|
|
if(CaptureTileLimit>0&&CapturePath.size())
|
|
{
|
|
const size_t Before=DesiredTiles.size();
|
|
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
|
|
const double TanHalfFov=std::tan(1.04719755*0.5);
|
|
const double Aspect=static_cast<double>(Width)/std::max(1,Height);
|
|
struct FCaptureTileRank { FVoxelTileKey Tile; double Depth=0.0; };
|
|
std::vector<FCaptureTileRank> Visible;Visible.reserve(DesiredTiles.size());
|
|
for(const FVoxelTileKey& Tile:DesiredTiles)
|
|
{
|
|
const FIntVector Origin=Tile.OriginVoxels();
|
|
const double HalfExtent=Tile.ExtentVoxels()*0.8660254037844386;
|
|
const double DX=Origin.X+Tile.ExtentVoxels()*0.5-CameraVoxels.X;
|
|
const double DY=Origin.Y+Tile.ExtentVoxels()*0.5-CameraVoxels.Y;
|
|
const double DZ=Origin.Z+Tile.ExtentVoxels()*0.5-CameraVoxels.Z;
|
|
const double Depth=DX*F.X+DY*F.Y+DZ*F.Z;
|
|
if(Depth+HalfExtent<0.0)continue;
|
|
const double LateralX=std::abs(DX*R.X+DY*R.Y+DZ*R.Z);
|
|
const double LateralY=std::abs(DX*U.X+DY*U.Y+DZ*U.Z);
|
|
const double ForwardReach=std::max(1.0,Depth+HalfExtent);
|
|
if(LateralX>ForwardReach*TanHalfFov*Aspect+HalfExtent
|
|
||LateralY>ForwardReach*TanHalfFov+HalfExtent)continue;
|
|
Visible.push_back({Tile,std::max(0.0,Depth)});
|
|
}
|
|
std::stable_sort(Visible.begin(),Visible.end(),[](const auto& A,const auto& B)
|
|
{return A.Depth<B.Depth;});
|
|
if(Visible.size()>static_cast<size_t>(CaptureTileLimit))
|
|
Visible.resize(static_cast<size_t>(CaptureTileLimit));
|
|
DesiredTiles.clear();DesiredTiles.reserve(Visible.size());
|
|
for(const FCaptureTileRank& Ranked:Visible)DesiredTiles.push_back(Ranked.Tile);
|
|
std::printf("[CaptureTileLimit] frustum_tiles=%zu selected=%zu original_desired=%zu limit=%d; reference capture only\n",
|
|
Visible.size(),DesiredTiles.size(),Before,CaptureTileLimit);
|
|
}
|
|
DesiredTileSet.clear();DesiredTileSet.reserve(DesiredTiles.size()*2+1);
|
|
for(const FVoxelTileKey& Key:DesiredTiles)DesiredTileSet.insert(Key);
|
|
if(bDrawOnlyTile)
|
|
{
|
|
const FVoxelTileKey DrawKey(FIntVector(DrawOnlyTileX,DrawOnlyTileY,DrawOnlyTileZ),
|
|
DrawOnlyTileLod);
|
|
if(!DesiredTileSet.contains(DrawKey))
|
|
{
|
|
Error="the requested draw-only tile is not in the full desired clipmap";
|
|
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
|
|
PostMessageA(Window,WM_CLOSE,0,0);
|
|
return;
|
|
}
|
|
std::printf("[DrawOnlyTile] key=(%d,%d,%d,lod%d) desired_targets=%zu filter=draw_only\n",
|
|
DrawOnlyTileX,DrawOnlyTileY,DrawOnlyTileZ,DrawOnlyTileLod,DesiredTiles.size());
|
|
}
|
|
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
|
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
|
const int32 StreamWorkers=bShapePipelineActive?(WorkerCount>=8?2:1):WorkerCount;
|
|
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,StreamWorkers,false,
|
|
(!bShapeWorld&&MeshCache.IsLoaded())?&MeshCache:nullptr,
|
|
bFarShapeStagePending?World->FullResClipLevels-1:-1,
|
|
false,false,false,false,ShapeWorld.get());
|
|
if(bGpuFarTimelineEnabled)
|
|
Streamer->EnableColdTimelineTrace(ColdProcessStart,&ColdTimelineFrameOrdinal);
|
|
if(bGpuFarEnabled&&ShapeWorld&&ShapeWorld->hasFarWorld())
|
|
{
|
|
ScheduleGpuFarTiles(DesiredTiles);
|
|
if(bGpuFarAuditEnabled)
|
|
{
|
|
std::vector<FVoxelTileKey> AuditKeys;AuditKeys.reserve(GpuFarCandidateTiles.size());
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(Key.Level>=6&&std::find(GpuFarCandidateTiles.begin(),GpuFarCandidateTiles.end(),Key)
|
|
!=GpuFarCandidateTiles.end())AuditKeys.push_back(Key);
|
|
GpuFarAudit=std::make_unique<FFarSurvivalAudit>(*World,*ShapeWorld,AuditKeys,false);
|
|
}
|
|
}
|
|
if(bShapePipelineActive)Streamer->SetDesiredTileFilter(InitialNeighborhoodTiles);
|
|
else if((CaptureTileLimit>0&&CapturePath.size())||bCaptureOnlyTile)
|
|
Streamer->SetDesiredTileFilter(DesiredTiles);
|
|
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
|
|
if(bShapePipelineActive&&Update.DesiredTiles!=static_cast<int32>(InitialNeighborhoodTiles.size()))
|
|
{
|
|
StopGeneration();
|
|
Error="the initial neighborhood streamer did not accept all 27 tiles";
|
|
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
|
|
return;
|
|
}
|
|
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
|
|
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
|
|
InitialWork.StartTime=GenerationStart;
|
|
bGenerationRunning=true;bRebuildPending=false;
|
|
std::printf("[ColdTimeline] stage=cpu_stream_start submitted_ms=%.3f cpu_jobs=%d gpu_queued=%zu\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
|
Update.NewJobs,GpuFarQueue.size());
|
|
const char* TunnelMode=!bRoundTunnels?"":(bRoundTunnelEnhanced
|
|
?" with detailed round/winding tunnels":" with legacy round/winding tunnels");
|
|
std::printf("Streaming seed %d%s%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
|
|
Seed,TunnelMode,bShapeWorld?" [shape intervals]":"",Update.DesiredTiles,
|
|
Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds);
|
|
StartGpuFarPreparation();
|
|
if(bShapePipelineActive)
|
|
{
|
|
std::string ShapeError;
|
|
if(!StartFullShapeBuild(DesiredTiles,ShapeError))
|
|
{
|
|
StopGeneration();
|
|
std::fprintf(stderr,"World load failed: %s\n",ShapeError.c_str());
|
|
return;
|
|
}
|
|
}
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
void RequestSeed(int32 Delta)
|
|
{
|
|
const uint32 Next=static_cast<uint32>(Seed)+static_cast<uint32>(Delta);
|
|
Seed=static_cast<int32>(Next);bRebuildPending=true;
|
|
std::printf("Seed changed to %d; replacing the current streaming world.\n",Seed);
|
|
}
|
|
|
|
void StartFarShapeGeneration()
|
|
{
|
|
if(!bFarShapeStagePending||!ShapeWorld||ShapeWorld->hasFarWorld()||!Streamer)return;
|
|
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
|
|
ShapeWorld->buildFarWorld();
|
|
bFarShapeStagePending=false;
|
|
Streamer->Stop();
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
Streamer.reset();
|
|
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1,
|
|
false,false,false,false,ShapeWorld.get());
|
|
if(bGpuFarTimelineEnabled)
|
|
Streamer->EnableColdTimelineTrace(ColdProcessStart,&ColdTimelineFrameOrdinal);
|
|
if(bGpuFarEnabled)
|
|
{
|
|
ScheduleGpuFarTiles(DesiredTiles);
|
|
if(bGpuFarAuditEnabled&&World)
|
|
{
|
|
std::vector<FVoxelTileKey> AuditKeys;AuditKeys.reserve(GpuFarCandidateTiles.size());
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(Key.Level>=6&&std::find(GpuFarCandidateTiles.begin(),GpuFarCandidateTiles.end(),Key)
|
|
!=GpuFarCandidateTiles.end())AuditKeys.push_back(Key);
|
|
GpuFarAudit=std::make_unique<FFarSurvivalAudit>(*World,*ShapeWorld,AuditKeys,false);
|
|
}
|
|
}
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
++CrossingIndex;
|
|
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex);
|
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
|
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
|
Work.StartTime=std::chrono::steady_clock::now();
|
|
bGenerationRunning=true;
|
|
std::printf("[FarStreamStart] desired=%d retained=%d new_jobs=%d workers=%d\n",
|
|
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount);
|
|
StartGpuFarPreparation();
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
void RecordGpuCrossingTileReady(const FVoxelTileKey& Key)
|
|
{
|
|
const auto ReadyAt=std::chrono::steady_clock::now();
|
|
for(auto& Pair:CrossingWork)
|
|
{
|
|
FCrossingWork& Work=Pair.second;
|
|
if(Work.bHasNearestNewTile&&Work.NearestNewTile==Key
|
|
&&Work.NearestReadySeconds<0.0&&Work.StartTime.time_since_epoch().count()!=0)
|
|
Work.NearestReadySeconds=std::chrono::duration<double>(ReadyAt-Work.StartTime).count();
|
|
}
|
|
}
|
|
|
|
void ReportGpuTileCompletion(const FVoxelTileKey& Key,FGpuFarTile& Tile,
|
|
uint32 BatchIndex=UINT32_MAX)
|
|
{
|
|
if(Tile.CompletedAt.time_since_epoch().count()!=0)return;
|
|
Tile.CompletedAt=std::chrono::steady_clock::now();
|
|
if(!bGpuFarTimelineEnabled)return;
|
|
std::printf("[GpuTileTimeline] event=completed key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu submitted_ms=%.3f\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,BatchIndex,
|
|
ColdTimelineMilliseconds(Tile.CompletedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
ColdTimelineMilliseconds(Tile.SubmittedAt));
|
|
std::printf("[GpuTileTimeline] event=uploaded key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu method=direct_gpu_compute_output copy_upload=none\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,BatchIndex,
|
|
ColdTimelineMilliseconds(Tile.CompletedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()));
|
|
}
|
|
|
|
void ReportGpuTileResident(const FVoxelTileKey& Key,FGpuFarTile& Tile)
|
|
{
|
|
if(Tile.bColdTimelineResidentReported)return;
|
|
Tile.bColdTimelineResidentReported=true;
|
|
const auto ResidentAt=std::chrono::steady_clock::now();
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[GpuTileTimeline] event=resident key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu submitted_ms=%.3f completed_ms=%.3f\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.BatchIndex,
|
|
ColdTimelineMilliseconds(ResidentAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
ColdTimelineMilliseconds(Tile.SubmittedAt),ColdTimelineMilliseconds(Tile.CompletedAt));
|
|
if(Tile.BatchIndex!=UINT32_MAX&&Tile.BatchIndex<GpuFarBatches.size())
|
|
{
|
|
FGpuFarBatch& Batch=GpuFarBatches[Tile.BatchIndex];
|
|
++Batch.ResidentCount;
|
|
if(bGpuFarTimelineEnabled&&Batch.ResidentCount==Batch.TileCount)
|
|
std::printf("[GpuBatchTimeline] event=resident batch=%u at_ms=%.3f frame=%llu tile_count=%u submitted_ms=%.3f completed_ms=%.3f\n",
|
|
Tile.BatchIndex,ColdTimelineMilliseconds(ResidentAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
|
|
ColdTimelineMilliseconds(Batch.SubmittedAt),ColdTimelineMilliseconds(Batch.CompletedAt));
|
|
}
|
|
}
|
|
|
|
bool IsRenderedTileReady(const FVoxelTileKey& Key)
|
|
{
|
|
if(bGpuFarEnabled&&ShapeWorld&&ShapeWorld->hasFarWorld()
|
|
&&GpuFarEligibleTiles.contains(Key))
|
|
{
|
|
auto It=GpuFarTiles.find(Key);
|
|
if(It==GpuFarTiles.end())return false;
|
|
FGpuFarTile& Tile=It->second;
|
|
if(Tile.BatchIndex!=UINT32_MAX&&Tile.BatchIndex<GpuFarBatches.size())
|
|
{
|
|
FGpuFarBatch& Batch=GpuFarBatches[Tile.BatchIndex];
|
|
if(!Batch.bGpuComplete&&Batch.CompletionEvent&&Context
|
|
&&Context->GetData(Batch.CompletionEvent,nullptr,0,D3D11_ASYNC_GETDATA_DONOTFLUSH)==S_OK)
|
|
{
|
|
Batch.bGpuComplete=true;
|
|
const auto PollAt=std::chrono::steady_clock::now();
|
|
Batch.CompletedAt=PollAt;
|
|
const double PollMs=std::chrono::duration<double,std::milli>(PollAt-ColdProcessStart).count();
|
|
if(GpuFarFirstCompletionPollMs<0.0)GpuFarFirstCompletionPollMs=PollMs;
|
|
GpuFarLastCompletionPollMs=PollMs;++GpuFarCompletionPolls;
|
|
Batch.CompletionEvent->Release();Batch.CompletionEvent=nullptr;
|
|
if(bGpuFarTimelineEnabled)
|
|
{
|
|
std::printf("[GpuBatchTimeline] event=completed batch=%u at_ms=%.3f frame=%llu tile_count=%u submitted_ms=%.3f\n",
|
|
Tile.BatchIndex,ColdTimelineMilliseconds(PollAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
|
|
ColdTimelineMilliseconds(Batch.SubmittedAt));
|
|
std::printf("[GpuBatchTimeline] event=uploaded batch=%u at_ms=%.3f frame=%llu method=direct_gpu_compute_output copy_upload=none\n",
|
|
Tile.BatchIndex,ColdTimelineMilliseconds(PollAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()));
|
|
}
|
|
for(const FVoxelTileKey& BatchKey:Batch.Keys)
|
|
{
|
|
auto BatchTileIt=GpuFarTiles.find(BatchKey);
|
|
if(BatchTileIt!=GpuFarTiles.end())
|
|
{
|
|
BatchTileIt->second.bGpuComplete=true;
|
|
ReportGpuTileCompletion(BatchKey,BatchTileIt->second,Tile.BatchIndex);
|
|
}
|
|
}
|
|
}
|
|
if(Batch.bGpuComplete&&!Batch.bTimingResolved)
|
|
{
|
|
if(VoxelForgeGpuFarViewer::ResolveGpuTiming(Context,Batch.TimingQueries,
|
|
Batch.GpuDispatchMs,&Batch.TimestampFrequency))
|
|
{
|
|
Batch.bTimingResolved=true;
|
|
GpuFarSubmissionSpanMs+=Batch.GpuDispatchMs;
|
|
GpuFarSubmissionSpanMaxMs=std::max(GpuFarSubmissionSpanMaxMs,Batch.GpuDispatchMs);
|
|
}
|
|
}
|
|
if(Batch.bGpuComplete&&Batch.bTimingResolved&&!Tile.bTimingResolved
|
|
&&Batch.TimestampFrequency)
|
|
{
|
|
double TileDispatchMs=0.0;
|
|
if(VoxelForgeGpuFarViewer::ResolveGpuTimestampPair(Context,Tile.TimingQueries,
|
|
Batch.TimestampFrequency,TileDispatchMs))
|
|
{
|
|
Tile.GpuDispatchMs=TileDispatchMs;Tile.bTimingResolved=true;
|
|
GpuFarGpuDispatchMs+=TileDispatchMs;
|
|
GpuFarGpuDispatchMaxMs=std::max(GpuFarGpuDispatchMaxMs,TileDispatchMs);
|
|
}
|
|
}
|
|
if(Batch.bGpuComplete)
|
|
{
|
|
RecordGpuCrossingTileReady(Key);
|
|
ReportGpuTileResident(Key,Tile);
|
|
}
|
|
return Batch.bGpuComplete;
|
|
}
|
|
if(!Tile.bGpuComplete&&Tile.CompletionEvent&&Context
|
|
&&Context->GetData(Tile.CompletionEvent,nullptr,0,D3D11_ASYNC_GETDATA_DONOTFLUSH)==S_OK)
|
|
{
|
|
Tile.bGpuComplete=true;
|
|
Tile.CompletionEvent->Release();Tile.CompletionEvent=nullptr;
|
|
ReportGpuTileCompletion(Key,Tile);
|
|
}
|
|
if(Tile.bGpuComplete&&!Tile.bTimingResolved)
|
|
{
|
|
double AsyncDispatchMs=0.0;
|
|
if(VoxelForgeGpuFarViewer::ResolveGpuTiming(Context,Tile.TimingQueries,AsyncDispatchMs))
|
|
{
|
|
if(Tile.GpuDispatchMs==0.0)
|
|
{
|
|
Tile.GpuDispatchMs=AsyncDispatchMs;
|
|
GpuFarGpuDispatchMs+=AsyncDispatchMs;
|
|
GpuFarGpuDispatchMaxMs=std::max(GpuFarGpuDispatchMaxMs,AsyncDispatchMs);
|
|
}
|
|
Tile.bTimingResolved=true;
|
|
}
|
|
}
|
|
if(Tile.bGpuComplete)
|
|
{
|
|
RecordGpuCrossingTileReady(Key);
|
|
ReportGpuTileResident(Key,Tile);
|
|
}
|
|
return Tile.bGpuComplete;
|
|
}
|
|
return ResidentTiles.contains(Key);
|
|
}
|
|
|
|
void PrepareGpuFarInputs(std::vector<FVoxelTileKey> NeedPreparation)
|
|
{
|
|
if(NeedPreparation.empty())return;
|
|
const auto PrepareStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=gpu_input_export begin_ms=%.3f tiles=%zu\n",
|
|
ColdTimelineMilliseconds(PrepareStart),NeedPreparation.size());
|
|
std::vector<std::optional<VoxelForgeCore::FarFieldTileInput>> Inputs(NeedPreparation.size());
|
|
std::vector<std::string> Errors(NeedPreparation.size());
|
|
std::vector<std::chrono::steady_clock::time_point> FinishedAt(NeedPreparation.size());
|
|
std::vector<uint64> FinishedFrame(NeedPreparation.size());
|
|
const std::size_t ThreadCount=std::min<std::size_t>(8,NeedPreparation.size());
|
|
std::atomic<std::size_t> Next{0};
|
|
std::vector<double> WorkerElapsed(ThreadCount,0.0);
|
|
std::vector<std::thread> Workers;Workers.reserve(ThreadCount);
|
|
for(std::size_t Worker=0;Worker<ThreadCount;++Worker)
|
|
{
|
|
Workers.emplace_back([&,Worker]
|
|
{
|
|
const auto WorkerStart=std::chrono::steady_clock::now();
|
|
for(;;)
|
|
{
|
|
const std::size_t Index=Next.fetch_add(1,std::memory_order_relaxed);
|
|
if(Index>=NeedPreparation.size())break;
|
|
const FVoxelTileKey& Key=NeedPreparation[Index];
|
|
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
|
|
try
|
|
{
|
|
Inputs[Index]=Key.Level<World->FullResClipLevels
|
|
?ShapeWorld->exportGpuFieldTile(CoreKey,Errors[Index])
|
|
:ShapeWorld->exportFarFieldTile(CoreKey,Errors[Index]);
|
|
if(Inputs[Index]&&Key.Level>=World->FullResClipLevels)
|
|
{
|
|
Inputs[Index]->visibleSkirtFaceMask=World->bGenerateSkirts
|
|
&&bGpuSkirtEmitEnabled
|
|
?ComputeVisibleSkirtFaceMask(Key,DesiredTileSet):0;
|
|
Inputs[Index]->skirtDepthVoxels=std::max(1.0f,
|
|
static_cast<float>(World->SkirtCells))*Key.StepVoxels();
|
|
if(!bGpuConnectorRepairEnabled)
|
|
{
|
|
Inputs[Index]->connectorPanels.clear();
|
|
Inputs[Index]->connectorRingCount=0;
|
|
Inputs[Index]->connectorVertexCount=0;
|
|
}
|
|
}
|
|
}
|
|
catch(const std::exception& Error) { Errors[Index]=Error.what(); }
|
|
FinishedAt[Index]=std::chrono::steady_clock::now();
|
|
FinishedFrame[Index]=ColdTimelineFrame();
|
|
}
|
|
WorkerElapsed[Worker]=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-WorkerStart).count();
|
|
});
|
|
}
|
|
for(std::thread& Worker:Workers)Worker.join();
|
|
const double PreparationMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-PrepareStart).count();
|
|
double WorkerSumMs=0.0;for(const double Elapsed:WorkerElapsed)WorkerSumMs+=Elapsed;
|
|
GpuFarPreparationWallMs+=PreparationMs;GpuFarPreparationWorkerMs+=WorkerSumMs;
|
|
std::size_t Failed=0;
|
|
for(std::size_t Index=0;Index<NeedPreparation.size();++Index)
|
|
{
|
|
if(Inputs[Index])
|
|
{
|
|
GpuFarPreparedInputs.emplace(NeedPreparation[Index],std::move(*Inputs[Index]));
|
|
GpuFarPreparedAt.emplace(NeedPreparation[Index],FinishedAt[Index]);
|
|
const FVoxelTileKey& Key=NeedPreparation[Index];
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[GpuTileTimeline] event=prepared key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
ColdTimelineMilliseconds(FinishedAt[Index]),
|
|
static_cast<unsigned long long>(FinishedFrame[Index]));
|
|
}
|
|
else
|
|
{
|
|
++Failed;
|
|
GpuFarPreparationErrors.emplace(NeedPreparation[Index],Errors[Index].empty()
|
|
?"far-field input export failed":Errors[Index]);
|
|
}
|
|
}
|
|
std::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
|
|
NeedPreparation.size(),ThreadCount,PreparationMs,WorkerSumMs,Failed);
|
|
std::printf("[ColdTimeline] stage=gpu_input_export end_ms=%.3f duration_ms=%.3f worker_sum_ms=%.3f\n",
|
|
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),
|
|
PreparationMs,WorkerSumMs);
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
|
|
{
|
|
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
|
|
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();GpuFarPreparedAt.clear();
|
|
GpuFarCandidateTiles.clear();GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
|
|
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld())return;
|
|
std::vector<FVoxelTileKey> CpuTiles;CpuTiles.reserve(Keys.size());
|
|
std::vector<FVoxelTileKey> Eligible;Eligible.reserve(Keys.size());
|
|
uint64 SkirtTransitionFallbackTiles=0;
|
|
uint64 RemoteLod3CpuFallbackTiles=0,RemoteLod4CpuFallbackTiles=0;
|
|
for(const FVoxelTileKey& Key:Keys)
|
|
{
|
|
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
|
|
const bool NearGpuEligible=Key.Level<World->FullResClipLevels;
|
|
if(Key.Level==3||Key.Level==4)
|
|
{
|
|
CpuTiles.push_back(Key);
|
|
if(Key.Level==3)++RemoteLod3CpuFallbackTiles;
|
|
else ++RemoteLod4CpuFallbackTiles;
|
|
if(bDrawOnlyTile&&IsDrawOnlyTile(Key))
|
|
std::printf("[DrawOnlyTileRoute] key=(%d,%d,%d,lod%d) route=cpu_remote_band_fallback\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level);
|
|
continue;
|
|
}
|
|
if(NearGpuEligible||ShapeWorld->isFarFieldTileEligible(CoreKey))
|
|
{
|
|
const uint8 VisibleSkirtFaces=World->bGenerateSkirts
|
|
?ComputeVisibleSkirtFaceMask(Key,DesiredTileSet):0;
|
|
if(Key.Level>=World->FullResClipLevels)
|
|
{
|
|
const auto Existing=GpuFarTiles.find(Key);
|
|
if(Existing!=GpuFarTiles.end()
|
|
&&Existing->second.VisibleSkirtFaceMask!=VisibleSkirtFaces)
|
|
{
|
|
const uint32 ReusedSlot=Existing->second.OutputSlot;
|
|
ReleaseGpuFarTile(Existing->second);
|
|
GpuFarTiles.erase(Existing);
|
|
GpuFarFreeOutputSlots.push_back(ReusedSlot);
|
|
}
|
|
}
|
|
if(VisibleSkirtFaces!=0&&NearGpuEligible)
|
|
{
|
|
// Keep fine tiles on the CPU reference path. Far tiles carry the
|
|
// CPU-decided visible face mask to the deterministic GPU skirt pass.
|
|
CpuTiles.push_back(Key);
|
|
++SkirtTransitionFallbackTiles;
|
|
}
|
|
else Eligible.push_back(Key);
|
|
}
|
|
else CpuTiles.push_back(Key);
|
|
if(bDrawOnlyTile&&IsDrawOnlyTile(Key))
|
|
std::printf("[DrawOnlyTileRoute] key=(%d,%d,%d,lod%d) route=%s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
Eligible.empty()||Eligible.back()!=Key?"cpu_fallback":"gpu_eligible");
|
|
}
|
|
const std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> CurrentEligible(
|
|
Eligible.begin(),Eligible.end());
|
|
for(auto It=GpuFarTiles.begin();It!=GpuFarTiles.end();)
|
|
{
|
|
if(CurrentEligible.contains(It->first)){++It;continue;}
|
|
GpuFarFreeOutputSlots.push_back(It->second.OutputSlot);
|
|
ReleaseGpuFarTile(It->second);
|
|
It=GpuFarTiles.erase(It);
|
|
}
|
|
if(RemoteLod3CpuFallbackTiles+RemoteLod4CpuFallbackTiles>0)
|
|
std::printf("[GpuRemoteBandFallback] lod3_cpu_tiles=%llu lod4_cpu_tiles=%llu reason=remote_capture_pixel_coverage\n",
|
|
static_cast<unsigned long long>(RemoteLod3CpuFallbackTiles),
|
|
static_cast<unsigned long long>(RemoteLod4CpuFallbackTiles));
|
|
if(Eligible.size()>GpuFarOutputCapacitySlots)
|
|
{
|
|
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
|
|
GpuFarTiles.clear();GpuFarFreeOutputSlots.clear();GpuFarNextOutputSlot=0;
|
|
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
|
|
}
|
|
if(!Eligible.empty()&&GpuFarOutputCapacitySlots==0)
|
|
GpuFarOutputCapacitySlots=static_cast<uint32>(Eligible.size());
|
|
if(bCaptureOnlyTile&&CaptureOutputSlot>=0)
|
|
{
|
|
GpuFarOutputCapacitySlots=std::max(GpuFarOutputCapacitySlots,
|
|
static_cast<uint32>(CaptureOutputSlot+1));
|
|
GpuFarNextOutputSlot=static_cast<uint32>(CaptureOutputSlot);
|
|
std::printf("[CaptureOutputSlotProbe] slot=%d pool_slots=%u\n",
|
|
CaptureOutputSlot,GpuFarOutputCapacitySlots);
|
|
}
|
|
GpuFarCandidateTiles=std::move(Eligible);
|
|
GpuFarCpuFallbackTiles=std::move(CpuTiles);
|
|
GpuFarNeedPreparation.reserve(GpuFarCandidateTiles.size());
|
|
for(const FVoxelTileKey& Key:GpuFarCandidateTiles)
|
|
if(!GpuFarTiles.contains(Key))GpuFarNeedPreparation.push_back(Key);
|
|
if(Streamer)Streamer->SetDesiredTileFilter(GpuFarCpuFallbackTiles);
|
|
const std::size_t NearGpuTiles=static_cast<std::size_t>(std::count_if(
|
|
GpuFarCandidateTiles.begin(),GpuFarCandidateTiles.end(),[&](const FVoxelTileKey& Key)
|
|
{return Key.Level<World->FullResClipLevels;}));
|
|
std::printf("[GpuFieldEligibility] candidate_gpu_tiles=%zu near_gpu_tiles=%zu cpu_fallback_tiles=%zu\n",
|
|
GpuFarCandidateTiles.size(),NearGpuTiles,GpuFarCpuFallbackTiles.size());
|
|
std::printf("[GpuSkirtSeamFallback] near_tiles=%llu reason=fine_visible_lod_skirts gpu_far_skirts=active\n",
|
|
static_cast<unsigned long long>(SkirtTransitionFallbackTiles));
|
|
}
|
|
|
|
void AddGpuFarCpuFallbackTiles(const std::vector<FVoxelTileKey>& Additional)
|
|
{
|
|
if(Additional.empty()||!Streamer)return;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Seen(
|
|
GpuFarCpuFallbackTiles.begin(),GpuFarCpuFallbackTiles.end());
|
|
std::size_t Added=0;
|
|
for(const FVoxelTileKey& Key:Additional)
|
|
if(Seen.insert(Key).second){GpuFarCpuFallbackTiles.push_back(Key);++Added;}
|
|
if(Added==0)return;
|
|
Streamer->SetDesiredTileFilter(GpuFarCpuFallbackTiles);
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(
|
|
CameraVoxels,ResidentKeys,CrossingIndex);
|
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
|
Work.SubmittedJobs+=Update.NewJobs;Work.PendingJobs+=Update.NewJobs;
|
|
std::printf("[GpuFarPrepFallbackAdded] tiles=%zu queued_jobs=%d cpu_fallback_total=%zu\n",
|
|
Added,Update.NewJobs,GpuFarCpuFallbackTiles.size());
|
|
}
|
|
|
|
void StartGpuFarPreparation()
|
|
{
|
|
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld()||bGpuFarPreparationStarted)return;
|
|
bGpuFarPreparationStarted=true;
|
|
if(GpuFarCandidateTiles.empty())
|
|
{
|
|
bGpuFarInputsFinalized=true;
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
const auto ReserveStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool begin_ms=%.3f slots=%u\n",
|
|
ColdTimelineMilliseconds(ReserveStart),GpuFarOutputCapacitySlots);
|
|
std::string PoolError;
|
|
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
|
|
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
|
|
bGpuFarEnabled=false;GpuFarOutputCapacitySlots=0;
|
|
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
|
|
GpuFarTiles.clear();GpuFarCandidateTiles.clear();GpuFarQueue.clear();GpuFarQueued.clear();
|
|
GpuFarNeedPreparation.clear();GpuFarEligibleTiles.clear();
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
bGpuFarInputsFinalized=true;
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
GpuFarPoolReserveMs+=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ReserveStart).count();
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool end_ms=%.3f duration_ms=%.3f\n",
|
|
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),GpuFarPoolReserveMs);
|
|
SampleGpuFarMemory();
|
|
if(GpuFarNeedPreparation.empty())
|
|
{
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
const std::vector<FVoxelTileKey> NeedPreparation=GpuFarNeedPreparation;
|
|
GpuFarPreparationThread=std::thread([this,NeedPreparation]()
|
|
{
|
|
try { PrepareGpuFarInputs(NeedPreparation); }
|
|
catch(const std::exception& Error)
|
|
{
|
|
for(const FVoxelTileKey& Key:NeedPreparation)
|
|
GpuFarPreparationErrors.emplace(Key,Error.what());
|
|
std::fprintf(stderr,"[GpuFarInputPrepareError] %s\n",Error.what());
|
|
}
|
|
catch(...)
|
|
{
|
|
for(const FVoxelTileKey& Key:NeedPreparation)
|
|
GpuFarPreparationErrors.emplace(Key,"unknown input preparation failure");
|
|
std::fprintf(stderr,"[GpuFarInputPrepareError] unknown failure\n");
|
|
}
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_release);
|
|
});
|
|
}
|
|
|
|
void SortGpuFarQueue()
|
|
{
|
|
std::sort(GpuFarQueue.begin(),GpuFarQueue.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
if(A.Level!=B.Level)return A.Level<B.Level;
|
|
const FIntVector AO=A.OriginVoxels(),BO=B.OriginVoxels();
|
|
const double AC=(AO.X+0.5*A.ExtentVoxels()-Position.X*4.0)*(AO.X+0.5*A.ExtentVoxels()-Position.X*4.0)
|
|
+(AO.Y+0.5*A.ExtentVoxels()-Position.Y*4.0)*(AO.Y+0.5*A.ExtentVoxels()-Position.Y*4.0)
|
|
+(AO.Z+0.5*A.ExtentVoxels()-Position.Z*4.0)*(AO.Z+0.5*A.ExtentVoxels()-Position.Z*4.0);
|
|
const double BC=(BO.X+0.5*B.ExtentVoxels()-Position.X*4.0)*(BO.X+0.5*B.ExtentVoxels()-Position.X*4.0)
|
|
+(BO.Y+0.5*B.ExtentVoxels()-Position.Y*4.0)*(BO.Y+0.5*B.ExtentVoxels()-Position.Y*4.0)
|
|
+(BO.Z+0.5*B.ExtentVoxels()-Position.Z*4.0)*(BO.Z+0.5*B.ExtentVoxels()-Position.Z*4.0);
|
|
return AC<BC;
|
|
});
|
|
GpuFarQueued.clear();for(const FVoxelTileKey& Key:GpuFarQueue)GpuFarQueued.insert(Key);
|
|
}
|
|
|
|
bool FinalizeGpuFarPreparation()
|
|
{
|
|
if(bGpuFarInputsFinalized)return true;
|
|
if(!bGpuFarPreparationStarted
|
|
||!bGpuFarPreparationFinished.load(std::memory_order_acquire))return false;
|
|
if(GpuFarPreparationThread.joinable())GpuFarPreparationThread.join();
|
|
if(bGpuFarShaderPrewarm&&!bGpuWarmupCompleted)
|
|
{
|
|
if(GpuShaderWarmupThread.joinable())GpuShaderWarmupThread.join();
|
|
if(!bGpuShaderWarmupQueueSucceeded)
|
|
{
|
|
std::fprintf(stderr,"[GpuShaderWarmupError] queue: %s\n",
|
|
GpuShaderWarmupQueueError.empty()?"warmup queue failed":GpuShaderWarmupQueueError.c_str());
|
|
bGpuFarEnabled=false;
|
|
}
|
|
else
|
|
{
|
|
std::string WarmupError;
|
|
if(!VoxelForgeGpuFarViewer::CompleteFarShaderWarmup(Device,Context,
|
|
GpuFarShaderPath,WarmupError))
|
|
{
|
|
std::fprintf(stderr,"[GpuShaderWarmupError] %s\n",WarmupError.c_str());
|
|
bGpuFarEnabled=false;
|
|
}
|
|
else bGpuWarmupCompleted=true;
|
|
}
|
|
}
|
|
if(!bGpuFarEnabled)
|
|
{
|
|
GpuFarCandidateTiles.clear();GpuFarQueue.clear();GpuFarQueued.clear();
|
|
GpuFarEligibleTiles.clear();
|
|
for(auto& Pair:GpuFarTiles)ReleaseGpuFarTile(Pair.second);
|
|
GpuFarTiles.clear();
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
bGpuFarInputsFinalized=true;
|
|
return false;
|
|
}
|
|
std::vector<FVoxelTileKey> Failed;
|
|
for(const FVoxelTileKey& Key:GpuFarCandidateTiles)
|
|
{
|
|
if(GpuFarTiles.contains(Key))
|
|
{GpuFarEligibleTiles.insert(Key);continue;}
|
|
if(!GpuFarPreparedInputs.contains(Key))
|
|
{
|
|
Failed.push_back(Key);
|
|
const auto Error=GpuFarPreparationErrors.find(Key);
|
|
std::fprintf(stderr,"[GpuFieldInputFallback] key=(%d,%d,%d,lod%d) reason=%s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
Error==GpuFarPreparationErrors.end()?"missing prepared input":Error->second.c_str());
|
|
continue;
|
|
}
|
|
GpuFarEligibleTiles.insert(Key);
|
|
GpuFarQueue.push_back(Key);
|
|
}
|
|
SortGpuFarQueue();
|
|
AddGpuFarCpuFallbackTiles(Failed);
|
|
bGpuFarInputsFinalized=true;
|
|
const std::size_t NearGpuTiles=static_cast<std::size_t>(std::count_if(
|
|
GpuFarEligibleTiles.begin(),GpuFarEligibleTiles.end(),[&](const FVoxelTileKey& Key)
|
|
{return Key.Level<World->FullResClipLevels;}));
|
|
std::printf("[GpuFieldEligibility] eligible_gpu_tiles=%zu near_gpu_tiles=%zu cpu_fallback_tiles=%zu\n",
|
|
GpuFarEligibleTiles.size(),NearGpuTiles,GpuFarCpuFallbackTiles.size());
|
|
return true;
|
|
}
|
|
|
|
void PumpGpuFarGeneration()
|
|
{
|
|
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld()||!Device||!Context
|
|
||!FinalizeGpuFarPreparation()||GpuFarQueue.empty())return;
|
|
const bool bColdDrainBeforeWholeView=!bInitialClipmapReported&&!bGpuFarAuditEnabled
|
|
&&!bDisableGpuColdStartDrain;
|
|
const auto PumpStart=std::chrono::steady_clock::now();
|
|
const double PumpMs=std::chrono::duration<double,std::milli>(PumpStart-ColdProcessStart).count();
|
|
if(GpuFarFirstPumpMs<0.0)GpuFarFirstPumpMs=PumpMs;
|
|
if(LastGpuFarPumpAt.time_since_epoch().count()!=0)
|
|
{
|
|
const double GapMs=std::chrono::duration<double,std::milli>(PumpStart-LastGpuFarPumpAt).count();
|
|
GpuFarPumpGapMs+=GapMs;GpuFarPumpGapMaxMs=std::max(GpuFarPumpGapMaxMs,GapMs);
|
|
}
|
|
LastGpuFarPumpAt=PumpStart;GpuFarLastPumpMs=PumpMs;++GpuFarPumpCalls;
|
|
const bool bDeferredBatch=!bGpuFarAuditEnabled;
|
|
if(bDeferredBatch)
|
|
{
|
|
std::string BatchError;
|
|
if(!VoxelForgeGpuFarViewer::BeginFarTileBatch(Device,Context,GpuFarShaderPath,BatchError))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarBatchError] begin: %s\n",BatchError.c_str());
|
|
bGpuFarEnabled=false;return;
|
|
}
|
|
}
|
|
const auto Start=std::chrono::steady_clock::now();
|
|
int32 Built=0;
|
|
std::vector<FVoxelTileKey> BuiltKeys;
|
|
BuiltKeys.reserve(bColdDrainBeforeWholeView?GpuFarQueue.size():32);
|
|
do
|
|
{
|
|
const FVoxelTileKey Key=GpuFarQueue.front();GpuFarQueue.pop_front();GpuFarQueued.erase(Key);
|
|
if(!DesiredTileSet.contains(Key)||GpuFarTiles.contains(Key))continue;
|
|
FGpuFarTile Tile;
|
|
if(!GpuFarFreeOutputSlots.empty())
|
|
{Tile.OutputSlot=GpuFarFreeOutputSlots.back();GpuFarFreeOutputSlots.pop_back();}
|
|
else if(GpuFarNextOutputSlot<GpuFarOutputCapacitySlots)
|
|
Tile.OutputSlot=GpuFarNextOutputSlot++;
|
|
else
|
|
{
|
|
std::fprintf(stderr,"[GpuFarPoolError] no free far-output slot for queued tile\n");
|
|
bGpuFarEnabled=false;break;
|
|
}
|
|
VoxelForgeGpuFarViewer::CpuMesh AuditMesh;
|
|
std::string Error;double EnqueueMs=0.0,GpuDispatchMs=0.0;
|
|
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
|
|
const auto PreparedInputIt=GpuFarPreparedInputs.find(Key);
|
|
const VoxelForgeCore::FarFieldTileInput* PreparedInput=PreparedInputIt==GpuFarPreparedInputs.end()
|
|
?nullptr:&PreparedInputIt->second;
|
|
if(!PreparedInput)
|
|
{
|
|
const auto PrepError=GpuFarPreparationErrors.find(Key);
|
|
std::fprintf(stderr,"[GpuFarInputError] key=(%d,%d,%d,lod%d): %s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
PrepError==GpuFarPreparationErrors.end()?"prepared far input is missing":PrepError->second.c_str());
|
|
GpuFarFreeOutputSlots.push_back(Tile.OutputSlot);
|
|
bGpuFarEnabled=false;
|
|
break;
|
|
}
|
|
const uint8 PreparedSkirtFaceMask=PreparedInput->visibleSkirtFaceMask;
|
|
if(!VoxelForgeGpuFarViewer::BuildFarTile(Device,Context,*ShapeWorld,CoreKey,
|
|
PreparedInput,
|
|
GpuFarShaderPath,&Tile.VertexBuffer,&Tile.IndexBuffer,&Tile.IndirectArgs,
|
|
&Tile.ConnectorVertexBuffer,&Tile.ConnectorIndexBuffer,
|
|
Tile.OutputSlot,GpuFarOutputCapacitySlots,&Tile.IndirectArgsByteOffset,
|
|
bDeferredBatch?nullptr:&Tile.CompletionEvent,
|
|
&Tile.TimingQueries,
|
|
(bGpuFarAuditEnabled&&Key.Level>=6)?&AuditMesh:nullptr,
|
|
&EnqueueMs,&GpuDispatchMs,Error))
|
|
{
|
|
GpuFarFreeOutputSlots.push_back(Tile.OutputSlot);
|
|
std::fprintf(stderr,"[GpuFarTileError] key=(%d,%d,%d,lod%d): %s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Error.c_str());
|
|
if(PreparedInputIt!=GpuFarPreparedInputs.end())GpuFarPreparedInputs.erase(PreparedInputIt);
|
|
bGpuFarEnabled=false;
|
|
break;
|
|
}
|
|
if(PreparedInputIt!=GpuFarPreparedInputs.end())GpuFarPreparedInputs.erase(PreparedInputIt);
|
|
if(bGpuFarAuditEnabled&&GpuFarAudit&&Key.Level>=6)
|
|
{
|
|
FStandaloneCpuTile AuditTile;AuditTile.Key=Key;
|
|
AuditTile.SurfaceVertexCount=static_cast<uint32>(AuditMesh.vertices.size());
|
|
AuditTile.Vertices.reserve(AuditMesh.vertices.size());
|
|
for(const auto& Vertex:AuditMesh.vertices)
|
|
AuditTile.Vertices.push_back({
|
|
{Vertex.position[0]*0.01f,Vertex.position[1]*0.01f,Vertex.position[2]*0.01f},
|
|
{Vertex.normal[0],Vertex.normal[1],Vertex.normal[2]}});
|
|
AuditTile.Indices=std::move(AuditMesh.indices);
|
|
GpuFarAudit->AuditTile(AuditTile);
|
|
}
|
|
Tile.VertexCapacityBytes=static_cast<uint64>(
|
|
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile)*32u;
|
|
Tile.VisibleSkirtFaceMask=PreparedSkirtFaceMask;
|
|
const auto PreparedAtIt=GpuFarPreparedAt.find(Key);
|
|
if(PreparedAtIt!=GpuFarPreparedAt.end())Tile.PreparedAt=PreparedAtIt->second;
|
|
Tile.SubmittedAt=std::chrono::steady_clock::now();
|
|
Tile.GpuDispatchMs=GpuDispatchMs;
|
|
GpuFarEnqueueWallSeconds+=EnqueueMs/1000.0;
|
|
if(GpuDispatchMs>0.0)
|
|
{
|
|
GpuFarGpuDispatchMs+=GpuDispatchMs;
|
|
GpuFarGpuDispatchMaxMs=std::max(GpuFarGpuDispatchMaxMs,GpuDispatchMs);
|
|
Tile.bTimingResolved=true;
|
|
}
|
|
++GpuFarTilesSubmitted;++Built;
|
|
GpuFarTiles.emplace(Key,Tile);
|
|
BuiltKeys.push_back(Key);
|
|
if(!bDeferredBatch&&bGpuFarTimelineEnabled)
|
|
std::printf("[GpuTileTimeline] event=submitted key=(%d,%d,%d,lod%d) batch=none at_ms=%.3f frame=%llu prepared_ms=%.3f path=direct_dispatch\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
ColdTimelineMilliseconds(Tile.SubmittedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
ColdTimelineMilliseconds(Tile.PreparedAt));
|
|
if(!bGpuFarFirstTileReported)
|
|
{
|
|
bGpuFarFirstTileReported=true;
|
|
std::printf("[GpuFarFirstTile] key=(%d,%d,%d,lod%d) cpu_export_upload_enqueue_ms=%.3f vertex_capacity_mb=%.3f mode=direct-indirect-draw\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,EnqueueMs,
|
|
Tile.VertexCapacityBytes/(1024.0*1024.0));
|
|
std::fflush(stdout);
|
|
}
|
|
}
|
|
while(!GpuFarQueue.empty()&&(bColdDrainBeforeWholeView
|
|
||(Built<32&&std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-Start).count()<4.0)));
|
|
if(Built>0)
|
|
{
|
|
if(bDeferredBatch)
|
|
{
|
|
FGpuFarBatch Batch;Batch.TileCount=static_cast<uint32>(Built);
|
|
Batch.PreparedAt=std::chrono::steady_clock::now();
|
|
Batch.Keys=BuiltKeys;
|
|
const uint32 BatchIndex=static_cast<uint32>(GpuFarBatches.size());
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[GpuBatchTimeline] event=prepared batch=%u at_ms=%.3f frame=%llu tile_count=%u path=direct_gpu_output\n",
|
|
BatchIndex,ColdTimelineMilliseconds(Batch.PreparedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount);
|
|
std::string BatchError;
|
|
if(!VoxelForgeGpuFarViewer::SubmitFarTileBatch(Device,Context,GpuFarShaderPath,
|
|
&Batch.CompletionEvent,&Batch.TimingQueries,BatchError))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarBatchError] submit: %s\n",BatchError.c_str());
|
|
bGpuFarEnabled=false;
|
|
}
|
|
else
|
|
{
|
|
Batch.SubmittedAt=std::chrono::steady_clock::now();
|
|
const double SubmitMs=std::chrono::duration<double,std::milli>(
|
|
Batch.SubmittedAt-ColdProcessStart).count();
|
|
if(GpuFarFirstSubmitMs<0.0)GpuFarFirstSubmitMs=SubmitMs;
|
|
GpuFarLastSubmitMs=SubmitMs;
|
|
GpuFarBatches.push_back(Batch);
|
|
for(const FVoxelTileKey& Key:BuiltKeys)
|
|
{
|
|
auto It=GpuFarTiles.find(Key);
|
|
if(It!=GpuFarTiles.end())
|
|
{
|
|
It->second.BatchIndex=BatchIndex;
|
|
It->second.SubmittedAt=Batch.SubmittedAt;
|
|
if(bGpuFarTimelineEnabled)std::printf("[GpuTileTimeline] event=submitted key=(%d,%d,%d,lod%d) batch=%u at_ms=%.3f frame=%llu prepared_ms=%.3f\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,BatchIndex,
|
|
ColdTimelineMilliseconds(Batch.SubmittedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
ColdTimelineMilliseconds(It->second.PreparedAt));
|
|
}
|
|
}
|
|
if(bGpuFarTimelineEnabled)std::printf("[GpuBatchTimeline] event=submitted batch=%u at_ms=%.3f frame=%llu tile_count=%u\n",
|
|
BatchIndex,ColdTimelineMilliseconds(Batch.SubmittedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount);
|
|
++GpuFarSubmissionBatches;
|
|
GpuFarMaxTilesPerSubmissionBatch=std::max<uint32>(GpuFarMaxTilesPerSubmissionBatch,
|
|
static_cast<uint32>(Built));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
++GpuFarSubmissionBatches;
|
|
GpuFarMaxTilesPerSubmissionBatch=std::max<uint32>(GpuFarMaxTilesPerSubmissionBatch,
|
|
static_cast<uint32>(Built));
|
|
}
|
|
}
|
|
SampleGpuFarMemory();
|
|
const double PumpWallMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-PumpStart).count();
|
|
GpuFarPumpWallMs+=PumpWallMs;GpuFarPumpMaxWallMs=std::max(GpuFarPumpMaxWallMs,PumpWallMs);
|
|
}
|
|
|
|
void ToggleFarRenderer()
|
|
{
|
|
if(!bShapeWorld||!ShapeWorld||!ShapeWorld->hasFarWorld())
|
|
{
|
|
bGpuFarEnabled=!bGpuFarEnabled;
|
|
std::printf("[FarRenderer] GPU far %s; it will take effect when far shapes are ready.\n",
|
|
bGpuFarEnabled?"enabled":"disabled");
|
|
return;
|
|
}
|
|
bGpuFarEnabled=!bGpuFarEnabled;
|
|
if(bGpuFarEnabled)
|
|
{
|
|
bPresentCpuFrameBeforeGpu=true;
|
|
bHoldCpuFrameDuringGpuWarmup=true;
|
|
bCpuReadyFramePresentedBeforeGpu=false;
|
|
ScheduleGpuFarTiles(DesiredTiles);
|
|
std::vector<FVoxelTileKey> CpuFar;
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(GpuFarEligibleTiles.contains(Key)&&ResidentTiles.contains(Key))CpuFar.push_back(Key);
|
|
for(const FVoxelTileKey& Key:CpuFar)RemoveResidentTile(Key);
|
|
}
|
|
else
|
|
{
|
|
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
|
|
Streamer->SetDesiredTileFilter(DesiredTiles);
|
|
}
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
++CrossingIndex;
|
|
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(
|
|
CameraVoxels,ResidentKeys,CrossingIndex);
|
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
|
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
|
Work.StartTime=std::chrono::steady_clock::now();
|
|
std::printf("[FarRenderer] mode=%s cpu_field_jobs=%d gpu_field_ready=%zu gpu_field_queued=%zu (H toggles)\n",
|
|
bGpuFarEnabled?"GPU":"CPU",Update.NewJobs,GpuFarTiles.size(),GpuFarQueue.size());
|
|
}
|
|
|
|
void TryCompleteShapePipeline()
|
|
{
|
|
if(!bShapePipelineActive||InitialNeighborhoodTiles.empty())return;
|
|
bool bNeighborhoodUploaded=true;
|
|
for(const FVoxelTileKey& Key:InitialNeighborhoodTiles)
|
|
if(!ResidentTiles.contains(Key)){bNeighborhoodUploaded=false;break;}
|
|
bNeighborhoodUploaded &= DirtyBatchSet.empty();
|
|
if(bNeighborhoodUploaded&&!bNeighborhoodReadyReported)
|
|
{
|
|
bNeighborhoodReadyReported=true;
|
|
const double Elapsed=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count();
|
|
std::printf("[NeighborhoodReady] %.3fs after cold process start; tiles=%zu/%zu vertices=%llu indices=%llu\n",
|
|
Elapsed,InitialNeighborhoodTiles.size(),InitialNeighborhoodTiles.size(),
|
|
static_cast<unsigned long long>(TotalVertices),
|
|
static_cast<unsigned long long>(TotalIndices));
|
|
std::fflush(stdout);
|
|
}
|
|
if(!bNeighborhoodReadyReported
|
|
||!bFullShapeBuildFinished.load(std::memory_order_acquire))return;
|
|
if(FullShapeBuildThread.joinable())FullShapeBuildThread.join();
|
|
if(!bFullShapeBuildSucceeded||!PendingFullShapeWorld)
|
|
{
|
|
bShapePipelineActive=false;bGenerationRunning=false;
|
|
std::fprintf(stderr,"[ShapePipeline] full shape build failed: %s\n",
|
|
FullShapeBuildError.c_str());
|
|
PostMessageA(Window,WM_CLOSE,0,0);
|
|
return;
|
|
}
|
|
|
|
const double FullShapeSeconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-FullShapeBuildStart).count();
|
|
if(Streamer)Streamer->Stop();
|
|
Streamer.reset();
|
|
ShapeWorld=std::move(PendingFullShapeWorld);
|
|
bFarShapeStagePending=false;bShapePipelineActive=false;
|
|
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount,false,nullptr,-1,
|
|
false,false,false,false,ShapeWorld.get());
|
|
if(bGpuFarEnabled&&ShapeWorld->hasFarWorld())
|
|
{
|
|
ScheduleGpuFarTiles(DesiredTiles);
|
|
if(bGpuFarAuditEnabled)
|
|
{
|
|
std::vector<FVoxelTileKey> AuditKeys;AuditKeys.reserve(GpuFarEligibleTiles.size());
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(Key.Level>=6&&GpuFarEligibleTiles.contains(Key))AuditKeys.push_back(Key);
|
|
GpuFarAudit=std::make_unique<FFarSurvivalAudit>(*World,*ShapeWorld,AuditKeys,false);
|
|
}
|
|
std::vector<FVoxelTileKey> CpuResidentNear;
|
|
for(const auto& Pair:ResidentTiles)
|
|
if(Pair.first.Level<World->FullResClipLevels
|
|
&&GpuFarEligibleTiles.contains(Pair.first))CpuResidentNear.push_back(Pair.first);
|
|
for(const FVoxelTileKey& Key:CpuResidentNear)RemoveResidentTile(Key);
|
|
}
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
++CrossingIndex;
|
|
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,ResidentKeys,CrossingIndex);
|
|
if(Update.DesiredTiles!=static_cast<int32>(DesiredTiles.size()))
|
|
{
|
|
Streamer->Stop();Streamer.reset();bGenerationRunning=false;
|
|
std::fprintf(stderr,"[ShapePipeline] full streamer accepted %d of %zu desired tiles.\n",
|
|
Update.DesiredTiles,DesiredTiles.size());
|
|
PostMessageA(Window,WM_CLOSE,0,0);
|
|
return;
|
|
}
|
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
|
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
|
Work.StartTime=std::chrono::steady_clock::now();
|
|
bGenerationRunning=true;
|
|
std::printf("[ShapePipelineFullReady] full_shape=%.3fs retained=%d new_jobs=%d workers=%d; filling outward\n",
|
|
FullShapeSeconds,Update.RetainedResidentTiles,Update.NewJobs,WorkerCount);
|
|
std::fflush(stdout);
|
|
}
|
|
|
|
void MarkBatchDirty(const FStandaloneBatchKey& Key)
|
|
{DirtyBatchSet.insert(Key);}
|
|
|
|
void RefreshResidentSkirtMasks()
|
|
{
|
|
for(auto& [Key,Resident]:ResidentTiles)
|
|
{
|
|
if(!DesiredTileSet.contains(Key))continue;
|
|
const uint8 NewMask=ComputeVisibleSkirtFaceMask(Key,DesiredTileSet);
|
|
if(NewMask==Resident.ActiveSkirtFaceMask)continue;
|
|
Resident.ActiveSkirtFaceMask=NewMask;
|
|
MarkBatchDirty(BatchForTile(Key));
|
|
}
|
|
}
|
|
|
|
static bool TileKeyLess(const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
if(A.Level!=B.Level)return A.Level<B.Level;
|
|
if(A.Coord.Z!=B.Coord.Z)return A.Coord.Z<B.Coord.Z;
|
|
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
|
|
return A.Coord.X<B.Coord.X;
|
|
}
|
|
|
|
void RemoveResidentTile(const FVoxelTileKey& Key)
|
|
{
|
|
auto Resident=ResidentTiles.find(Key);
|
|
if(Resident==ResidentTiles.end())return;
|
|
const FStandaloneBatchKey Batch=BatchForTile(Key);
|
|
auto Members=BatchMembers.find(Batch);
|
|
if(Members!=BatchMembers.end())
|
|
{
|
|
auto& Items=Members->second;
|
|
Items.erase(std::remove(Items.begin(),Items.end(),Key),Items.end());
|
|
if(Items.empty())BatchMembers.erase(Members);
|
|
}
|
|
MarkBatchDirty(Batch);
|
|
ResidentTiles.erase(Resident);
|
|
}
|
|
|
|
void RecenterStreaming()
|
|
{
|
|
if(!World||!Streamer)return;
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
|
if(bShapeWorld&&ShapeWorld)
|
|
{
|
|
const auto CoreTiles=ShapeWorld->desiredTiles(
|
|
{CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z});
|
|
NewDesired.Tiles.clear();NewDesired.Tiles.reserve(CoreTiles.size());
|
|
for(const auto& Key:CoreTiles)
|
|
NewDesired.Tiles.emplace_back(
|
|
FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
}
|
|
if(NewDesired.CenterChunk==BuildTargetChunk)return;
|
|
const FIntVector Previous=BuildTargetChunk;
|
|
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);
|
|
std::vector<FVoxelTileKey> Leavers;
|
|
for(const auto& Pair:ResidentTiles)if(!NewSet.contains(Pair.first))Leavers.push_back(Pair.first);
|
|
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;
|
|
NewCoreTiles.reserve(NewDesired.Tiles.size());
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
if(!ResidentTiles.contains(Key))
|
|
NewCoreTiles.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
|
|
std::string PageError;
|
|
size_t CompiledPageCount=0;
|
|
if(!ShapeWorld->compileTileRegion(NewCoreTiles,
|
|
static_cast<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,
|
|
PageError.c_str());
|
|
return;
|
|
}
|
|
PageCoveredTiles=static_cast<uint64>(CompiledPageCount);
|
|
std::vector<VoxelForgeCore::TileKey> CoreDesired;
|
|
CoreDesired.reserve(NewDesired.Tiles.size());
|
|
for(const FVoxelTileKey& Key:NewDesired.Tiles)
|
|
CoreDesired.push_back({{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level});
|
|
ShapeWorld->retainTileRegions(CoreDesired);
|
|
for(const FVoxelTileKey& Key:Leavers)DeferredLeaverTiles.insert(Key);
|
|
const double PageSeconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-CrossingStart).count();
|
|
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),
|
|
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
|
|
{
|
|
for(const FVoxelTileKey& Key:Leavers)RemoveResidentTile(Key);
|
|
}
|
|
DesiredTiles=NewDesired.Tiles;
|
|
DesiredTileSet=std::move(NewSet);
|
|
for(auto It=GpuFarTiles.begin();It!=GpuFarTiles.end();)
|
|
{
|
|
if(DesiredTileSet.contains(It->first)){++It;continue;}
|
|
GpuFarFreeOutputSlots.push_back(It->second.OutputSlot);
|
|
ReleaseGpuFarTile(It->second);
|
|
It=GpuFarTiles.erase(It);
|
|
}
|
|
ScheduleGpuFarTiles(DesiredTiles);
|
|
RefreshResidentSkirtMasks();
|
|
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
|
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)
|
|
if(DesiredTileSet.contains(Pair.first))ResidentKeys.push_back(Pair.first);
|
|
++CrossingIndex;
|
|
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesiredWithSet(
|
|
std::move(NewDesired),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.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,
|
|
Update.CenterChunk.X-Previous.X,Update.CenterChunk.Y-Previous.Y,Update.CenterChunk.Z-Previous.Z,
|
|
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,
|
|
bShapeWorld?Leavers.size():0,
|
|
Update.CancelledQueuedJobs,Update.FullResTiles);
|
|
std::fflush(stdout);
|
|
ReportCrossingWork();
|
|
}
|
|
|
|
bool RebuildBatch(const FStandaloneBatchKey& Key)
|
|
{
|
|
auto MembersIt=BatchMembers.find(Key);
|
|
auto Existing=StreamingBatches.find(Key);
|
|
if(MembersIt==BatchMembers.end()||MembersIt->second.empty())
|
|
{
|
|
if(Existing!=StreamingBatches.end())
|
|
{
|
|
TotalVertices-=Existing->second.VertexCount;TotalIndices-=Existing->second.IndexCount;
|
|
if(Existing->second.VertexBuffer)Existing->second.VertexBuffer->Release();
|
|
if(Existing->second.IndexBuffer)Existing->second.IndexBuffer->Release();
|
|
StreamingBatches.erase(Existing);
|
|
}
|
|
return true;
|
|
}
|
|
std::vector<FVoxelTileKey> Members=MembersIt->second;
|
|
std::sort(Members.begin(),Members.end(),TileKeyLess);
|
|
size_t VertexCount=0,IndexCount=0;
|
|
for(const FVoxelTileKey& Tile:Members)
|
|
{
|
|
const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue;
|
|
const FResidentTileMesh& Resident=It->second;
|
|
VertexCount+=Resident.Vertices.size();
|
|
if(Resident.TriangleSkirtFaceMasks.size()==Resident.Indices.size()/3)
|
|
{
|
|
for(const uint8 Faces:Resident.TriangleSkirtFaceMasks)
|
|
if(Faces==0||(Faces&Resident.ActiveSkirtFaceMask)!=0)IndexCount+=3;
|
|
}
|
|
else IndexCount+=Resident.Indices.size();
|
|
}
|
|
if(VertexCount==0||IndexCount==0)
|
|
{
|
|
if(Existing!=StreamingBatches.end())
|
|
{
|
|
TotalVertices-=Existing->second.VertexCount;TotalIndices-=Existing->second.IndexCount;
|
|
if(Existing->second.VertexBuffer)Existing->second.VertexBuffer->Release();
|
|
if(Existing->second.IndexBuffer)Existing->second.IndexBuffer->Release();
|
|
StreamingBatches.erase(Existing);
|
|
}
|
|
return true;
|
|
}
|
|
if(VertexCount>std::numeric_limits<uint32>::max()||IndexCount>std::numeric_limits<UINT>::max())
|
|
{std::fprintf(stderr,"LOD %d batch exceeded D3D11 limits.\n",Key.Level);return false;}
|
|
std::vector<FGpuVertex> Vertices;Vertices.reserve(VertexCount);
|
|
std::vector<uint32> Indices;Indices.reserve(IndexCount);
|
|
UINT DrawOnlyIndexStart=0,DrawOnlyIndexCount=0;
|
|
for(const FVoxelTileKey& Tile:Members)
|
|
{
|
|
const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue;
|
|
const size_t TileIndexStart=Indices.size();
|
|
const uint32 Base=static_cast<uint32>(Vertices.size());
|
|
const FResidentTileMesh& Resident=It->second;
|
|
for(const FStandaloneCpuVertex& V:Resident.Vertices)
|
|
Vertices.push_back({{V.Position[0],V.Position[1],V.Position[2]},{V.Normal[0],V.Normal[1],V.Normal[2]}});
|
|
const bool bHasFaceMasks=Resident.TriangleSkirtFaceMasks.size()==Resident.Indices.size()/3;
|
|
for(size_t Triangle=0;Triangle<Resident.Indices.size()/3;++Triangle)
|
|
{
|
|
if(bHasFaceMasks)
|
|
{
|
|
const uint8 Faces=Resident.TriangleSkirtFaceMasks[Triangle];
|
|
if(Faces!=0&&(Faces&Resident.ActiveSkirtFaceMask)==0)continue;
|
|
}
|
|
const size_t Start=Triangle*3;
|
|
Indices.push_back(Base+Resident.Indices[Start]);
|
|
Indices.push_back(Base+Resident.Indices[Start+1]);
|
|
Indices.push_back(Base+Resident.Indices[Start+2]);
|
|
}
|
|
if(bDrawOnlyTile&&IsDrawOnlyTile(Tile))
|
|
{
|
|
DrawOnlyIndexStart=static_cast<UINT>(TileIndexStart);
|
|
DrawOnlyIndexCount=static_cast<UINT>(Indices.size()-TileIndexStart);
|
|
}
|
|
}
|
|
FStreamingGpuBatch NewBatch;
|
|
const bool bVertex=MakeBuffer(&NewBatch.VertexBuffer,Vertices.data(),Vertices.size()*sizeof(FGpuVertex),D3D11_BIND_VERTEX_BUFFER);
|
|
const bool bIndex=bVertex&&MakeBuffer(&NewBatch.IndexBuffer,Indices.data(),Indices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER);
|
|
if(!bIndex)
|
|
{
|
|
if(NewBatch.VertexBuffer)NewBatch.VertexBuffer->Release();
|
|
std::fprintf(stderr,"D3D11 upload failed for LOD %d batch (%d,%d,%d).\n",Key.Level,Key.X,Key.Y,Key.Z);
|
|
return false;
|
|
}
|
|
NewBatch.IndexCount=static_cast<UINT>(Indices.size());NewBatch.VertexCount=Vertices.size();
|
|
NewBatch.DrawOnlyIndexStart=DrawOnlyIndexStart;
|
|
NewBatch.DrawOnlyIndexCount=DrawOnlyIndexCount;
|
|
if(Existing!=StreamingBatches.end())
|
|
{
|
|
TotalVertices-=Existing->second.VertexCount;TotalIndices-=Existing->second.IndexCount;
|
|
if(Existing->second.VertexBuffer)Existing->second.VertexBuffer->Release();
|
|
if(Existing->second.IndexBuffer)Existing->second.IndexBuffer->Release();
|
|
Existing->second=NewBatch;
|
|
}
|
|
else StreamingBatches.emplace(Key,NewBatch);
|
|
TotalVertices+=NewBatch.VertexCount;TotalIndices+=NewBatch.IndexCount;
|
|
return true;
|
|
}
|
|
|
|
void UploadDirtyBatches()
|
|
{
|
|
if(DirtyBatchSet.empty())return;
|
|
std::vector<FStandaloneBatchKey> Dirty(DirtyBatchSet.begin(),DirtyBatchSet.end());
|
|
const FIntVector Center=Streamer?Streamer->CenterChunk():BuildTargetChunk;
|
|
auto Distance=[&](const FStandaloneBatchKey& K)
|
|
{
|
|
const double Scale=static_cast<double>(1<<K.Level);
|
|
const double DX=(K.X*8.0+4.0)*Scale-Center.X;
|
|
const double DY=(K.Y*8.0+4.0)*Scale-Center.Y;
|
|
const double DZ=(K.Z+0.5)*Scale-Center.Z;
|
|
return DX*DX+DY*DY+DZ*DZ;
|
|
};
|
|
std::sort(Dirty.begin(),Dirty.end(),[&](const auto& A,const auto& B)
|
|
{const double DA=Distance(A),DB=Distance(B);return DA==DB?A.Level<B.Level:DA<DB;});
|
|
const bool bDrainColdStart=bGpuFarEnabled&&!bInitialClipmapReported
|
|
&&!bDisableGpuColdStartDrain;
|
|
const auto Start=std::chrono::steady_clock::now();int32 Uploaded=0;
|
|
for(const FStandaloneBatchKey& Key:Dirty)
|
|
{
|
|
if(!bDrainColdStart&&Uploaded>=MaxBatchUploadsPerFrame)break;
|
|
if(!bDrainColdStart&&Uploaded>0&&BatchUploadBudgetMilliseconds>0.0
|
|
&&std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-Start).count()>=BatchUploadBudgetMilliseconds)break;
|
|
if(!DirtyBatchSet.contains(Key))continue;
|
|
if(RebuildBatch(Key))
|
|
{
|
|
DirtyBatchSet.erase(Key);++Uploaded;
|
|
if(bGpuFarTimelineEnabled)
|
|
{
|
|
const auto UploadAt=std::chrono::steady_clock::now();
|
|
const bool bHasUploadBuffer=StreamingBatches.contains(Key);
|
|
std::printf("[CpuUploadBatchTimeline] event=uploaded batch=(lod%d,%d,%d,%d) at_ms=%.3f frame=%llu has_buffer=%s\n",
|
|
Key.Level,Key.X,Key.Y,Key.Z,ColdTimelineMilliseconds(UploadAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
bHasUploadBuffer?"yes":"no_empty_mesh");
|
|
const auto MembersIt=BatchMembers.find(Key);
|
|
if(MembersIt!=BatchMembers.end())for(const FVoxelTileKey& TileKey:MembersIt->second)
|
|
{
|
|
auto ResidentIt=ResidentTiles.find(TileKey);
|
|
if(ResidentIt==ResidentTiles.end()||ResidentIt->second.bColdTimelineResidentReported)continue;
|
|
FResidentTileMesh& Resident=ResidentIt->second;
|
|
Resident.bColdTimelineResidentReported=true;
|
|
std::printf("[CpuTileTimeline] event=uploaded key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu batch=(lod%d,%d,%d,%d) queued_ms=%.3f started_ms=%.3f finished_ms=%.3f method=%s\n",
|
|
TileKey.Coord.X,TileKey.Coord.Y,TileKey.Coord.Z,TileKey.Level,
|
|
ColdTimelineMilliseconds(UploadAt),static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
Key.Level,Key.X,Key.Y,Key.Z,ColdTimelineMilliseconds(Resident.CpuQueuedAt),
|
|
ColdTimelineMilliseconds(Resident.CpuStartedAt),ColdTimelineMilliseconds(Resident.CpuFinishedAt),
|
|
bHasUploadBuffer?"cpu_batch_vertex_index":"empty_surface_no_gpu_copy");
|
|
std::printf("[CpuTileTimeline] event=resident key=(%d,%d,%d,lod%d) at_ms=%.3f frame=%llu batch=(lod%d,%d,%d,%d)\n",
|
|
TileKey.Coord.X,TileKey.Coord.Y,TileKey.Coord.Z,TileKey.Level,
|
|
ColdTimelineMilliseconds(UploadAt),static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
Key.Level,Key.X,Key.Y,Key.Z);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IsFullDetailUploaded()
|
|
{
|
|
if(!Streamer||World->FullResClipLevels<=0)return false;
|
|
int32 DesiredFullRes=0,ReadyFullRes=0;
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
{
|
|
if(Key.Level>=World->FullResClipLevels)continue;
|
|
++DesiredFullRes;
|
|
if(IsRenderedTileReady(Key))++ReadyFullRes;
|
|
}
|
|
if(ReadyFullRes!=DesiredFullRes)return false;
|
|
for(const FStandaloneBatchKey& Key:DirtyBatchSet)if(Key.Level<World->FullResClipLevels)return false;
|
|
return true;
|
|
}
|
|
|
|
void ReportTileTimings()
|
|
{
|
|
for(int32 L=0;L<=World->MaxClipLevel;++L)
|
|
{
|
|
if(GeneratedByLevel[L]<static_cast<uint64>(DesiredByLevel[L])||DesiredByLevel[L]==0)continue;
|
|
std::vector<double>& Times=TileSecondsByLevel[L];if(Times.empty())continue;
|
|
std::sort(Times.begin(),Times.end());
|
|
const double Average=WorkerSecondsByLevel[L]/static_cast<double>(GeneratedByLevel[L]);
|
|
const double P50=Times[(Times.size()-1)*50/100];
|
|
const double P95=Times[(Times.size()-1)*95/100];
|
|
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;
|
|
Work.WallSeconds=Work.StartTime.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double>(std::chrono::steady_clock::now()-Work.StartTime).count():0.0;
|
|
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),
|
|
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,
|
|
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]));
|
|
}
|
|
}
|
|
|
|
void PumpGeneration()
|
|
{
|
|
if(bRebuildPending){StartGeneration();return;}
|
|
if(!Streamer)return;
|
|
std::vector<FStandaloneCpuTile> Ready=Streamer->TakeReady();
|
|
for(FStandaloneCpuTile& Tile:Ready)
|
|
{
|
|
const int32 Level=Tile.Key.Level;
|
|
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;
|
|
++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.TriangleSkirtFaceMasks=std::move(Tile.TriangleSkirtFaceMasks);
|
|
Resident.ActiveSkirtFaceMask=ComputeVisibleSkirtFaceMask(Tile.Key,DesiredTileSet);
|
|
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
|
|
Resident.CpuQueuedAt=Tile.QueuedAt;Resident.CpuStartedAt=Tile.StartedAt;
|
|
Resident.CpuFinishedAt=Tile.CompletedAt;
|
|
ResidentTiles.emplace(Tile.Key,std::move(Resident));
|
|
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
|
|
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
|
|
}
|
|
ReportCrossingWork();
|
|
UploadDirtyBatches();
|
|
if(!DeferredLeaverTiles.empty()&&DirtyBatchSet.empty())
|
|
{
|
|
bool bDesiredResident=true;
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(!IsRenderedTileReady(Key)){bDesiredResident=false;break;}
|
|
if(bDesiredResident)
|
|
{
|
|
std::vector<FVoxelTileKey> ReadyToDrop;
|
|
for(const FVoxelTileKey& Key:DeferredLeaverTiles)
|
|
{
|
|
if(Streamer->IsDesired(Key))continue;
|
|
if(ResidentTiles.contains(Key))ReadyToDrop.push_back(Key);
|
|
}
|
|
for(const FVoxelTileKey& Key:ReadyToDrop)RemoveResidentTile(Key);
|
|
DeferredLeaverTiles.clear();
|
|
}
|
|
}
|
|
if(bShapePipelineActive)TryCompleteShapePipeline();
|
|
if(FlightTargetCrossings>0&&bFlightFinished&&!bFlightClosePosted
|
|
&&DirtyBatchSet.empty()&&DeferredLeaverTiles.empty())
|
|
{
|
|
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)
|
|
{
|
|
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(IsRenderedTileReady(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);
|
|
}
|
|
}
|
|
if(!bShapePipelineActive&&!bFullDetailReported&&IsFullDetailUploaded())
|
|
{
|
|
bFullDetailReported=true;
|
|
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
|
|
const std::size_t DesiredNear=static_cast<std::size_t>(std::count_if(
|
|
DesiredTiles.begin(),DesiredTiles.end(),[&](const FVoxelTileKey& Key)
|
|
{return Key.Level<World->FullResClipLevels;}));
|
|
std::printf("[FullDetailReady] %.3f s after cold process start; rendered=%zu/%zu tiles through LOD%d.\n",
|
|
Elapsed,DesiredNear,DesiredNear,World->FullResClipLevels-1);
|
|
std::printf("[ColdTimeline] event=near_all_tiles_ready at_ms=%.3f waits_for=all_%zu_LOD0_1_gpu_or_cpu_tiles\n",
|
|
Elapsed*1000.0,DesiredNear);
|
|
ReportTileTimings();std::fflush(stdout);
|
|
StartFarShapeGeneration();
|
|
}
|
|
size_t DesiredResidentCount=0;
|
|
for(const FVoxelTileKey& Key:DesiredTiles)
|
|
if(IsRenderedTileReady(Key))++DesiredResidentCount;
|
|
if(!bInitialClipmapReported&&DesiredResidentCount==DesiredTiles.size()&&DirtyBatchSet.empty())
|
|
{
|
|
bInitialClipmapReported=true;
|
|
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-ColdProcessStart).count();
|
|
std::printf("[InitialClipmapReady] %.3f s after cold process start; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
|
|
Elapsed,DesiredResidentCount,static_cast<unsigned long long>(TotalVertices),
|
|
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
|
|
std::printf("[ColdTimeline] event=whole_clipmap_ready at_ms=%.3f waits_for=%zu_GPU_tiles_plus_CPU_fallback_and_uploads\n",
|
|
Elapsed*1000.0,GpuFarEligibleTiles.size());
|
|
std::printf("[PresentStartupSummary] nonblocking_startup_calls=%llu skipped_busy_calls=%llu gpu_warmup_held_calls=%llu policy=do_not_wait_until_initial_clipmap_ready\n",
|
|
static_cast<unsigned long long>(StartupNonBlockingPresentCalls),
|
|
static_cast<unsigned long long>(StartupNonBlockingPresentSkips),
|
|
static_cast<unsigned long long>(GpuWarmupHeldPresents));
|
|
if(bGpuFarEnabled)
|
|
{
|
|
SampleGpuFarMemory();
|
|
const double AverageGpuMs=GpuFarTilesSubmitted
|
|
?GpuFarGpuDispatchMs/static_cast<double>(GpuFarTilesSubmitted):0.0;
|
|
std::printf("[GpuFarTiming] submitted_tiles=%llu submission_batches=%llu max_tiles_per_batch=%u pooled_output_reserve_wall_ms=%.3f cpu_input_prepare_wall_ms=%.3f cpu_input_worker_sum_ms=%.3f cpu_upload_enqueue_sum_ms=%.3f gpu_dispatch_timestamp_total_ms=%.3f gpu_dispatch_mean_per_tile_ms=%.6f gpu_dispatch_max_tile_ms=%.6f gpu_submission_span_total_ms=%.3f gpu_submission_span_max_batch_ms=%.3f frame_budget_90hz_ms=11.111\n",
|
|
static_cast<unsigned long long>(GpuFarTilesSubmitted),
|
|
static_cast<unsigned long long>(GpuFarSubmissionBatches),GpuFarMaxTilesPerSubmissionBatch,
|
|
GpuFarPoolReserveMs,GpuFarPreparationWallMs,GpuFarPreparationWorkerMs,
|
|
GpuFarEnqueueWallSeconds*1000.0,
|
|
GpuFarGpuDispatchMs,AverageGpuMs,GpuFarGpuDispatchMaxMs,
|
|
GpuFarSubmissionSpanMs,GpuFarSubmissionSpanMaxMs);
|
|
std::printf("[GpuFarTimeline] pump_calls=%llu pump_first_ms=%.3f pump_last_ms=%.3f pump_gap_sum_ms=%.3f pump_gap_max_ms=%.3f pump_cpu_sum_ms=%.3f pump_cpu_max_ms=%.3f submit_first_ms=%.3f submit_last_ms=%.3f completion_polls=%llu completion_first_ms=%.3f completion_last_ms=%.3f queue_envelope_ms=%.3f gpu_busy_ms=%.3f estimated_idle_or_poll_ms=%.3f\n",
|
|
static_cast<unsigned long long>(GpuFarPumpCalls),GpuFarFirstPumpMs,GpuFarLastPumpMs,
|
|
GpuFarPumpGapMs,GpuFarPumpGapMaxMs,GpuFarPumpWallMs,GpuFarPumpMaxWallMs,
|
|
GpuFarFirstSubmitMs,GpuFarLastSubmitMs,
|
|
static_cast<unsigned long long>(GpuFarCompletionPolls),GpuFarFirstCompletionPollMs,
|
|
GpuFarLastCompletionPollMs,
|
|
GpuFarFirstSubmitMs>=0.0&&GpuFarLastCompletionPollMs>=0.0
|
|
?GpuFarLastCompletionPollMs-GpuFarFirstSubmitMs:0.0,
|
|
GpuFarSubmissionSpanMs,
|
|
GpuFarFirstSubmitMs>=0.0&&GpuFarLastCompletionPollMs>=0.0
|
|
?std::max(0.0,GpuFarLastCompletionPollMs-GpuFarFirstSubmitMs-GpuFarSubmissionSpanMs):0.0);
|
|
std::printf("[GpuFarMemory] dxgi_local_sampled_peak_mb=%.1f dxgi_local_budget_mb=%.1f\n",
|
|
GpuFarLocalMemoryPeakBytes/(1024.0*1024.0),
|
|
GpuFarLocalMemoryBudgetBytes/(1024.0*1024.0));
|
|
}
|
|
ReportTileTimings();std::fflush(stdout);
|
|
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
if(bInitialClipmapReported&&bGpuFarAuditEnabled&&GpuFarAudit&&!bGpuFarAuditReported)
|
|
{
|
|
GpuFarAudit->Finalize();GpuFarAudit->Print();
|
|
bGpuFarAuditReported=true;
|
|
std::printf("[GpuFarSurvivalAudit] mesh_source=GPU field_readback=required status=%s\n",
|
|
GpuFarAudit->Passing()?"PASS":"FAIL");
|
|
std::fflush(stdout);
|
|
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
bGenerationRunning=DesiredResidentCount!=DesiredTiles.size()||!DirtyBatchSet.empty()
|
|
||(bGpuFarEnabled&&!GpuFarQueue.empty());
|
|
}
|
|
|
|
void CaptureRenderedFrame()
|
|
{
|
|
const bool bReadyToCapture=bCaptureAfterClipmap?bInitialClipmapReported:IsFullDetailUploaded();
|
|
if(CapturePath.empty()||bAutoCaptureWritten||!bReadyToCapture)return;
|
|
bAutoCaptureWritten=SaveBackBufferPng(CapturePath);
|
|
if(bAutoCaptureWritten)
|
|
{
|
|
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
|
|
std::printf("Back-buffer capture saved: %s (%.3f s after generation start; full detail uploaded).\n",CapturePath.c_str(),Elapsed);
|
|
std::fflush(stdout);PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
else std::fprintf(stderr,"Back-buffer capture failed: %s\n",CapturePath.c_str());
|
|
}
|
|
|
|
void Capture(bool bOn)
|
|
{
|
|
bCaptured=bOn;
|
|
if(bOn){SetCapture(Window);ShowCursor(FALSE);GetCursorPos(&LastMouse);POINT C{Width/2,Height/2};ClientToScreen(Window,&C);SetCursorPos(C.x,C.y);}
|
|
else{ReleaseCapture();ShowCursor(TRUE);}
|
|
}
|
|
|
|
void UpdateTitle()
|
|
{
|
|
uint64 VisibleIndices=0;for(const auto& Pair:StreamingBatches)
|
|
if(bShowLevel[Pair.first.Level]
|
|
&&(CaptureOnlyLod<0||Pair.first.Level==CaptureOnlyLod))
|
|
VisibleIndices+=Pair.second.IndexCount;
|
|
std::ostringstream S;S<<"VoxelForge standalone | seed "<<Seed<<" | "<<(bShapeWorld?"shape-interval owner world":(bRoundTunnels?"round/winding research tunnels":"base world"))
|
|
<<(bRoundTunnels?(bRoundTunnelEnhanced?" (detailed walls)":" (legacy walls)"):"")
|
|
<<" | "<<VisibleIndices/3<<" tris | "<<FrameMs<<" ms | field "<<(bGpuFarEnabled?"GPU":"CPU")
|
|
<<" | "<<(bGenerationRunning?"generating":"ready")
|
|
<<" | pos "<<std::fixed<<std::setprecision(1)<<Position.X<<", "<<Position.Y<<", "<<Position.Z<<" m";
|
|
SetWindowTextA(Window,S.str().c_str());
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
const uint64 FrameOrdinal=ColdTimelineFrameOrdinal.fetch_add(1,std::memory_order_relaxed)+1;
|
|
const auto DrawStart=std::chrono::steady_clock::now();
|
|
const bool bTraceColdFrame=bGpuFarTimelineEnabled&&!bInitialClipmapReported;
|
|
if(bTraceColdFrame)
|
|
std::printf("[ColdFrame] stage=begin frame=%llu at_ms=%.3f inter_draw_gap_ms=%.3f\n",
|
|
static_cast<unsigned long long>(FrameOrdinal),ColdTimelineMilliseconds(DrawStart),
|
|
LastDrawEndAt.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double,std::milli>(DrawStart-LastDrawEndAt).count():0.0);
|
|
const double InterDrawGapMs=LastDrawEndAt.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double,std::milli>(DrawStart-LastDrawEndAt).count():0.0;
|
|
PumpGeneration();
|
|
const auto CpuPumpEnd=std::chrono::steady_clock::now();
|
|
const bool bDeferGpuUntilCpuFrame=bGpuFarEnabled&&bPresentCpuFrameBeforeGpu
|
|
&&!bCpuReadyFramePresentedBeforeGpu;
|
|
if(!bDeferGpuUntilCpuFrame)PumpGpuFarGeneration();
|
|
const auto GpuPumpEnd=std::chrono::steady_clock::now();
|
|
const auto Now=std::chrono::steady_clock::now();
|
|
if(FlightTargetCrossings>0&&bFullDetailReported&&!bFlightStarted)
|
|
{
|
|
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(FlightTargetCrossings>0&&bFlightStarted&&!bFlightFinished)
|
|
{
|
|
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));
|
|
const float Step=MoveSpeed*Dt*(Keys[VK_SHIFT]?4.0f:1.0f);
|
|
if(Keys['W'])Position=Add(Position,Mul(F,Step));if(Keys['S'])Position=Sub(Position,Mul(F,Step));
|
|
if(Keys['A'])Position=Sub(Position,Mul(R,Step));if(Keys['D'])Position=Add(Position,Mul(R,Step));
|
|
if(Keys[VK_SPACE]||Keys['E'])Position.Z+=Step;if(Keys[VK_CONTROL]||Keys['Q'])Position.Z-=Step;
|
|
if(Streamer&&NeedsRecenter(BuildTargetChunk))RecenterStreaming();
|
|
const float Clear[4]={0.055f,0.075f,0.105f,1.0f};
|
|
Context->OMSetRenderTargets(1,&RenderTarget,DepthView);Context->ClearRenderTargetView(RenderTarget,Clear);
|
|
Context->ClearDepthStencilView(DepthView,D3D11_CLEAR_DEPTH,1.0f,0);
|
|
D3D11_VIEWPORT Viewport{0,0,static_cast<float>(Width),static_cast<float>(Height),0,1};Context->RSSetViewports(1,&Viewport);
|
|
Context->RSSetState(bWireframe?WireRasterizer:SolidRasterizer);Context->IASetInputLayout(InputLayout);
|
|
Context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
FConstants C{};const M4 V=View(Position,F,R,U);const M4 P=Perspective(1.04719755f,static_cast<float>(Width)/std::max(1,Height),0.05f,5000.0f);
|
|
M4 Combined{};for(int I=0;I<4;++I)for(int J=0;J<4;++J)for(int K=0;K<4;++K)Combined.M[I][J]+=V.M[I][K]*P.M[K][J];
|
|
C.ViewProjection=Transpose(Combined);C.Camera[0]=Position.X;C.Camera[1]=Position.Y;C.Camera[2]=Position.Z;
|
|
C.Forward[0]=F.X;C.Forward[1]=F.Y;C.Forward[2]=F.Z;C.LightDirection[0]=-0.5f;C.LightDirection[1]=-0.72f;C.LightDirection[2]=-0.48f;
|
|
C.FogStart=260.0f;C.FogEnd=2600.0f;C.Ambient=0.36f;
|
|
C.Padding[0]=1.0f;
|
|
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);Context->VSSetConstantBuffers(0,1,&Constants);Context->PSSetConstantBuffers(0,1,&Constants);
|
|
Context->VSSetShader(VertexShader,nullptr,0);Context->PSSetShader(PixelShader,nullptr,0);
|
|
for(const auto& Pair:StreamingBatches)
|
|
{
|
|
const FStreamingGpuBatch& L=Pair.second;
|
|
if(!bShowLevel[Pair.first.Level]
|
|
||(CaptureOnlyLod>=0&&Pair.first.Level!=CaptureOnlyLod)
|
|
||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount
|
|
||(bDrawOnlyTile&&L.DrawOnlyIndexCount==0))continue;
|
|
const UINT Stride=sizeof(FGpuVertex),Offset=0;Context->IASetVertexBuffers(0,1,&L.VertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(L.IndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
if(bDrawOnlyTile)
|
|
Context->DrawIndexed(L.DrawOnlyIndexCount,L.DrawOnlyIndexStart,0);
|
|
else Context->DrawIndexed(L.IndexCount,0,0);
|
|
}
|
|
// Avoid issuing hundreds of tiny indirect draw calls while the
|
|
// initial far batches are still being built. Once the clipmap is
|
|
// ready the pooled GPU meshes draw directly as before.
|
|
if(bGpuFarEnabled&&ShapeWorld&&ShapeWorld->hasFarWorld())
|
|
{
|
|
C.Padding[0]=0.01f;
|
|
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);
|
|
Context->VSSetConstantBuffers(0,1,&Constants);Context->PSSetConstantBuffers(0,1,&Constants);
|
|
for(auto& Pair:GpuFarTiles)
|
|
{
|
|
FGpuFarTile& Tile=Pair.second;
|
|
if(!IsDrawOnlyTile(Pair.first)||!bShowLevel[Pair.first.Level]
|
|
||(CaptureOnlyLod>=0&&Pair.first.Level!=CaptureOnlyLod)
|
|
||!Tile.VertexBuffer||!Tile.IndexBuffer||!Tile.IndirectArgs)continue;
|
|
if(!bInitialClipmapReported&&Pair.first.Level>=World->FullResClipLevels)continue;
|
|
if(!IsRenderedTileReady(Pair.first))continue;
|
|
if(bAuditGpuSkirtDrawArgs&&Tile.VisibleSkirtFaceMask!=0&&!Tile.bSkirtDrawArgsAudited)
|
|
{
|
|
if(!GpuSkirtArgsStaging)
|
|
{
|
|
D3D11_BUFFER_DESC StagingDesc{};
|
|
StagingDesc.ByteWidth=11u*sizeof(uint32);
|
|
StagingDesc.Usage=D3D11_USAGE_STAGING;
|
|
StagingDesc.CPUAccessFlags=D3D11_CPU_ACCESS_READ;
|
|
const HRESULT CreateResult=Device->CreateBuffer(&StagingDesc,nullptr,
|
|
&GpuSkirtArgsStaging);
|
|
if(FAILED(CreateResult))
|
|
std::fprintf(stderr,"[GpuSkirtDrawAuditError] create_staging_hr=0x%08lx\n",
|
|
static_cast<unsigned long>(CreateResult));
|
|
}
|
|
if(GpuSkirtArgsStaging)
|
|
{
|
|
const D3D11_BOX ArgsSource{Tile.IndirectArgsByteOffset,0,0,
|
|
Tile.IndirectArgsByteOffset+11u*sizeof(uint32),1,1};
|
|
Context->CopySubresourceRegion(GpuSkirtArgsStaging,0,0,0,0,
|
|
Tile.IndirectArgs,0,&ArgsSource);
|
|
D3D11_MAPPED_SUBRESOURCE ArgsMapped{};
|
|
const HRESULT MapResult=Context->Map(GpuSkirtArgsStaging,0,
|
|
D3D11_MAP_READ,0,&ArgsMapped);
|
|
if(SUCCEEDED(MapResult))
|
|
{
|
|
const auto* Args=static_cast<const uint32*>(ArgsMapped.pData);
|
|
const uint32 ExpectedStartIndex=(Tile.IndirectArgsByteOffset
|
|
/(11u*sizeof(uint32)))*32768u;
|
|
++GpuSkirtDrawAuditTiles;
|
|
GpuSkirtDrawAuditSumIndices+=Args[5];
|
|
GpuSkirtDrawAuditMaxIndices=std::max<uint64>(GpuSkirtDrawAuditMaxIndices,Args[5]);
|
|
if(Args[5]==0)++GpuSkirtDrawAuditZeroCounts;
|
|
if(Args[10]!=0)++GpuSkirtDrawAuditOverflow;
|
|
if(Args[7]!=ExpectedStartIndex)++GpuSkirtDrawAuditBadStartIndex;
|
|
std::printf("[GpuSkirtDrawArgs] key=(%d,%d,%d,lod%d) face_mask=0x%02x output_slot=%u surface_indices=%u skirt_panel_indices=%u instances=%u connector_start_index=%u expected_start_index=%u overflow=%u\n",
|
|
Pair.first.Coord.X,Pair.first.Coord.Y,Pair.first.Coord.Z,Pair.first.Level,
|
|
static_cast<unsigned>(Tile.VisibleSkirtFaceMask),Tile.OutputSlot,
|
|
Args[0],Args[5],Args[6],Args[7],ExpectedStartIndex,Args[10]);
|
|
Context->Unmap(GpuSkirtArgsStaging,0);
|
|
}
|
|
else std::fprintf(stderr,"[GpuSkirtDrawAuditError] map_staging_hr=0x%08lx\n",
|
|
static_cast<unsigned long>(MapResult));
|
|
}
|
|
Tile.bSkirtDrawArgsAudited=true;
|
|
}
|
|
const UINT Stride=32,Offset=0;
|
|
Context->IASetVertexBuffers(0,1,&Tile.VertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(Tile.IndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
Context->DrawIndexedInstancedIndirect(Tile.IndirectArgs,Tile.IndirectArgsByteOffset);
|
|
if(Tile.ConnectorVertexBuffer&&Tile.ConnectorIndexBuffer)
|
|
{
|
|
Context->IASetVertexBuffers(0,1,&Tile.ConnectorVertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(Tile.ConnectorIndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
Context->DrawIndexedInstancedIndirect(Tile.IndirectArgs,
|
|
Tile.IndirectArgsByteOffset+5u*sizeof(uint32));
|
|
}
|
|
}
|
|
}
|
|
if(bAuditGpuSkirtDrawArgs&&bInitialClipmapReported&&!bGpuSkirtDrawAuditReported)
|
|
{
|
|
std::printf("[GpuSkirtDrawArgsSummary] tiles=%llu zero_index_counts=%llu overflow=%llu bad_slot_start_indices=%llu total_panel_plus_skirt_indices=%llu max_panel_plus_skirt_indices=%llu status=%s\n",
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditTiles),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditZeroCounts),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditOverflow),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditBadStartIndex),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditSumIndices),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditMaxIndices),
|
|
(GpuSkirtDrawAuditTiles>0&&GpuSkirtDrawAuditOverflow==0
|
|
&&GpuSkirtDrawAuditBadStartIndex==0)?"PASS":"FAIL");
|
|
bGpuSkirtDrawAuditReported=true;
|
|
std::fflush(stdout);
|
|
}
|
|
CaptureRenderedFrame();
|
|
const bool bHoldFrontBufferForGpuWarmup=bHoldCpuFrameDuringGpuWarmup
|
|
&&bCpuReadyFramePresentedBeforeGpu&&bGpuFarEnabled
|
|
&&GpuFarTilesSubmitted>0&&!bInitialClipmapReported;
|
|
if(bHoldFrontBufferForGpuWarmup)
|
|
{
|
|
if(!bGpuWarmupHoldReported)
|
|
{
|
|
bGpuWarmupHoldReported=true;
|
|
std::printf("[GpuWarmupPresentHold] begin_ms=%.3f policy=retain_last_CPU_ready_front_buffer_until_GPU_clipmap_ready\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
std::fflush(stdout);
|
|
}
|
|
++GpuWarmupHeldPresents;
|
|
// Keep pumping CPU and GPU work at the display cadence without
|
|
// calling the driver present path that stalls on its first GPU frame.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(
|
|
bGpuFarEnabled&&!bDisableGpuColdStartDrain?1:11));
|
|
LastDrawEndAt=std::chrono::steady_clock::now();
|
|
if(bTraceColdFrame)
|
|
std::printf("[ColdFrame] stage=end frame=%llu at_ms=%.3f cpu_pump_ms=%.3f gpu_pump_ms=%.3f held=1 present_ms=0.000\n",
|
|
static_cast<unsigned long long>(FrameOrdinal),ColdTimelineMilliseconds(LastDrawEndAt),
|
|
std::chrono::duration<double,std::milli>(CpuPumpEnd-DrawStart).count(),
|
|
std::chrono::duration<double,std::milli>(GpuPumpEnd-CpuPumpEnd).count());
|
|
return;
|
|
}
|
|
const auto BeforePresent=std::chrono::steady_clock::now();
|
|
const bool bNonBlockingStartupPresent=bGpuFarEnabled&&!bInitialClipmapReported
|
|
&&!bForceBlockingStartupPresent;
|
|
const UINT PresentSyncInterval=(bDisableVSync||bNonBlockingStartupPresent)?0u:1u;
|
|
const UINT PresentFlags=bNonBlockingStartupPresent?DXGI_PRESENT_DO_NOT_WAIT:0u;
|
|
const auto PresentCallStart=std::chrono::steady_clock::now();
|
|
const HRESULT PresentResult=SwapChain->Present(PresentSyncInterval,PresentFlags);
|
|
const auto PresentCallEnd=std::chrono::steady_clock::now();
|
|
if(GpuFarTilesSubmitted>0&&!bGpuFirstBackedPresentReported)
|
|
{
|
|
bGpuFirstBackedPresentReported=true;
|
|
std::printf("[GpuFirstBackedPresent] at_ms=%.3f call_ms=%.3f sync=%u flags=0x%08x hr=0x%08lx submitted_tiles=%llu batches=%llu\n",
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-PresentCallStart).count(),
|
|
PresentSyncInterval,PresentFlags,static_cast<unsigned long>(PresentResult),
|
|
static_cast<unsigned long long>(GpuFarTilesSubmitted),
|
|
static_cast<unsigned long long>(GpuFarSubmissionBatches));
|
|
std::fflush(stdout);
|
|
}
|
|
if(bInitialClipmapReported&&!bWholeViewPresented)
|
|
{
|
|
bWholeViewPresented=true;
|
|
std::printf("[WholeViewPresented] at_ms=%.3f present_ms=%.3f sync=%u hr=0x%08lx\n",
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-PresentCallStart).count(),
|
|
PresentSyncInterval,static_cast<unsigned long>(PresentResult));
|
|
std::fflush(stdout);
|
|
}
|
|
if(bGpuWarmupHoldReported&&!bGpuWarmupReleaseReported)
|
|
{
|
|
bGpuWarmupReleaseReported=true;
|
|
std::printf("[GpuWarmupPresentRelease] at_ms=%.3f first_post_warmup_present_ms=%.3f sync=%u hr=0x%08lx\n",
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-PresentCallStart).count(),
|
|
PresentSyncInterval,static_cast<unsigned long>(PresentResult));
|
|
std::fflush(stdout);
|
|
}
|
|
const uint64 PresentOrdinal=++PresentCallCount;
|
|
if(bNonBlockingStartupPresent)
|
|
{
|
|
++StartupNonBlockingPresentCalls;
|
|
if(PresentResult==DXGI_ERROR_WAS_STILL_DRAWING)++StartupNonBlockingPresentSkips;
|
|
else if(FAILED(PresentResult))
|
|
std::fprintf(stderr,"[PresentStartupError] hr=0x%08lx\n",
|
|
static_cast<unsigned long>(PresentResult));
|
|
}
|
|
if(PresentOrdinal<=3)
|
|
std::printf("[PresentProfile] ordinal=%llu at_ms=%.3f sync=%u flags=0x%08x mode=%s hr=0x%08lx call_ms=%.3f gpu_tiles_submitted=%llu gpu_batches=%llu\n",
|
|
static_cast<unsigned long long>(PresentOrdinal),
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-ColdProcessStart).count(),
|
|
PresentSyncInterval,PresentFlags,bNonBlockingStartupPresent?"startup-do-not-wait":"normal",
|
|
static_cast<unsigned long>(PresentResult),
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-PresentCallStart).count(),
|
|
static_cast<unsigned long long>(GpuFarTilesSubmitted),
|
|
static_cast<unsigned long long>(GpuFarSubmissionBatches));
|
|
if(bDeferGpuUntilCpuFrame&&!StreamingBatches.empty())
|
|
{
|
|
bCpuReadyFramePresentedBeforeGpu=true;
|
|
std::printf("[CpuReadyFrameBeforeGpu] at_ms=%.3f uploaded_cpu_batches=%zu next_action=submit_gpu_far\n",
|
|
std::chrono::duration<double,std::milli>(PresentCallEnd-ColdProcessStart).count(),
|
|
StreamingBatches.size());
|
|
std::fflush(stdout);
|
|
}
|
|
UpdateTitle();
|
|
const auto DrawEnd=std::chrono::steady_clock::now();
|
|
const double CpuPumpMs=std::chrono::duration<double,std::milli>(CpuPumpEnd-DrawStart).count();
|
|
const double GpuPumpMs=std::chrono::duration<double,std::milli>(GpuPumpEnd-CpuPumpEnd).count();
|
|
const double PresentCallMs=std::chrono::duration<double,std::milli>(PresentCallEnd-PresentCallStart).count();
|
|
const double TitleMs=std::chrono::duration<double,std::milli>(DrawEnd-PresentCallEnd).count();
|
|
const double RenderPresentMs=PresentCallMs+TitleMs;
|
|
if(InterDrawGapMs>50.0||CpuPumpMs>50.0||GpuPumpMs>50.0||RenderPresentMs>50.0)
|
|
std::printf("[LongViewerFrame] at_ms=%.3f inter_draw_gap_ms=%.3f cpu_pump_ms=%.3f gpu_pump_ms=%.3f pre_present_ms=%.3f present_sync=%u present_call_ms=%.3f title_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(DrawStart-ColdProcessStart).count(),
|
|
InterDrawGapMs,CpuPumpMs,GpuPumpMs,
|
|
std::chrono::duration<double,std::milli>(BeforePresent-GpuPumpEnd).count(),
|
|
PresentSyncInterval,PresentCallMs,TitleMs);
|
|
if(bTraceColdFrame)
|
|
std::printf("[ColdFrame] stage=end frame=%llu at_ms=%.3f cpu_pump_ms=%.3f gpu_pump_ms=%.3f pre_present_ms=%.3f present_ms=%.3f present_sync=%u present_hr=0x%08lx held=0\n",
|
|
static_cast<unsigned long long>(FrameOrdinal),ColdTimelineMilliseconds(DrawEnd),
|
|
CpuPumpMs,GpuPumpMs,
|
|
std::chrono::duration<double,std::milli>(BeforePresent-GpuPumpEnd).count(),
|
|
PresentCallMs,PresentSyncInterval,static_cast<unsigned long>(PresentResult));
|
|
LastDrawEndAt=DrawEnd;
|
|
}
|
|
};
|
|
|
|
FApp* GApp=nullptr;
|
|
LRESULT CALLBACK WindowProc(HWND H,UINT Message,WPARAM W,LPARAM L)
|
|
{
|
|
if(!GApp)return DefWindowProc(H,Message,W,L);
|
|
switch(Message)
|
|
{
|
|
case WM_SIZE:if(W!=SIZE_MINIMIZED)GApp->Resize(LOWORD(L),HIWORD(L));return 0;
|
|
case WM_KEYDOWN:
|
|
{
|
|
const bool bWasDown=(L&(1ll<<30))!=0;if(W<256)GApp->Keys[W]=true;
|
|
if(W==VK_ESCAPE){if(GApp->bCaptured)GApp->Capture(false);else DestroyWindow(H);}
|
|
if(!bWasDown&&W=='F')GApp->bWireframe=!GApp->bWireframe;
|
|
if(!bWasDown&&W=='T'){GApp->bRoundTunnels=!GApp->bRoundTunnels;GApp->bRebuildPending=true;}
|
|
if(!bWasDown&&W=='V'){GApp->bShapeWorld=!GApp->bShapeWorld;GApp->bRebuildPending=true;}
|
|
if(!bWasDown&&W=='G'&&GApp->bRoundTunnels)
|
|
{
|
|
GApp->bRoundTunnelEnhanced=!GApp->bRoundTunnelEnhanced;
|
|
GApp->bRebuildPending=true;
|
|
std::printf("Round-tunnel comparison: %s wall field.\n",
|
|
GApp->bRoundTunnelEnhanced?"detailed/smoothed":"legacy smooth tube");
|
|
}
|
|
if(!bWasDown&&W=='H')GApp->ToggleFarRenderer();
|
|
if(!bWasDown&&(W=='N'||W=='P'))GApp->RequestSeed(W=='N'?1:-1);
|
|
if(!bWasDown&&W>='1'&&W<='8'){const int32 I=static_cast<int32>(W-'1');GApp->bShowLevel[I]=!GApp->bShowLevel[I];}
|
|
if(!bWasDown&&W=='9')GApp->bShowLevel[8]=!GApp->bShowLevel[8];
|
|
if(!bWasDown&&W=='0')for(bool& B:GApp->bShowLevel)B=!B;
|
|
return 0;
|
|
}
|
|
case WM_KEYUP:if(W<256)GApp->Keys[W]=false;return 0;
|
|
case WM_RBUTTONDOWN:GApp->Capture(true);return 0;
|
|
case WM_RBUTTONUP:GApp->Capture(false);return 0;
|
|
case WM_MOUSEMOVE:
|
|
if(GApp->bCaptured){POINT C{GApp->Width/2,GApp->Height/2};ClientToScreen(H,&C);POINT P{GET_X_LPARAM(L),GET_Y_LPARAM(L)},Screen=P;ClientToScreen(H,&Screen);const int32 DX=Screen.x-C.x,DY=Screen.y-C.y;if(DX||DY){GApp->Yaw+=DX*0.0035f;GApp->Pitch=std::clamp(GApp->Pitch-DY*0.0035f,-1.55f,1.55f);SetCursorPos(C.x,C.y);}}return 0;
|
|
case WM_MOUSEWHEEL:GApp->MoveSpeed=std::clamp(GApp->MoveSpeed*(GET_WHEEL_DELTA_WPARAM(W)>0?1.25f:0.8f),0.25f,1000.0f);return 0;
|
|
case WM_DESTROY:if(GApp->Streamer)GApp->Streamer->Stop();PostQuitMessage(0);return 0;
|
|
}
|
|
return DefWindowProc(H,Message,W,L);
|
|
}
|
|
|
|
std::filesystem::path DefaultDataPath()
|
|
{
|
|
wchar_t Module[MAX_PATH]{};GetModuleFileNameW(nullptr,Module,MAX_PATH);
|
|
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,
|
|
VoxelForgeCore::World* CoreWorld)
|
|
{
|
|
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)));
|
|
if(CoreWorld)
|
|
{
|
|
const auto CoreTiles=CoreWorld->desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z});
|
|
for(const auto& Key:CoreTiles)
|
|
if(Key.lod==Level&&static_cast<int32>(Tiles.size())<Count)
|
|
Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
}
|
|
else
|
|
{
|
|
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 generator=%s command='%s'\n",
|
|
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,
|
|
CoreWorld&&!bCompareEvaluators?"VoxelForgeCore":"compatibility-diagnostic",
|
|
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;
|
|
uint64 TileVertices=0,TileIndices=0;
|
|
if(CoreWorld&&!bCompareEvaluators)
|
|
{
|
|
VoxelForgeCore::TileGenerationOptions Options;
|
|
Options.bandMinZVoxel=BandMin;Options.bandMaxZVoxel=BandMax;
|
|
Options.captureDensitySamples=bDensityHash;
|
|
std::string CoreError;
|
|
auto Mesh=CoreWorld->generateTile(
|
|
{{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level},Options,CoreError);
|
|
if(!Mesh)
|
|
{
|
|
std::fprintf(stderr,"[VoxelForgeCore] benchmark tile generation failed: %s\n",CoreError.c_str());
|
|
return 8;
|
|
}
|
|
TileSamples=static_cast<int64>(Mesh->densitySampleCount);
|
|
TileVertices=Mesh->vertices.size();TileIndices=Mesh->indices.size();
|
|
if(bDensityHash)DebugDensitySamples=std::move(Mesh->debugDensitySamples);
|
|
}
|
|
else
|
|
{
|
|
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
BandMin,BandMax,&TileSamples,nullptr,nullptr,
|
|
bDensityHash?&DebugDensitySamples:nullptr);
|
|
TileVertices=static_cast<uint64>(Mesh.Vertices.Num());
|
|
TileIndices=static_cast<uint64>(Mesh.Triangles.Num());
|
|
}
|
|
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
|
|
Vertices+=TileVertices;
|
|
Indices+=TileIndices;
|
|
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;
|
|
}
|
|
|
|
struct FComparePoint
|
|
{
|
|
double X=0.0,Y=0.0,Z=0.0;
|
|
};
|
|
|
|
struct FCompareBucketHash
|
|
{
|
|
size_t operator()(const FIntVector& K)const noexcept
|
|
{
|
|
uint64 H=static_cast<uint32>(K.X)*0x9E3779B185EBCA87ull;
|
|
H^=static_cast<uint64>(static_cast<uint32>(K.Y))*0xC2B2AE3D27D4EB4Full;
|
|
H^=static_cast<uint64>(static_cast<uint32>(K.Z))*0x165667B19E3779F9ull;
|
|
return static_cast<size_t>(H^(H>>32));
|
|
}
|
|
};
|
|
|
|
struct FDirectedSurfaceDistance
|
|
{
|
|
double Sum=0.0,Maximum=0.0;
|
|
uint64 Count=0,Unmatched=0;
|
|
};
|
|
|
|
constexpr int32 OwnerBandGrid=64;
|
|
constexpr double OwnerBandSpacing=0.75;
|
|
constexpr std::array<double,4> OwnerBandMinWavelength={2.0,4.0,8.0,16.0};
|
|
constexpr std::array<double,4> OwnerBandMaxWavelength={4.0,8.0,16.0,32.0};
|
|
|
|
void FFT64(std::array<std::complex<double>,OwnerBandGrid>& Values)
|
|
{
|
|
for(int32 I=1,J=0;I<OwnerBandGrid;++I)
|
|
{
|
|
int32 Bit=OwnerBandGrid>>1;
|
|
for(;J&Bit;Bit>>=1)J^=Bit;
|
|
J^=Bit;
|
|
if(I<J)std::swap(Values[I],Values[J]);
|
|
}
|
|
for(int32 Length=2;Length<=OwnerBandGrid;Length<<=1)
|
|
{
|
|
const double Angle=-6.2831853071795864769/static_cast<double>(Length);
|
|
const std::complex<double> Step(std::cos(Angle),std::sin(Angle));
|
|
for(int32 Base=0;Base<OwnerBandGrid;Base+=Length)
|
|
{
|
|
std::complex<double> W(1.0,0.0);
|
|
for(int32 I=0;I<Length/2;++I)
|
|
{
|
|
const std::complex<double> Even=Values[Base+I];
|
|
const std::complex<double> Odd=Values[Base+I+Length/2]*W;
|
|
Values[Base+I]=Even+Odd;Values[Base+I+Length/2]=Even-Odd;W*=Step;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool OwnerSurfaceBandRms(const std::array<double,OwnerBandGrid*OwnerBandGrid>& Heights,
|
|
const std::array<uint8,OwnerBandGrid*OwnerBandGrid>& Valid,std::array<double,4>& OutRms)
|
|
{
|
|
size_t ValidCount=0;
|
|
double Sums[3][4]{};
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)for(int32 X=0;X<OwnerBandGrid;++X)
|
|
{
|
|
const size_t Index=static_cast<size_t>(Y)*OwnerBandGrid+X;
|
|
if(!Valid[Index])continue;
|
|
++ValidCount;
|
|
const double U=(X-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const double V=(Y-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const double Row[3]={1.0,U,V};
|
|
for(int32 I=0;I<3;++I)
|
|
{
|
|
for(int32 J=0;J<3;++J)Sums[I][J]+=Row[I]*Row[J];
|
|
Sums[I][3]+=Row[I]*Heights[Index];
|
|
}
|
|
}
|
|
if(ValidCount<static_cast<size_t>(OwnerBandGrid*OwnerBandGrid*9/10))return false;
|
|
for(int32 Column=0;Column<3;++Column)
|
|
{
|
|
int32 Pivot=Column;
|
|
for(int32 Row=Column+1;Row<3;++Row)
|
|
if(std::abs(Sums[Row][Column])>std::abs(Sums[Pivot][Column]))Pivot=Row;
|
|
if(std::abs(Sums[Pivot][Column])<1.0e-10)return false;
|
|
if(Pivot!=Column)for(int32 J=Column;J<4;++J)std::swap(Sums[Pivot][J],Sums[Column][J]);
|
|
const double Scale=Sums[Column][Column];
|
|
for(int32 J=Column;J<4;++J)Sums[Column][J]/=Scale;
|
|
for(int32 Row=0;Row<3;++Row)if(Row!=Column)
|
|
{
|
|
const double Factor=Sums[Row][Column];
|
|
for(int32 J=Column;J<4;++J)Sums[Row][J]-=Factor*Sums[Column][J];
|
|
}
|
|
}
|
|
const double Plane[3]={Sums[0][3],Sums[1][3],Sums[2][3]};
|
|
std::array<std::array<std::complex<double>,OwnerBandGrid>,OwnerBandGrid> Spectrum{};
|
|
double WindowSquareSum=0.0;
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)
|
|
{
|
|
const double WY=0.5-0.5*std::cos(6.2831853071795864769*Y/(OwnerBandGrid-1));
|
|
for(int32 X=0;X<OwnerBandGrid;++X)
|
|
{
|
|
const size_t Index=static_cast<size_t>(Y)*OwnerBandGrid+X;
|
|
const double WX=0.5-0.5*std::cos(6.2831853071795864769*X/(OwnerBandGrid-1));
|
|
const double U=(X-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const double V=(Y-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const double Residual=Valid[Index]?Heights[Index]-(Plane[0]+Plane[1]*U+Plane[2]*V):0.0;
|
|
Spectrum[Y][X]=Residual*(WX*WY);
|
|
WindowSquareSum+=WX*WX*WY*WY;
|
|
}
|
|
FFT64(Spectrum[Y]);
|
|
}
|
|
for(int32 X=0;X<OwnerBandGrid;++X)
|
|
{
|
|
std::array<std::complex<double>,OwnerBandGrid> Column{};
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)Column[Y]=Spectrum[Y][X];
|
|
FFT64(Column);
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)Spectrum[Y][X]=Column[Y];
|
|
}
|
|
std::array<double,4> Power{};
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)for(int32 X=0;X<OwnerBandGrid;++X)
|
|
{
|
|
const double FX=std::min(X,OwnerBandGrid-X)
|
|
/(static_cast<double>(OwnerBandGrid)*OwnerBandSpacing);
|
|
const double FY=std::min(Y,OwnerBandGrid-Y)
|
|
/(static_cast<double>(OwnerBandGrid)*OwnerBandSpacing);
|
|
const double Frequency=std::sqrt(FX*FX+FY*FY);
|
|
if(Frequency<=0.0)continue;
|
|
const double Wavelength=1.0/Frequency;
|
|
for(size_t Band=0;Band<Power.size();++Band)
|
|
if(Wavelength>=OwnerBandMinWavelength[Band]&&Wavelength<OwnerBandMaxWavelength[Band])
|
|
Power[Band]+=std::norm(Spectrum[Y][X]);
|
|
}
|
|
const double ParsevalScale=static_cast<double>(OwnerBandGrid*OwnerBandGrid)*WindowSquareSum;
|
|
for(size_t Band=0;Band<OutRms.size();++Band)
|
|
OutRms[Band]=std::sqrt(std::max(0.0,Power[Band]/ParsevalScale));
|
|
return true;
|
|
}
|
|
|
|
template<class TDensity>
|
|
bool SampleOwnerSurfaceHeightPatch(V3 Origin,V3 Normal,V3 TangentU,V3 TangentV,
|
|
TDensity&& Density,std::array<double,OwnerBandGrid*OwnerBandGrid>& OutHeights,
|
|
std::array<uint8,OwnerBandGrid*OwnerBandGrid>& OutValid)
|
|
{
|
|
size_t ValidCount=0;
|
|
for(int32 Y=0;Y<OwnerBandGrid;++Y)for(int32 X=0;X<OwnerBandGrid;++X)
|
|
{
|
|
const double DU=(X-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const double DV=(Y-(OwnerBandGrid-1)*0.5)*OwnerBandSpacing;
|
|
const V3 Base=Add(Origin,Add(Mul(TangentU,static_cast<float>(DU)),Mul(TangentV,static_cast<float>(DV))));
|
|
double T=0.0;bool bConverged=false;
|
|
for(int32 Iteration=0;Iteration<7;++Iteration)
|
|
{
|
|
const V3 P=Add(Base,Mul(Normal,static_cast<float>(T)));
|
|
const double D=Density(P);
|
|
if(!std::isfinite(D)||std::abs(T)>16.0)break;
|
|
// Core density keeps a 0.05-voxel near-zero sign margin. Accepting a
|
|
// tighter root tolerance makes every otherwise-good Core patch fail.
|
|
if(std::abs(D)<0.06){bConverged=true;break;}
|
|
const double DP=Density(Add(Base,Mul(Normal,static_cast<float>(T+0.5))));
|
|
const double DM=Density(Add(Base,Mul(Normal,static_cast<float>(T-0.5))));
|
|
const double Gradient=DP-DM;
|
|
if(!std::isfinite(Gradient)||std::abs(Gradient)<0.25)break;
|
|
T-=std::clamp(D/Gradient,-4.0,4.0);
|
|
}
|
|
const size_t Index=static_cast<size_t>(Y)*OwnerBandGrid+X;
|
|
OutHeights[Index]=T;OutValid[Index]=bConverged?1:0;
|
|
ValidCount+=bConverged?1u:0u;
|
|
}
|
|
return ValidCount>=static_cast<size_t>(OwnerBandGrid*OwnerBandGrid*9/10);
|
|
}
|
|
|
|
FDirectedSurfaceDistance DirectedSurfaceDistance(
|
|
const std::vector<FComparePoint>& From,const std::vector<FComparePoint>& To,
|
|
double BucketWidth=1.0,int32 SearchCells=3)
|
|
{
|
|
FDirectedSurfaceDistance Result;
|
|
if(From.empty()||To.empty()){Result.Unmatched=From.size();return Result;}
|
|
std::unordered_map<FIntVector,std::vector<const FComparePoint*>,FCompareBucketHash> Buckets;
|
|
Buckets.reserve(To.size());
|
|
const auto BucketOf=[=](const FComparePoint& P)
|
|
{
|
|
return FIntVector(static_cast<int32>(std::floor(P.X/BucketWidth)),
|
|
static_cast<int32>(std::floor(P.Y/BucketWidth)),
|
|
static_cast<int32>(std::floor(P.Z/BucketWidth)));
|
|
};
|
|
for(const FComparePoint& P:To)Buckets[BucketOf(P)].push_back(&P);
|
|
for(const FComparePoint& P:From)
|
|
{
|
|
const FIntVector Center=BucketOf(P);
|
|
double BestSquared=std::numeric_limits<double>::infinity();
|
|
for(int32 Z=-SearchCells;Z<=SearchCells;++Z)
|
|
for(int32 Y=-SearchCells;Y<=SearchCells;++Y)
|
|
for(int32 X=-SearchCells;X<=SearchCells;++X)
|
|
{
|
|
const auto It=Buckets.find(Center+FIntVector(X,Y,Z));
|
|
if(It==Buckets.end())continue;
|
|
for(const FComparePoint* Candidate:It->second)
|
|
{
|
|
const double DX=P.X-Candidate->X,DY=P.Y-Candidate->Y,DZ=P.Z-Candidate->Z;
|
|
BestSquared=std::min(BestSquared,DX*DX+DY*DY+DZ*DZ);
|
|
}
|
|
}
|
|
if(std::isfinite(BestSquared))
|
|
{
|
|
const double Distance=std::sqrt(BestSquared);
|
|
Result.Sum+=Distance;Result.Maximum=std::max(Result.Maximum,Distance);++Result.Count;
|
|
}
|
|
else ++Result.Unmatched;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
int RunOwnerShapeComparison(FStandaloneOwnerWorld& World,VoxelForgeCore::World& Shape,
|
|
V3 CameraPosition,float CameraYaw,float CameraPitch,
|
|
int32 Workers,int32 LevelFilter,int32 TileLimit,
|
|
std::string_view AreaLabel,bool bCheckOwnerReliefBands)
|
|
{
|
|
if(!Shape.isCompiled())
|
|
{std::fprintf(stderr,"Shape comparison requires a compiled core world.\n");return 11;}
|
|
if(LevelFilter>=World.FullResClipLevels&&!Shape.hasFarWorld())Shape.buildFarWorld();
|
|
const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0);
|
|
Shape.auditViewSurface({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z},CameraYaw,CameraPitch);
|
|
{
|
|
const int32 Offsets[][3]={{0,0,0},{4,0,0},{-4,0,0},{0,4,0},{0,-4,0},{0,0,4},{0,0,-4}};
|
|
for(const auto& Offset:Offsets)
|
|
{
|
|
const float X=static_cast<float>(CameraVoxels.X+Offset[0]);
|
|
const float Y=static_cast<float>(CameraVoxels.Y+Offset[1]);
|
|
const float Z=static_cast<float>(CameraVoxels.Z+Offset[2]);
|
|
float OwnerDensity=0.0f;
|
|
OwnerDensity=World.Generator->GetDensityAt(X,Y,Z);
|
|
const float ShapeDensity=Shape.densityAt({X,Y,Z});
|
|
std::printf("[ShapeProbe] voxels=(%.1f,%.1f,%.1f) owner=%.5f shape=%.5f owner_air=%d shape_air=%d protected_air=%d solid_overlay=%d\n",
|
|
X,Y,Z,OwnerDensity,ShapeDensity,OwnerDensity>=0.0f?1:0,ShapeDensity>=0.0f?1:0,
|
|
Shape.debugCriticalAirAt({X,Y,Z})?1:0,Shape.debugSolidOverlayAt({X,Y,Z})?1:0);
|
|
}
|
|
}
|
|
std::vector<FVoxelTileKey> CoreDesired;
|
|
for(const auto& Key:Shape.desiredTiles({CameraVoxels.X,CameraVoxels.Y,CameraVoxels.Z}))
|
|
CoreDesired.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
std::vector<FVoxelTileKey> Tiles;
|
|
for(const FVoxelTileKey& Tile:CoreDesired)
|
|
if(LevelFilter<0?Tile.Level<World.FullResClipLevels:Tile.Level==LevelFilter)
|
|
Tiles.push_back(Tile);
|
|
if(TileLimit>0&&Tiles.size()>static_cast<size_t>(TileLimit))
|
|
{
|
|
constexpr double DegreesToRadians=3.14159265358979323846/180.0;
|
|
const double YawRadians=CameraYaw*DegreesToRadians,PitchRadians=CameraPitch*DegreesToRadians;
|
|
const V3 ViewForward{static_cast<float>(std::sin(YawRadians)*std::cos(PitchRadians)),
|
|
static_cast<float>(std::cos(YawRadians)*std::cos(PitchRadians)),
|
|
static_cast<float>(std::sin(PitchRadians))};
|
|
const auto ViewScore=[&](const FVoxelTileKey& Tile)
|
|
{
|
|
const FIntVector O=Tile.OriginVoxels();
|
|
const V3 D{static_cast<float>(O.X+Tile.ExtentVoxels()*0.5-CameraVoxels.X),
|
|
static_cast<float>(O.Y+Tile.ExtentVoxels()*0.5-CameraVoxels.Y),
|
|
static_cast<float>(O.Z+Tile.ExtentVoxels()*0.5-CameraVoxels.Z)};
|
|
const double Distance=std::sqrt(Dot(D,D));
|
|
// Keep near tiles first while preferring the half-space in front of this view.
|
|
return Distance*Distance+32.0*std::max(0.0,Distance-Dot(D,ViewForward));
|
|
};
|
|
std::sort(Tiles.begin(),Tiles.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
const double AS=ViewScore(A),BS=ViewScore(B);
|
|
if(AS!=BS)return AS<BS;
|
|
if(A.Coord.X!=B.Coord.X)return A.Coord.X<B.Coord.X;
|
|
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
|
|
return A.Coord.Z<B.Coord.Z;
|
|
});
|
|
Tiles.resize(static_cast<size_t>(TileLimit));
|
|
}
|
|
if(Tiles.empty()){std::fprintf(stderr,"No tiles are available for the requested shape comparison level.\n");return 12;}
|
|
|
|
struct FTileComparison
|
|
{
|
|
uint64 Samples=0,Changed=0,MeshMismatch=0;
|
|
uint64 OldHash=14695981039346656037ull,NewHash=14695981039346656037ull;
|
|
std::vector<FComparePoint> OldSurface,NewSurface;
|
|
std::array<double,4> OwnerBandRms{},ShapeBandRms{};
|
|
double SurfacePairDistanceVoxels=0.0;
|
|
bool bHasOwnerBandPatch=false;
|
|
};
|
|
std::vector<FTileComparison> Results(Tiles.size());
|
|
int32 MinChunkZ=MIN_int32,MaxChunkZ=MAX_int32,TopChunkZ=0,BottomChunkZ=0;
|
|
if(World.Settings->bClampViewToStrate&&World.Manager->GetStrateChunkZBounds(
|
|
StandaloneWorldMesher::CameraChunk(CameraVoxels).Z,TopChunkZ,BottomChunkZ))
|
|
{MinChunkZ=BottomChunkZ*CHUNK_SIZE;MaxChunkZ=(TopChunkZ+1)*CHUNK_SIZE;}
|
|
|
|
std::atomic<size_t> Next{0};
|
|
std::vector<std::thread> Threads;
|
|
const int32 ThreadCount=std::clamp(Workers,1,8);
|
|
Threads.reserve(ThreadCount);
|
|
for(int32 Worker=0;Worker<ThreadCount;++Worker)
|
|
{
|
|
Threads.emplace_back([&]()
|
|
{
|
|
UVoxelMarchingCubesMesher Mesher;
|
|
Mesher.SetGenerator(World.Generator.get());
|
|
Mesher.bGenerateSkirts=World.bGenerateSkirts;
|
|
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);
|
|
Mesher.LODOctaveDrop=World.LODOctaveDrop;
|
|
for(;;)
|
|
{
|
|
const size_t Index=Next.fetch_add(1,std::memory_order_relaxed);
|
|
if(Index>=Tiles.size())break;
|
|
const FVoxelTileKey& Tile=Tiles[Index];
|
|
const int32 Cells=Tile.Level<World.FullResClipLevels?CHUNK_SIZE:World.CoarseTileCells;
|
|
const int32 Step=std::max(1,Tile.ExtentVoxels()/Cells);
|
|
VoxelForgeCore::TileGenerationOptions ShapeOptions;
|
|
ShapeOptions.bandMinZVoxel=MinChunkZ;ShapeOptions.bandMaxZVoxel=MaxChunkZ;
|
|
ShapeOptions.captureDensitySamples=true;
|
|
std::string CoreError;
|
|
auto NewMesh=Shape.generateTile({{Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z},Tile.Level},
|
|
ShapeOptions,CoreError);
|
|
std::vector<float> NewDensities=NewMesh?std::move(NewMesh->debugDensitySamples):std::vector<float>{};
|
|
std::vector<float> OldDensities;
|
|
FVoxelMeshData OldMesh;
|
|
OldMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&OldDensities);
|
|
FTileComparison& R=Results[Index];
|
|
const size_t Shared=std::min(NewDensities.size(),OldDensities.size());
|
|
R.Samples=Shared;
|
|
R.MeshMismatch=std::max(NewDensities.size(),OldDensities.size())-Shared;
|
|
for(size_t I=0;I<Shared;++I)
|
|
{
|
|
if((NewDensities[I]<0.0f)!=(OldDensities[I]<0.0f))++R.Changed;
|
|
R.NewHash^=std::bit_cast<uint32>(NewDensities[I]);R.NewHash*=1099511628211ull;
|
|
R.OldHash^=std::bit_cast<uint32>(OldDensities[I]);R.OldHash*=1099511628211ull;
|
|
}
|
|
if(Tile.Level==0&&NewMesh&&OldMesh.NumSurfaceVertices>0
|
|
&&NewMesh->groups.surfaceVertexCount>0)
|
|
{
|
|
const int32 OldSurfaceCount=std::min(OldMesh.NumSurfaceVertices,
|
|
OldMesh.Vertices.Num());
|
|
const size_t NewSurfaceCount=std::min<size_t>(
|
|
NewMesh->groups.surfaceVertexCount,NewMesh->vertices.size());
|
|
const FIntVector TileOrigin=Tile.OriginVoxels();
|
|
const V3 TileCenter{TileOrigin.X+Tile.ExtentVoxels()*0.5f,
|
|
TileOrigin.Y+Tile.ExtentVoxels()*0.5f,
|
|
TileOrigin.Z+Tile.ExtentVoxels()*0.5f};
|
|
double OwnerCenterDistanceSq=std::numeric_limits<double>::infinity();
|
|
double ShapeCenterDistanceSq=std::numeric_limits<double>::infinity();
|
|
V3 OwnerPoint{},OwnerNormal{},ShapePoint{},ShapeNormal{};
|
|
for(int32 OldIndex=0;OldIndex<OldSurfaceCount;++OldIndex)
|
|
{
|
|
if(OldIndex>=OldMesh.Normals.Num())continue;
|
|
const FVector& OP=OldMesh.Vertices[OldIndex];
|
|
const FVector& ON=OldMesh.Normals[OldIndex];
|
|
const V3 CandidatePoint{static_cast<float>(OP.X*0.04),
|
|
static_cast<float>(OP.Y*0.04),static_cast<float>(OP.Z*0.04)};
|
|
const V3 CandidateNormal=Normalize({static_cast<float>(ON.X),
|
|
static_cast<float>(ON.Y),static_cast<float>(ON.Z)});
|
|
const V3 Delta=Sub(CandidatePoint,TileCenter);
|
|
const double DistanceSq=Dot(Delta,Delta);
|
|
if(DistanceSq<OwnerCenterDistanceSq)
|
|
{
|
|
OwnerCenterDistanceSq=DistanceSq;OwnerPoint=CandidatePoint;
|
|
OwnerNormal=CandidateNormal;
|
|
}
|
|
}
|
|
if(std::isfinite(OwnerCenterDistanceSq))
|
|
{
|
|
for(size_t NewIndex=0;NewIndex<NewSurfaceCount;++NewIndex)
|
|
{
|
|
const auto& NV=NewMesh->vertices[NewIndex];
|
|
const V3 CandidatePoint{static_cast<float>(NV.position.x*0.04),
|
|
static_cast<float>(NV.position.y*0.04),
|
|
static_cast<float>(NV.position.z*0.04)};
|
|
const V3 CandidateNormal=Normalize({static_cast<float>(NV.normal.x),
|
|
static_cast<float>(NV.normal.y),static_cast<float>(NV.normal.z)});
|
|
if(Dot(CandidateNormal,OwnerNormal)<0.5f)continue;
|
|
const V3 Delta=Sub(CandidatePoint,OwnerPoint);
|
|
const double DistanceSq=Dot(Delta,Delta);
|
|
if(DistanceSq<ShapeCenterDistanceSq)
|
|
{
|
|
ShapeCenterDistanceSq=DistanceSq;ShapePoint=CandidatePoint;
|
|
ShapeNormal=CandidateNormal;
|
|
}
|
|
}
|
|
}
|
|
if(std::isfinite(OwnerCenterDistanceSq)&&std::isfinite(ShapeCenterDistanceSq)
|
|
&&ShapeCenterDistanceSq<=400.0)
|
|
{
|
|
const auto TangentBasis=[](V3 Normal,V3& TangentU,V3& TangentV)
|
|
{
|
|
TangentU=Cross(Normal,{0.0f,0.0f,1.0f});
|
|
if(Dot(TangentU,TangentU)<1.0e-6f)
|
|
TangentU=Cross(Normal,{1.0f,0.0f,0.0f});
|
|
TangentU=Normalize(TangentU);
|
|
TangentV=Normalize(Cross(Normal,TangentU));
|
|
};
|
|
const auto FieldNormal=[](V3 Point,V3 Normal,const auto& Density)
|
|
{
|
|
if(Dot(Normal,Normal)>=0.25f)return Normalize(Normal);
|
|
const V3 GX{0.5f,0.0f,0.0f},GY{0.0f,0.5f,0.0f},GZ{0.0f,0.0f,0.5f};
|
|
return Normalize({Density(Add(Point,GX))-Density(Sub(Point,GX)),
|
|
Density(Add(Point,GY))-Density(Sub(Point,GY)),
|
|
Density(Add(Point,GZ))-Density(Sub(Point,GZ))});
|
|
};
|
|
const auto OwnerDensity=[&](V3 P)
|
|
{return World.Generator->GetDensityAt(P.X,P.Y,P.Z);};
|
|
const auto ShapeDensity=[&](V3 P)
|
|
{return Shape.densityAt({P.X,P.Y,P.Z});};
|
|
OwnerNormal=FieldNormal(OwnerPoint,OwnerNormal,OwnerDensity);
|
|
ShapeNormal=FieldNormal(ShapePoint,ShapeNormal,ShapeDensity);
|
|
V3 OwnerTangentU{},OwnerTangentV{},ShapeTangentU{},ShapeTangentV{};
|
|
TangentBasis(OwnerNormal,OwnerTangentU,OwnerTangentV);
|
|
TangentBasis(ShapeNormal,ShapeTangentU,ShapeTangentV);
|
|
std::array<double,OwnerBandGrid*OwnerBandGrid> OwnerHeights{},ShapeHeights{};
|
|
std::array<uint8,OwnerBandGrid*OwnerBandGrid> OwnerValid{},ShapeValid{};
|
|
const bool bOwnerPatch=SampleOwnerSurfaceHeightPatch(OwnerPoint,
|
|
OwnerNormal,OwnerTangentU,OwnerTangentV,OwnerDensity,
|
|
OwnerHeights,OwnerValid);
|
|
const bool bShapePatch=SampleOwnerSurfaceHeightPatch(ShapePoint,
|
|
ShapeNormal,ShapeTangentU,ShapeTangentV,ShapeDensity,
|
|
ShapeHeights,ShapeValid);
|
|
if(bOwnerPatch&&bShapePatch
|
|
&&OwnerSurfaceBandRms(OwnerHeights,OwnerValid,R.OwnerBandRms)
|
|
&&OwnerSurfaceBandRms(ShapeHeights,ShapeValid,R.ShapeBandRms))
|
|
{
|
|
R.SurfacePairDistanceVoxels=std::sqrt(ShapeCenterDistanceSq);
|
|
R.bHasOwnerBandPatch=true;
|
|
}
|
|
}
|
|
}
|
|
constexpr size_t VertexStride=4;
|
|
if(NewMesh)
|
|
{
|
|
R.NewSurface.reserve(NewMesh->vertices.size()/VertexStride+1);
|
|
for(size_t I=0;I<NewMesh->vertices.size();I+=VertexStride)
|
|
{const auto& P=NewMesh->vertices[I].position;R.NewSurface.push_back({P.x*0.01,P.y*0.01,P.z*0.01});}
|
|
}
|
|
R.OldSurface.reserve(static_cast<size_t>(OldMesh.Vertices.Num())/VertexStride+1);
|
|
for(int32 I=0;I<OldMesh.Vertices.Num();I+=static_cast<int32>(VertexStride))
|
|
{const FVector& P=OldMesh.Vertices[I];R.OldSurface.push_back({P.X*0.01,P.Y*0.01,P.Z*0.01});}
|
|
}
|
|
});
|
|
}
|
|
for(std::thread& Thread:Threads)Thread.join();
|
|
|
|
uint64 Samples=0,Changed=0,MeshMismatch=0;
|
|
uint64 OwnerBandPatches=0;
|
|
std::array<double,4> OwnerBandEnergy{},ShapeBandEnergy{};
|
|
double SurfacePairDistanceSum=0.0;
|
|
uint64 OldHash=14695981039346656037ull,NewHash=14695981039346656037ull;
|
|
size_t OldVertexCount=0,NewVertexCount=0;
|
|
std::vector<FComparePoint> OldSurface,NewSurface;
|
|
for(const FTileComparison& R:Results)
|
|
{
|
|
Samples+=R.Samples;Changed+=R.Changed;MeshMismatch+=R.MeshMismatch;
|
|
if(R.bHasOwnerBandPatch)
|
|
{
|
|
++OwnerBandPatches;SurfacePairDistanceSum+=R.SurfacePairDistanceVoxels;
|
|
for(size_t Band=0;Band<4;++Band)
|
|
{
|
|
OwnerBandEnergy[Band]+=R.OwnerBandRms[Band]*R.OwnerBandRms[Band];
|
|
ShapeBandEnergy[Band]+=R.ShapeBandRms[Band]*R.ShapeBandRms[Band];
|
|
}
|
|
}
|
|
OldHash^=R.OldHash;OldHash*=1099511628211ull;
|
|
NewHash^=R.NewHash;NewHash*=1099511628211ull;
|
|
OldVertexCount+=R.OldSurface.size();NewVertexCount+=R.NewSurface.size();
|
|
}
|
|
OldSurface.reserve(OldVertexCount);NewSurface.reserve(NewVertexCount);
|
|
for(FTileComparison& R:Results)
|
|
{
|
|
OldSurface.insert(OldSurface.end(),R.OldSurface.begin(),R.OldSurface.end());
|
|
NewSurface.insert(NewSurface.end(),R.NewSurface.begin(),R.NewSurface.end());
|
|
}
|
|
const FDirectedSurfaceDistance OldToNew=DirectedSurfaceDistance(OldSurface,NewSurface,1.0,3);
|
|
const FDirectedSurfaceDistance NewToOld=DirectedSurfaceDistance(NewSurface,OldSurface,1.0,3);
|
|
const uint64 DistanceSamples=OldToNew.Count+NewToOld.Count;
|
|
const double MeanDistance=DistanceSamples
|
|
?(OldToNew.Sum+NewToOld.Sum)/static_cast<double>(DistanceSamples):0.0;
|
|
std::printf("[OwnerShapeCompare] tiles=%zu fullres_levels=%d level_filter=%d lattice_samples=%llu changed_signs=%llu changed_share=%.6f%% old_hash=0x%016llx shape_hash=0x%016llx mesh_sample_mismatch=%llu old_surface_samples=%zu shape_surface_samples=%zu symmetric_vertex_mean_m=%.4f symmetric_vertex_max_m=%.4f unmatched_surface_samples=%llu vertex_stride=4 workers=%d\n",
|
|
Tiles.size(),World.FullResClipLevels,LevelFilter,static_cast<unsigned long long>(Samples),
|
|
static_cast<unsigned long long>(Changed),Samples?100.0*Changed/Samples:0.0,
|
|
static_cast<unsigned long long>(OldHash),static_cast<unsigned long long>(NewHash),
|
|
static_cast<unsigned long long>(MeshMismatch),OldSurface.size(),NewSurface.size(),
|
|
MeanDistance,std::max(OldToNew.Maximum,NewToOld.Maximum),
|
|
static_cast<unsigned long long>(OldToNew.Unmatched+NewToOld.Unmatched),ThreadCount);
|
|
const std::array<const char*,4> BandNames={"2-4","4-8","8-16","16-32"};
|
|
const std::array<double,4> BandMinimumRatio={0.0,0.75,0.75,0.65};
|
|
bool bReliefBandsPass=OwnerBandPatches>0;
|
|
std::printf("[OwnerReliefBandCompare] area=%.*s paired_surface_patches=%llu mean_surface_pair_distance=%.3f_vox grid=%dx%d spacing=%.2f_vox bands_rms_vox=",
|
|
static_cast<int>(AreaLabel.size()),AreaLabel.data(),
|
|
static_cast<unsigned long long>(OwnerBandPatches),OwnerBandPatches
|
|
?SurfacePairDistanceSum/static_cast<double>(OwnerBandPatches):0.0,
|
|
OwnerBandGrid,OwnerBandGrid,OwnerBandSpacing);
|
|
for(size_t Band=0;Band<4;++Band)
|
|
{
|
|
const double OwnerRms=OwnerBandPatches
|
|
?std::sqrt(OwnerBandEnergy[Band]/static_cast<double>(OwnerBandPatches)):0.0;
|
|
const double ShapeRms=OwnerBandPatches
|
|
?std::sqrt(ShapeBandEnergy[Band]/static_cast<double>(OwnerBandPatches)):0.0;
|
|
const double Ratio=OwnerRms>1.0e-5?ShapeRms/OwnerRms:0.0;
|
|
std::printf("%s(owner=%.4f,shape=%.4f,ratio=%.3f)%s",BandNames[Band],
|
|
OwnerRms,ShapeRms,Ratio,Band<3?" ":"\n");
|
|
if(Band>0&&OwnerRms>=0.025&&Ratio<BandMinimumRatio[Band])bReliefBandsPass=false;
|
|
}
|
|
std::printf("[OwnerReliefBandAudit] minimum_ratio_4-8=0.750 minimum_ratio_8-16=0.750 minimum_ratio_16-32=0.650 evaluated_patches=%llu status=%s%s\n",
|
|
static_cast<unsigned long long>(OwnerBandPatches),bReliefBandsPass?"PASS":"FAIL",
|
|
bCheckOwnerReliefBands?" (gate enabled)":" (measurement only)");
|
|
std::fflush(stdout);
|
|
return Samples>0&&MeshMismatch==0&&(!bCheckOwnerReliefBands||bReliefBandsPass)?0:13;
|
|
}
|
|
|
|
int RunRoundWallComparison(FStandaloneOwnerWorld& World,V3 CameraPosition,int32 TileCount)
|
|
{
|
|
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>(TileCount));
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
if(Tile.Level==0&&static_cast<int32>(Tiles.size())<TileCount)Tiles.push_back(Tile);
|
|
if(Tiles.empty()){std::fprintf(stderr,"No LOD0 tiles are available for the round-tunnel comparison.\n");return 9;}
|
|
struct FComparisonTile
|
|
{
|
|
std::vector<float> Densities;
|
|
std::vector<FComparePoint> Surface;
|
|
};
|
|
std::vector<FComparisonTile> Legacy(Tiles.size());
|
|
std::vector<FComparisonTile> Detailed(Tiles.size());
|
|
UVoxelMarchingCubesMesher Mesher;
|
|
Mesher.SetGenerator(World.Generator.get());Mesher.bGenerateSkirts=World.bGenerateSkirts;
|
|
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);Mesher.LODOctaveDrop=World.LODOctaveDrop;
|
|
int32 MinChunkZ=MIN_int32,MaxChunkZ=MAX_int32,TopChunkZ=0,BottomChunkZ=0;
|
|
if(World.Settings->bClampViewToStrate&&World.Manager->GetStrateChunkZBounds(
|
|
StandaloneWorldMesher::CameraChunk(CameraVoxels).Z,TopChunkZ,BottomChunkZ))
|
|
{
|
|
MinChunkZ=BottomChunkZ*CHUNK_SIZE;
|
|
MaxChunkZ=(TopChunkZ+1)*CHUNK_SIZE;
|
|
}
|
|
uint64 TotalSamples=0,ChangedSigns=0;
|
|
uint64 LegacyHash=14695981039346656037ull,DetailedHash=14695981039346656037ull;
|
|
const bool PreviousRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels;
|
|
const bool PreviousEnhancedWalls=World.Generator->bStandaloneResearchEnhancedWalls;
|
|
const auto Evaluate=[&](bool bEnhanced,std::vector<FComparisonTile>& Out)
|
|
{
|
|
World.Generator->bStandaloneResearchRoundTunnels=true;
|
|
World.Generator->bStandaloneResearchEnhancedWalls=bEnhanced;
|
|
for(size_t I=0;I<Tiles.size();++I)
|
|
{
|
|
const FVoxelTileKey& Tile=Tiles[I];
|
|
const int32 Step=1,Cells=CHUNK_SIZE;
|
|
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
MinChunkZ,MaxChunkZ,nullptr,nullptr,nullptr,&Out[I].Densities);
|
|
Out[I].Surface.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
|
|
for(const FVector& P:Mesh.Vertices)
|
|
Out[I].Surface.push_back({P.X*0.01,P.Y*0.01,P.Z*0.01});
|
|
}
|
|
};
|
|
Evaluate(false,Legacy);
|
|
Evaluate(true,Detailed);
|
|
World.Generator->bStandaloneResearchRoundTunnels=PreviousRoundTunnels;
|
|
World.Generator->bStandaloneResearchEnhancedWalls=PreviousEnhancedWalls;
|
|
FDirectedSurfaceDistance LegacyToDetailed,DetailedToLegacy;
|
|
std::vector<FComparePoint> AllLegacySurface,AllDetailedSurface;
|
|
for(size_t TileIndex=0;TileIndex<Tiles.size();++TileIndex)
|
|
{
|
|
const auto& A=Legacy[TileIndex];const auto& B=Detailed[TileIndex];
|
|
const size_t Shared=std::min(A.Densities.size(),B.Densities.size());
|
|
TotalSamples+=Shared;
|
|
for(size_t I=0;I<Shared;++I)
|
|
{
|
|
if((A.Densities[I]<0.0f)!=(B.Densities[I]<0.0f))++ChangedSigns;
|
|
LegacyHash^=std::bit_cast<uint32>(A.Densities[I]);LegacyHash*=1099511628211ull;
|
|
DetailedHash^=std::bit_cast<uint32>(B.Densities[I]);DetailedHash*=1099511628211ull;
|
|
}
|
|
AllLegacySurface.insert(AllLegacySurface.end(),A.Surface.begin(),A.Surface.end());
|
|
AllDetailedSurface.insert(AllDetailedSurface.end(),B.Surface.begin(),B.Surface.end());
|
|
}
|
|
LegacyToDetailed=DirectedSurfaceDistance(AllLegacySurface,AllDetailedSurface);
|
|
DetailedToLegacy=DirectedSurfaceDistance(AllDetailedSurface,AllLegacySurface);
|
|
const uint64 DistanceSamples=LegacyToDetailed.Count+DetailedToLegacy.Count;
|
|
const double MeanDistance=DistanceSamples>0
|
|
?(LegacyToDetailed.Sum+DetailedToLegacy.Sum)/static_cast<double>(DistanceSamples):0.0;
|
|
const double Hausdorff=std::max(LegacyToDetailed.Maximum,DetailedToLegacy.Maximum);
|
|
std::printf("[RoundTunnelCompare] tiles=%zu lattice_samples=%llu changed_signs=%llu changed_share=%.6f%% legacy_density_hash=0x%016llx detailed_density_hash=0x%016llx old_vertices=%zu new_vertices=%zu symmetric_vertex_mean_m=%.4f symmetric_vertex_max_m=%.4f unmatched_vertices=%llu\n",
|
|
Tiles.size(),static_cast<unsigned long long>(TotalSamples),
|
|
static_cast<unsigned long long>(ChangedSigns),TotalSamples?100.0*ChangedSigns/TotalSamples:0.0,
|
|
static_cast<unsigned long long>(LegacyHash),static_cast<unsigned long long>(DetailedHash),
|
|
AllLegacySurface.size(),AllDetailedSurface.size(),MeanDistance,Hausdorff,
|
|
static_cast<unsigned long long>(LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched));
|
|
return TotalSamples>0&&LegacyToDetailed.Unmatched+DetailedToLegacy.Unmatched==0?0:10;
|
|
}
|
|
|
|
void HashWord(uint64& Hash,uint64 Word,int32 Bytes=8)
|
|
{
|
|
for(int32 I=0;I<Bytes;++I)
|
|
{
|
|
Hash^=static_cast<uint8>(Word>>(I*8));
|
|
Hash*=1099511628211ull;
|
|
}
|
|
}
|
|
|
|
struct FHashedTile
|
|
{
|
|
FVoxelTileKey Key;
|
|
uint64 Hash=14695981039346656037ull;
|
|
};
|
|
|
|
bool TileKeyLessForHash(const FVoxelTileKey& A,const FVoxelTileKey& B)
|
|
{
|
|
if(A.Level!=B.Level)return A.Level<B.Level;
|
|
if(A.Coord.Z!=B.Coord.Z)return A.Coord.Z<B.Coord.Z;
|
|
if(A.Coord.Y!=B.Coord.Y)return A.Coord.Y<B.Coord.Y;
|
|
return A.Coord.X<B.Coord.X;
|
|
}
|
|
|
|
uint64 HashTileRows(std::vector<FHashedTile>& Rows)
|
|
{
|
|
std::sort(Rows.begin(),Rows.end(),[](const FHashedTile& A,const FHashedTile& B)
|
|
{return TileKeyLessForHash(A.Key,B.Key);});
|
|
uint64 Hash=14695981039346656037ull;
|
|
for(const FHashedTile& Row:Rows)
|
|
{
|
|
HashWord(Hash,static_cast<uint32>(Row.Key.Level),4);
|
|
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.X),4);
|
|
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Y),4);
|
|
HashWord(Hash,static_cast<uint32>(Row.Key.Coord.Z),4);
|
|
HashWord(Hash,Row.Hash);
|
|
}
|
|
return Hash;
|
|
}
|
|
|
|
int RunPipelinedStreamingBenchmark(std::shared_ptr<FStandaloneOwnerWorld> OwnerWorld,
|
|
const VoxelForgeCore::ResolvedWorldSnapshot& CoreSnapshot,
|
|
const FVector& CameraPositionVoxels,int32 WorkerCount,int32 MaxLevel,
|
|
VoxelForgeCore::DetailVariant DetailVariant,const VoxelForgeCore::JobScheduler& Scheduler,
|
|
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,bool bSkipUV,bool bSkipColors,
|
|
std::chrono::steady_clock::time_point ColdStart)
|
|
{
|
|
using Clock=std::chrono::steady_clock;
|
|
FStandaloneOwnerWorld& World=*OwnerWorld;
|
|
const bool bRoundTunnels=World.Generator->bStandaloneResearchRoundTunnels;
|
|
const bool bEnhancedTunnelWalls=World.Generator->bStandaloneResearchEnhancedWalls;
|
|
VoxelForgeCore::World Shape(CoreSnapshot);
|
|
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
|
const auto CoreTiles=Shape.desiredTiles(
|
|
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
|
Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size());
|
|
for(const auto& Key:CoreTiles)
|
|
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
if(MaxLevel>=0)
|
|
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
|
|
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
|
|
FIntVector FineMin(INT32_MAX,INT32_MAX,INT32_MAX),FineMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
FIntVector ClipMin(INT32_MAX,INT32_MAX,INT32_MAX),ClipMax(INT32_MIN,INT32_MIN,INT32_MIN);
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
{
|
|
const FIntVector Origin=Tile.OriginVoxels();const int32 Extent=Tile.ExtentVoxels();
|
|
ClipMin.X=std::min(ClipMin.X,Origin.X);ClipMin.Y=std::min(ClipMin.Y,Origin.Y);ClipMin.Z=std::min(ClipMin.Z,Origin.Z);
|
|
ClipMax.X=std::max(ClipMax.X,Origin.X+Extent);ClipMax.Y=std::max(ClipMax.Y,Origin.Y+Extent);ClipMax.Z=std::max(ClipMax.Z,Origin.Z+Extent);
|
|
if(Tile.Level<World.FullResClipLevels)
|
|
{
|
|
FineMin.X=std::min(FineMin.X,Origin.X);FineMin.Y=std::min(FineMin.Y,Origin.Y);FineMin.Z=std::min(FineMin.Z,Origin.Z);
|
|
FineMax.X=std::max(FineMax.X,Origin.X+Extent);FineMax.Y=std::max(FineMax.Y,Origin.Y+Extent);FineMax.Z=std::max(FineMax.Z,Origin.Z+Extent);
|
|
}
|
|
}
|
|
if(FineMin.X==INT32_MAX||ClipMin.X==INT32_MAX)
|
|
{std::fprintf(stderr,"[StreamPipeline] no desired detail or clipmap tiles.\n");return 7;}
|
|
|
|
const FIntVector CenterChunk=Desired.CenterChunk;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> Neighborhood;
|
|
Neighborhood.reserve(32);
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
if(Tile.Level==0&&std::abs(Tile.Coord.X-CenterChunk.X)<=1
|
|
&&std::abs(Tile.Coord.Y-CenterChunk.Y)<=1&&std::abs(Tile.Coord.Z-CenterChunk.Z)<=1)
|
|
Neighborhood.insert(Tile);
|
|
if(Neighborhood.size()!=27)
|
|
{std::fprintf(stderr,"[StreamPipeline] expected 27 player-neighborhood tiles, found %zu.\n",Neighborhood.size());return 7;}
|
|
|
|
// The initial desired set now owns all clipmap metadata needed by this Core-only run.
|
|
// Release the separate compatibility-world generation graph before compiling Core.
|
|
World.Generator.reset();
|
|
World.Manager.reset();
|
|
World.DefinitionStorage.clear();
|
|
World.BiomeStorage.clear();
|
|
World.OperationStorage.clear();
|
|
World.DensitySamples.clear();
|
|
|
|
std::string BuildError;
|
|
const bool bIncludeFarWorld=MaxLevel<0||MaxLevel>=World.FullResClipLevels;
|
|
const auto ShapeBuildStart=Clock::now();
|
|
VoxelForgeCore::ShapeBuildRequest ShapeRequest;
|
|
ShapeRequest.minVoxel={FineMin.X,FineMin.Y,FineMin.Z};
|
|
ShapeRequest.maxVoxel={FineMax.X,FineMax.Y,FineMax.Z};
|
|
ShapeRequest.clipMinVoxel={ClipMin.X,ClipMin.Y,ClipMin.Z};
|
|
ShapeRequest.clipMaxVoxel={ClipMax.X,ClipMax.Y,ClipMax.Z};
|
|
ShapeRequest.workerLimit=static_cast<uint32>(std::clamp(WorkerCount,1,8));
|
|
ShapeRequest.detail=DetailVariant;ShapeRequest.includeFarWorld=bIncludeFarWorld;
|
|
ShapeRequest.roundTunnels=bRoundTunnels;
|
|
ShapeRequest.enhancedTunnelWalls=bEnhancedTunnelWalls;
|
|
if(!Shape.compile(ShapeRequest,Scheduler,BuildError))
|
|
{std::fprintf(stderr,"[StreamPipeline] full shape build failed: %s\n",BuildError.c_str());return 2;}
|
|
const auto ShapeReady=Clock::now();
|
|
const double ShapeSeconds=std::chrono::duration<double>(ShapeReady-ShapeBuildStart).count();
|
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,bHashDensity,nullptr,MaxLevel,
|
|
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,&Shape);
|
|
const auto StreamStart=Clock::now();
|
|
const FStandaloneStreamingUpdate Update=Streamer.StartWithDesired(Desired);
|
|
if(Update.DesiredTiles!=static_cast<int32>(Desired.Tiles.size()))
|
|
{
|
|
Streamer.Stop();
|
|
std::fprintf(stderr,"[StreamPipeline] expected %zu tiles but queued %d.\n",
|
|
Desired.Tiles.size(),Update.DesiredTiles);return 7;
|
|
}
|
|
|
|
std::vector<FHashedTile> DensityRows,MeshRows,PayloadRows,TopologyRows;
|
|
int32 Finished=0,NeighborhoodFinished=0,FullDetailFinished=0;
|
|
int32 FullDetailExpected=0;
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
if(Tile.Level<World.FullResClipLevels)++FullDetailExpected;
|
|
Clock::time_point NeighborhoodReady{},FullDetailReady{},WholeViewReady{};
|
|
bool bNeighborhoodReported=false,bFullDetailReported=false;
|
|
while(Finished<Update.DesiredTiles)
|
|
{
|
|
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
|
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
|
for(const FStandaloneCpuTile& Tile:Ready)
|
|
{
|
|
++Finished;
|
|
WholeViewReady=std::max(WholeViewReady,Tile.CompletedAt);
|
|
if(Neighborhood.contains(Tile.Key))
|
|
{
|
|
++NeighborhoodFinished;
|
|
NeighborhoodReady=std::max(NeighborhoodReady,Tile.CompletedAt);
|
|
}
|
|
if(Tile.Key.Level<World.FullResClipLevels)
|
|
{
|
|
++FullDetailFinished;
|
|
FullDetailReady=std::max(FullDetailReady,Tile.CompletedAt);
|
|
}
|
|
if(bHashDensity)
|
|
{
|
|
if(Tile.bHasDensityHash)DensityRows.push_back({Tile.Key,Tile.DensityHash});
|
|
if(Tile.bHasMeshHash)MeshRows.push_back({Tile.Key,Tile.MeshHash});
|
|
if(Tile.bHasPayloadHash)PayloadRows.push_back({Tile.Key,Tile.PayloadHash});
|
|
if(Tile.bHasTopologyHash)TopologyRows.push_back({Tile.Key,Tile.TopologyHash});
|
|
}
|
|
}
|
|
if(!bNeighborhoodReported&&NeighborhoodFinished==static_cast<int32>(Neighborhood.size()))
|
|
{
|
|
std::printf("[PipelineMilestone] neighborhood_ready=%.3fs generated=%d/27\n",
|
|
std::chrono::duration<double>(NeighborhoodReady-ColdStart).count(),NeighborhoodFinished);
|
|
bNeighborhoodReported=true;
|
|
}
|
|
if(!bFullDetailReported&&FullDetailFinished==FullDetailExpected)
|
|
{
|
|
std::printf("[PipelineMilestone] full_detail_ready=%.3fs generated=%d/%d\n",
|
|
std::chrono::duration<double>(FullDetailReady-ColdStart).count(),
|
|
FullDetailFinished,FullDetailExpected);
|
|
bFullDetailReported=true;
|
|
}
|
|
}
|
|
Streamer.Stop();
|
|
const double NeighborhoodSeconds=std::chrono::duration<double>(NeighborhoodReady-ColdStart).count();
|
|
const double NeighborhoodMeshSeconds=std::chrono::duration<double>(NeighborhoodReady-ShapeReady).count();
|
|
const double FullDetailSeconds=std::chrono::duration<double>(FullDetailReady-ColdStart).count();
|
|
const double WholeViewSeconds=std::chrono::duration<double>(WholeViewReady-ColdStart).count();
|
|
const double StreamSeconds=std::chrono::duration<double>(WholeViewReady-StreamStart).count();
|
|
std::printf("[StreamPipeline] workers=%d shape=%.3fs neighborhood_mesh=%.3fs neighborhood_ready=%.3fs full_detail_ready=%.3fs whole_view_ready=%.3fs neighborhood_tiles=%d/27 full_detail_tiles=%d/%d whole_tiles=%d/%zu stream_wall=%.3fs cache_hits=0 full_payload=1\n",
|
|
WorkerCount,ShapeSeconds,NeighborhoodMeshSeconds,NeighborhoodSeconds,
|
|
FullDetailSeconds,WholeViewSeconds,NeighborhoodFinished,
|
|
FullDetailFinished,FullDetailExpected,Finished,Desired.Tiles.size(),StreamSeconds);
|
|
|
|
bool bHashesPassing=true;
|
|
if(bHashDensity)
|
|
{
|
|
bHashesPassing=DensityRows.size()==Desired.Tiles.size()
|
|
&&MeshRows.size()==Desired.Tiles.size()
|
|
&&PayloadRows.size()==Desired.Tiles.size()
|
|
&&TopologyRows.size()==Desired.Tiles.size();
|
|
std::vector<FHashedTile> FullDensity,FullMesh,FullPayload,FullTopology;
|
|
for(const FHashedTile& Row:DensityRows)if(Row.Key.Level<World.FullResClipLevels)FullDensity.push_back(Row);
|
|
for(const FHashedTile& Row:MeshRows)if(Row.Key.Level<World.FullResClipLevels)FullMesh.push_back(Row);
|
|
for(const FHashedTile& Row:PayloadRows)if(Row.Key.Level<World.FullResClipLevels)FullPayload.push_back(Row);
|
|
for(const FHashedTile& Row:TopologyRows)if(Row.Key.Level<World.FullResClipLevels)FullTopology.push_back(Row);
|
|
std::printf("[PipelineFullDetailHash] tiles=%d density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
|
FullDetailExpected,static_cast<unsigned long long>(HashTileRows(FullDensity)),
|
|
static_cast<unsigned long long>(HashTileRows(FullMesh)),
|
|
static_cast<unsigned long long>(HashTileRows(FullPayload)),
|
|
static_cast<unsigned long long>(HashTileRows(FullTopology)));
|
|
std::printf("[PipelineWholeViewHash] tiles=%zu density=0x%016llx mesh=0x%016llx payload=0x%016llx topology=0x%016llx\n",
|
|
Desired.Tiles.size(),static_cast<unsigned long long>(HashTileRows(DensityRows)),
|
|
static_cast<unsigned long long>(HashTileRows(MeshRows)),
|
|
static_cast<unsigned long long>(HashTileRows(PayloadRows)),
|
|
static_cast<unsigned long long>(HashTileRows(TopologyRows)));
|
|
}
|
|
return NeighborhoodFinished==static_cast<int32>(Neighborhood.size())
|
|
&&FullDetailFinished==FullDetailExpected&&Finished==Update.DesiredTiles&&bHashesPassing?0:6;
|
|
}
|
|
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World* CoreWorld,
|
|
const FVector& CameraPositionVoxels,
|
|
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
|
|
bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
|
|
bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,
|
|
bool bSkipUV,bool bSkipColors,
|
|
bool bShardAudit,bool bFarSurvivalAudit,bool bFarAuditBadShell,
|
|
float CameraYaw,float CameraPitch,
|
|
const std::string& BakeCachePath,
|
|
const FStandaloneMeshCacheMetadata& CacheMetadata)
|
|
{
|
|
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
|
if(CoreWorld)
|
|
{
|
|
const auto CoreTiles=CoreWorld->desiredTiles(
|
|
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
|
Desired.Tiles.clear();Desired.Tiles.reserve(CoreTiles.size());
|
|
for(const auto& Key:CoreTiles)
|
|
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
}
|
|
if(MaxLevel>=0)
|
|
Desired.Tiles.erase(std::remove_if(Desired.Tiles.begin(),Desired.Tiles.end(),
|
|
[&](const FVoxelTileKey& Tile){return Tile.Level>MaxLevel;}),Desired.Tiles.end());
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> DesiredTileSet;
|
|
DesiredTileSet.reserve(Desired.Tiles.size()*2+1);
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)DesiredTileSet.insert(Tile);
|
|
if(Desired.Tiles.size()>1000000)
|
|
{
|
|
std::fprintf(stderr,"Stream benchmark rejected an unsafe desired set of %zu tiles.\n",
|
|
Desired.Tiles.size());
|
|
return 7;
|
|
}
|
|
const int32 FullDetailExpected=static_cast<int32>(std::count_if(
|
|
Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& Tile)
|
|
{return Tile.Level<World.FullResClipLevels;}));
|
|
std::unique_ptr<FFarSurvivalAudit> FarAudit;
|
|
if(bFarSurvivalAudit)
|
|
{
|
|
if(!CoreWorld||!CoreWorld->hasFarWorld())
|
|
{std::fprintf(stderr,"Far survival audit requires a compiled core world with far-parent data.\n");return 15;}
|
|
FarAudit=std::make_unique<FFarSurvivalAudit>(World,*CoreWorld,Desired.Tiles,bFarAuditBadShell);
|
|
}
|
|
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
|
|
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
|
|
std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows;
|
|
std::array<std::vector<FHashedTile>,MaxLevels+1> TopologyHashRows;
|
|
std::array<uint64,MaxLevels+1> SamplesByLevel{};
|
|
std::array<uint64,MaxLevels+1> VerticesByLevel{};
|
|
std::array<uint64,MaxLevels+1> IndicesByLevel{};
|
|
std::array<uint64,MaxLevels+1> TilesByLevel{};
|
|
std::array<double,MaxLevels+1> WorkerSecondsByLevel{};
|
|
std::array<double,MaxLevels+1> FirstCompletionByLevel{};
|
|
std::array<double,MaxLevels+1> LastCompletionByLevel{};
|
|
std::array<uint64,MaxLevels+1> OutputBytesByLevel{};
|
|
std::array<uint64,MaxLevels+1> MeshPayloadAllocatedBytesByLevel{};
|
|
FirstCompletionByLevel.fill(std::numeric_limits<double>::infinity());
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodTiles;
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
if(Tile.Level==0&&std::abs(Tile.Coord.X-Desired.CenterChunk.X)<=1
|
|
&&std::abs(Tile.Coord.Y-Desired.CenterChunk.Y)<=1
|
|
&&std::abs(Tile.Coord.Z-Desired.CenterChunk.Z)<=1)
|
|
PlayerNeighborhoodTiles.insert(Tile);
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodReady;
|
|
auto PlayerNeighborhoodCompleted=std::chrono::steady_clock::time_point{};
|
|
std::vector<FStandaloneCpuTile> CacheTiles;
|
|
if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size());
|
|
if(CoreWorld&&!FarAudit)
|
|
{
|
|
// Tile keys and band limits are already captured in Desired. Core owns field and
|
|
// mesh generation, so discard the unused compatibility generator before streaming.
|
|
World.Generator.reset();
|
|
World.Manager.reset();
|
|
World.DefinitionStorage.clear();
|
|
World.BiomeStorage.clear();
|
|
World.OperationStorage.clear();
|
|
World.DensitySamples.clear();
|
|
}
|
|
const auto Start=std::chrono::steady_clock::now();
|
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,
|
|
bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel,
|
|
bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,CoreWorld);
|
|
const FStandaloneStreamingUpdate Update=CoreWorld
|
|
?Streamer.StartWithDesired(Desired):Streamer.Start(CameraPositionVoxels);
|
|
const double LOD0Width=static_cast<double>(2*World.ClipRadius+2)
|
|
*CHUNK_SIZE*VOXEL_SIZE/100.0;
|
|
std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s uv=%s colors=%s material_groups=%s hash=%s\n",
|
|
World.ClipRadius,LOD0Width,World.FullResClipLevels,MaxLevel<0?World.MaxClipLevel:MaxLevel,
|
|
Streamer.GetWorkerCount(),FullDetailExpected,Update.DesiredTiles,
|
|
bRoundTunnels?(bEnhancedWalls?"round-detailed":"round-legacy"):"base",
|
|
(bSkipUvAndColors||bSkipUV)?"skipped":"full",
|
|
(bSkipUvAndColors||bSkipColors)?"skipped":"full",
|
|
bSkipMaterialGroups?"skipped":"full",
|
|
(bHashDensity||!BakeCachePath.empty())?"on":"off");
|
|
std::fflush(stdout);
|
|
int32 Finished=0,FullDetailFinished=0;
|
|
bool bFullDetailTimeReported=false;
|
|
double FullDetailSeconds=0.0;
|
|
const int32 DesiredTotal=Update.DesiredTiles;
|
|
FMeshSheetAudit MeshSheetStats;
|
|
FMeshArtifactAudit MeshArtifactStats;
|
|
FCoarsePanelAudit CoarsePanelStats;
|
|
uint64 MeshAuditedTiles=0;
|
|
while(Finished<DesiredTotal)
|
|
{
|
|
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
|
if(Ready.empty())
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
for(FStandaloneCpuTile& Tile:Ready)
|
|
{
|
|
if(FarAudit)FarAudit->AuditTile(Tile);
|
|
++Finished;
|
|
const int32 Level=Tile.Key.Level;
|
|
if(PlayerNeighborhoodTiles.contains(Tile.Key))
|
|
{
|
|
PlayerNeighborhoodReady.insert(Tile.Key);
|
|
if(PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
|
|
PlayerNeighborhoodCompleted=Tile.CompletedAt;
|
|
}
|
|
if(Level>=0&&Level<=MaxLevels)
|
|
{
|
|
SamplesByLevel[Level]+=Tile.DensitySamples;
|
|
VerticesByLevel[Level]+=Tile.Vertices.size();
|
|
IndicesByLevel[Level]+=Tile.Indices.size();
|
|
++TilesByLevel[Level];
|
|
WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
|
const double CompletedSeconds=std::chrono::duration<double>(Tile.CompletedAt-Start).count();
|
|
FirstCompletionByLevel[Level]=std::min(FirstCompletionByLevel[Level],CompletedSeconds);
|
|
LastCompletionByLevel[Level]=std::max(LastCompletionByLevel[Level],CompletedSeconds);
|
|
OutputBytesByLevel[Level]+=static_cast<uint64>(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex)
|
|
+static_cast<uint64>(Tile.Indices.capacity())*sizeof(uint32)
|
|
+static_cast<uint64>(Tile.TriangleSkirtFaceMasks.capacity())*sizeof(uint8);
|
|
MeshPayloadAllocatedBytesByLevel[Level]+=Tile.MeshPayloadAllocatedBytes;
|
|
if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash});
|
|
if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash});
|
|
if(Tile.bHasPayloadHash)PayloadHashRows[Level].push_back({Tile.Key,Tile.PayloadHash});
|
|
if(Tile.bHasTopologyHash)TopologyHashRows[Level].push_back({Tile.Key,Tile.TopologyHash});
|
|
}
|
|
if(Level<World.FullResClipLevels)++FullDetailFinished;
|
|
if(bShardAudit&&Level==0)
|
|
{
|
|
const FMeshSheetAudit TileAudit=AuditThinMeshSheets(Tile,CameraPositionVoxels,
|
|
CameraYaw,CameraPitch,
|
|
MeshSheetStats.ThinSheets==0
|
|
&&MeshSheetStats.SmallComponentsInView
|
|
+MeshSheetStats.BoundarySmallComponentsInView<8);
|
|
MeshSheetStats.Components+=TileAudit.Components;
|
|
MeshSheetStats.SmallComponents+=TileAudit.SmallComponents;
|
|
MeshSheetStats.ThinSheets+=TileAudit.ThinSheets;
|
|
MeshSheetStats.SmallComponentsInView+=TileAudit.SmallComponentsInView;
|
|
MeshSheetStats.BoundarySmallComponentsInView+=TileAudit.BoundarySmallComponentsInView;
|
|
MeshSheetStats.TrianglesInThinSheets+=TileAudit.TrianglesInThinSheets;
|
|
MeshSheetStats.MinimumThicknessVoxels=std::min(MeshSheetStats.MinimumThicknessVoxels,
|
|
TileAudit.MinimumThicknessVoxels);
|
|
++MeshAuditedTiles;
|
|
}
|
|
if(bShardAudit&&Level<=1&&CoreWorld)
|
|
{
|
|
const FMeshArtifactAudit TileAudit=AuditMeshArtifacts(
|
|
Tile,*CoreWorld,std::max(1,World.SkirtCells)*(1<<Level)*0.25,
|
|
ComputeVisibleSkirtFaceMask(Tile.Key,DesiredTileSet),
|
|
MeshArtifactStats.ExposedSkirtTriangles<8,
|
|
MeshArtifactStats.RepeatedOffsetPatches<8);
|
|
MeshArtifactStats.Tiles+=TileAudit.Tiles;
|
|
MeshArtifactStats.SurfaceVertices+=TileAudit.SurfaceVertices;
|
|
MeshArtifactStats.EdgeVertices+=TileAudit.EdgeVertices;
|
|
MeshArtifactStats.MidpointVertices+=TileAudit.MidpointVertices;
|
|
MeshArtifactStats.SkirtTriangles+=TileAudit.SkirtTriangles;
|
|
MeshArtifactStats.HiddenSameLodSkirtTriangles+=TileAudit.HiddenSameLodSkirtTriangles;
|
|
MeshArtifactStats.ExposedSkirtTriangles+=TileAudit.ExposedSkirtTriangles;
|
|
MeshArtifactStats.ReverseSideSolidSkirtTriangles+=TileAudit.ReverseSideSolidSkirtTriangles;
|
|
MeshArtifactStats.RepeatedOffsetPatches+=TileAudit.RepeatedOffsetPatches;
|
|
MeshArtifactStats.FieldMismatchPatches+=TileAudit.FieldMismatchPatches;
|
|
MeshArtifactStats.NormalMismatchPatches+=TileAudit.NormalMismatchPatches;
|
|
MeshArtifactStats.GradientFallbackPatches+=TileAudit.GradientFallbackPatches;
|
|
MeshArtifactStats.GradientFallbackSamples+=TileAudit.GradientFallbackSamples;
|
|
MeshArtifactStats.DetailDisplacementSamples+=TileAudit.DetailDisplacementSamples;
|
|
MeshArtifactStats.HighDetailDisplacementSamples+=TileAudit.HighDetailDisplacementSamples;
|
|
MeshArtifactStats.DetailDisplacementPatches+=TileAudit.DetailDisplacementPatches;
|
|
MeshArtifactStats.MaximumDetailDisplacement=std::max(
|
|
MeshArtifactStats.MaximumDetailDisplacement,TileAudit.MaximumDetailDisplacement);
|
|
MeshArtifactStats.DetailGradientSamples+=TileAudit.DetailGradientSamples;
|
|
MeshArtifactStats.DetailGradientOverLimit+=TileAudit.DetailGradientOverLimit;
|
|
MeshArtifactStats.MaximumDetailGradient=std::max(
|
|
MeshArtifactStats.MaximumDetailGradient,TileAudit.MaximumDetailGradient);
|
|
MeshArtifactStats.CurvedEdgeChecks+=TileAudit.CurvedEdgeChecks;
|
|
MeshArtifactStats.CurvedEdgeRootPatches+=TileAudit.CurvedEdgeRootPatches;
|
|
MeshArtifactStats.CurvedEdgeRootBadVertices+=TileAudit.CurvedEdgeRootBadVertices;
|
|
MeshArtifactStats.MaximumCurvedEdgeRootError=std::max(
|
|
MeshArtifactStats.MaximumCurvedEdgeRootError,
|
|
TileAudit.MaximumCurvedEdgeRootError);
|
|
MeshArtifactStats.FieldEdgeChecks+=TileAudit.FieldEdgeChecks;
|
|
MeshArtifactStats.AnalyticCrossingMisses+=TileAudit.AnalyticCrossingMisses;
|
|
MeshArtifactStats.OldInt8RangeEdges+=TileAudit.OldInt8RangeEdges;
|
|
MeshArtifactStats.NormalChecks+=TileAudit.NormalChecks;
|
|
MeshArtifactStats.SumAbsSourceResidual+=TileAudit.SumAbsSourceResidual;
|
|
MeshArtifactStats.SumAnalyticEdgeShiftVoxels+=TileAudit.SumAnalyticEdgeShiftVoxels;
|
|
if(TileAudit.MaximumAnalyticEdgeShiftVoxels
|
|
>MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels)
|
|
{
|
|
MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels=TileAudit.MaximumAnalyticEdgeShiftVoxels;
|
|
MeshArtifactStats.WorstShiftPosition=TileAudit.WorstShiftPosition;
|
|
MeshArtifactStats.WorstShiftFraction=TileAudit.WorstShiftFraction;
|
|
MeshArtifactStats.WorstShiftD0=TileAudit.WorstShiftD0;
|
|
MeshArtifactStats.WorstShiftD1=TileAudit.WorstShiftD1;
|
|
MeshArtifactStats.WorstShiftExactD0=TileAudit.WorstShiftExactD0;
|
|
MeshArtifactStats.WorstShiftExactD1=TileAudit.WorstShiftExactD1;
|
|
MeshArtifactStats.WorstShiftAxis=TileAudit.WorstShiftAxis;
|
|
}
|
|
if(TileAudit.MaximumAbsSourceResidual>MeshArtifactStats.MaximumAbsSourceResidual)
|
|
{
|
|
MeshArtifactStats.MaximumAbsSourceResidual=TileAudit.MaximumAbsSourceResidual;
|
|
MeshArtifactStats.WorstResidualPosition=TileAudit.WorstResidualPosition;
|
|
}
|
|
MeshArtifactStats.SumNormalAgreement+=TileAudit.SumNormalAgreement;
|
|
MeshArtifactStats.MinimumNormalAgreement=std::min(
|
|
MeshArtifactStats.MinimumNormalAgreement,TileAudit.MinimumNormalAgreement);
|
|
MeshArtifactStats.MaximumAirSkirtDensity=std::max(
|
|
MeshArtifactStats.MaximumAirSkirtDensity,TileAudit.MaximumAirSkirtDensity);
|
|
MeshArtifactStats.MaximumPatchDominance=std::max(
|
|
MeshArtifactStats.MaximumPatchDominance,TileAudit.MaximumPatchDominance);
|
|
for(size_t Bin=0;Bin<MeshArtifactStats.OffsetHistogram.size();++Bin)
|
|
MeshArtifactStats.OffsetHistogram[Bin]+=TileAudit.OffsetHistogram[Bin];
|
|
}
|
|
if(bShardAudit&&Level>=1&&CoreWorld)
|
|
{
|
|
const FCoarsePanelAudit TileAudit=AuditCoarseSurfacePanels(
|
|
Tile,*CoreWorld,CoreWorld->configuration(),CoarsePanelStats.OffSurfacePanels<8);
|
|
CoarsePanelStats.Tiles+=TileAudit.Tiles;
|
|
CoarsePanelStats.PlanarOpenComponents+=TileAudit.PlanarOpenComponents;
|
|
CoarsePanelStats.OffSurfacePanels+=TileAudit.OffSurfacePanels;
|
|
CoarsePanelStats.Triangles+=TileAudit.Triangles;
|
|
CoarsePanelStats.MaximumSourceErrorVoxels=std::max(
|
|
CoarsePanelStats.MaximumSourceErrorVoxels,TileAudit.MaximumSourceErrorVoxels);
|
|
}
|
|
if(!BakeCachePath.empty())CacheTiles.push_back(std::move(Tile));
|
|
}
|
|
if(!bFullDetailTimeReported&&FullDetailFinished>=FullDetailExpected)
|
|
{
|
|
bFullDetailTimeReported=true;
|
|
FullDetailSeconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-Start).count();
|
|
std::vector<FHashedTile> FullRows;
|
|
std::vector<FHashedTile> FullMeshRows;
|
|
std::vector<FHashedTile> FullPayloadRows;
|
|
std::vector<FHashedTile> FullTopologyRows;
|
|
for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level)
|
|
{
|
|
FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end());
|
|
FullMeshRows.insert(FullMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
|
|
FullPayloadRows.insert(FullPayloadRows.end(),PayloadHashRows[Level].begin(),PayloadHashRows[Level].end());
|
|
FullTopologyRows.insert(FullTopologyRows.end(),TopologyHashRows[Level].begin(),TopologyHashRows[Level].end());
|
|
}
|
|
if(bHashDensity||!BakeCachePath.empty())
|
|
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
|
|
FullDetailSeconds,FullDetailFinished,FullDetailExpected,
|
|
static_cast<unsigned long long>(HashTileRows(FullRows)),
|
|
static_cast<unsigned long long>(HashTileRows(FullMeshRows)),
|
|
static_cast<unsigned long long>(HashTileRows(FullPayloadRows)),
|
|
static_cast<unsigned long long>(HashTileRows(FullTopologyRows)));
|
|
else
|
|
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n",
|
|
FullDetailSeconds,FullDetailFinished,FullDetailExpected);
|
|
std::fflush(stdout);
|
|
}
|
|
}
|
|
const double FullClipSeconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-Start).count();
|
|
if(!PlayerNeighborhoodTiles.empty()&&PlayerNeighborhoodReady.size()==PlayerNeighborhoodTiles.size())
|
|
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=%.3fs collision_cook=unavailable_in_standalone\n",
|
|
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size(),
|
|
std::chrono::duration<double>(PlayerNeighborhoodCompleted-Start).count());
|
|
else
|
|
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=missing collision_cook=unavailable_in_standalone\n",
|
|
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size());
|
|
const double TotalWorkerSeconds=std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0);
|
|
const uint64 TotalMeshPayloadAllocatedBytes=std::accumulate(
|
|
MeshPayloadAllocatedBytesByLevel.begin(),MeshPayloadAllocatedBytesByLevel.end(),uint64{0});
|
|
const uint64 TotalViewerOutputBytes=std::accumulate(
|
|
OutputBytesByLevel.begin(),OutputBytesByLevel.end(),uint64{0});
|
|
const double WorkerBusyPercent=100.0*TotalWorkerSeconds
|
|
/(std::max(1,WorkerCount)*std::max(0.001,FullClipSeconds));
|
|
std::vector<FHashedTile> AllRows;
|
|
std::vector<FHashedTile> AllMeshRows;
|
|
std::vector<FHashedTile> AllPayloadRows;
|
|
std::vector<FHashedTile> AllTopologyRows;
|
|
if(bHashDensity||!BakeCachePath.empty())
|
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
|
{
|
|
AllRows.insert(AllRows.end(),HashRows[Level].begin(),HashRows[Level].end());
|
|
AllMeshRows.insert(AllMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
|
|
AllPayloadRows.insert(AllPayloadRows.end(),PayloadHashRows[Level].begin(),PayloadHashRows[Level].end());
|
|
AllTopologyRows.insert(AllTopologyRows.end(),TopologyHashRows[Level].begin(),TopologyHashRows[Level].end());
|
|
}
|
|
if(bHashDensity||!BakeCachePath.empty())
|
|
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB tiles=%d density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
|
|
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
|
|
std::max(0.0,100.0-WorkerBusyPercent),
|
|
static_cast<double>(TotalMeshPayloadAllocatedBytes)/1048576.0,
|
|
static_cast<double>(TotalViewerOutputBytes)/1048576.0,
|
|
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)),
|
|
static_cast<unsigned long long>(HashTileRows(AllMeshRows)),
|
|
static_cast<unsigned long long>(HashTileRows(AllPayloadRows)),
|
|
static_cast<unsigned long long>(HashTileRows(AllTopologyRows)));
|
|
else
|
|
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB tiles=%d hash=off\n",
|
|
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
|
|
std::max(0.0,100.0-WorkerBusyPercent),
|
|
static_cast<double>(TotalMeshPayloadAllocatedBytes)/1048576.0,
|
|
static_cast<double>(TotalViewerOutputBytes)/1048576.0,DesiredTotal);
|
|
std::printf("[StreamCacheUse] hits=%llu requested=%d\n",
|
|
static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal);
|
|
if(bShardAudit)
|
|
std::printf("[ThinMeshShardAudit] level=0 tiles=%llu view_components=%llu small_components=%llu isolated_in_view_small=%llu boundary_in_view_small=%llu thin_sheets=%llu sheet_triangles=%llu thickness_lt=1.000 vox pca_scale=3.5x_sigma status=%s\n",
|
|
static_cast<unsigned long long>(MeshAuditedTiles),
|
|
static_cast<unsigned long long>(MeshSheetStats.Components),
|
|
static_cast<unsigned long long>(MeshSheetStats.SmallComponents),
|
|
static_cast<unsigned long long>(MeshSheetStats.SmallComponentsInView),
|
|
static_cast<unsigned long long>(MeshSheetStats.BoundarySmallComponentsInView),
|
|
static_cast<unsigned long long>(MeshSheetStats.ThinSheets),
|
|
static_cast<unsigned long long>(MeshSheetStats.TrianglesInThinSheets),
|
|
MeshSheetStats.ThinSheets==0&&MeshSheetStats.SmallComponentsInView==0?"PASS":"FAIL");
|
|
if(bShardAudit&&CoreWorld)
|
|
{
|
|
const double MidpointShare=MeshArtifactStats.EdgeVertices
|
|
?100.0*static_cast<double>(MeshArtifactStats.MidpointVertices)
|
|
/MeshArtifactStats.EdgeVertices:0.0;
|
|
const double MeanSourceResidual=MeshArtifactStats.FieldEdgeChecks
|
|
?MeshArtifactStats.SumAbsSourceResidual/MeshArtifactStats.FieldEdgeChecks:0.0;
|
|
const double MeanAnalyticEdgeShift=MeshArtifactStats.FieldEdgeChecks
|
|
?MeshArtifactStats.SumAnalyticEdgeShiftVoxels
|
|
/std::max<uint64>(1,MeshArtifactStats.FieldEdgeChecks-MeshArtifactStats.AnalyticCrossingMisses):0.0;
|
|
const double MeanNormalAgreement=MeshArtifactStats.NormalChecks
|
|
?MeshArtifactStats.SumNormalAgreement/MeshArtifactStats.NormalChecks:1.0;
|
|
std::printf("[MeshFieldFitAudit] edge_checks=%llu analytic_crossing_misses=%llu old_int8_range_edges=%llu source_residual_mean=%.4f max=%.4f vox analytic_edge_shift_mean=%.4f max=%.4f vox sampled_normal_dot_mean=%.4f min=%.4f\n",
|
|
static_cast<unsigned long long>(MeshArtifactStats.FieldEdgeChecks),
|
|
static_cast<unsigned long long>(MeshArtifactStats.AnalyticCrossingMisses),
|
|
static_cast<unsigned long long>(MeshArtifactStats.OldInt8RangeEdges),
|
|
MeanSourceResidual,MeshArtifactStats.MaximumAbsSourceResidual,
|
|
MeanAnalyticEdgeShift,MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels,
|
|
MeanNormalAgreement,MeshArtifactStats.MinimumNormalAgreement);
|
|
std::printf("[MeshFieldFitWorst] shift=%.4f vox vertex=(%.3f,%.3f,%.3f) axis=%d fraction=%.4f packed=(%.4f,%.4f) analytic=(%.4f,%.4f) max_source_residual=%.4f vox vertex=(%.3f,%.3f,%.3f)\n",
|
|
MeshArtifactStats.MaximumAnalyticEdgeShiftVoxels,
|
|
MeshArtifactStats.WorstShiftPosition[0],MeshArtifactStats.WorstShiftPosition[1],
|
|
MeshArtifactStats.WorstShiftPosition[2],MeshArtifactStats.WorstShiftAxis,
|
|
MeshArtifactStats.WorstShiftFraction,MeshArtifactStats.WorstShiftD0,
|
|
MeshArtifactStats.WorstShiftD1,MeshArtifactStats.WorstShiftExactD0,
|
|
MeshArtifactStats.WorstShiftExactD1,MeshArtifactStats.MaximumAbsSourceResidual,
|
|
MeshArtifactStats.WorstResidualPosition[0],MeshArtifactStats.WorstResidualPosition[1],
|
|
MeshArtifactStats.WorstResidualPosition[2]);
|
|
// The restored four-voxel relief creates sparse edge-curvature misses across
|
|
// the full 512-tile view. Keep the connected-block rejection strict, while
|
|
// allowing up to 4% scattered misses that do not form a visible patch.
|
|
const uint64 RootBadLimit=std::max<uint64>(64,
|
|
MeshArtifactStats.CurvedEdgeChecks*40u/1000u);
|
|
const double RootBadShare=MeshArtifactStats.CurvedEdgeChecks
|
|
?static_cast<double>(MeshArtifactStats.CurvedEdgeRootBadVertices)
|
|
/MeshArtifactStats.CurvedEdgeChecks:0.0;
|
|
const bool bRootAuditPass=MeshArtifactStats.CurvedEdgeRootPatches==0
|
|
&&MeshArtifactStats.CurvedEdgeRootBadVertices<=RootBadLimit;
|
|
std::printf("[MeshCurvedEdgeRootAudit] midpoint_edges=%llu bad_vertices=%llu bad_share=%.3f%% allowed=%llu patches=%llu max_midpoint_root_error=%.4f vox error_threshold=0.080 status=%s\n",
|
|
static_cast<unsigned long long>(MeshArtifactStats.CurvedEdgeChecks),
|
|
static_cast<unsigned long long>(MeshArtifactStats.CurvedEdgeRootBadVertices),
|
|
RootBadShare*100.0,static_cast<unsigned long long>(RootBadLimit),
|
|
static_cast<unsigned long long>(MeshArtifactStats.CurvedEdgeRootPatches),
|
|
MeshArtifactStats.MaximumCurvedEdgeRootError,
|
|
bRootAuditPass?"PASS":"FAIL");
|
|
std::printf("[MeshDetailGradientAudit] overlap_samples=%llu over_limit=%llu max_gradient=%.4f vox_per_vox limit=0.550 status=%s\n",
|
|
static_cast<unsigned long long>(MeshArtifactStats.DetailGradientSamples),
|
|
static_cast<unsigned long long>(MeshArtifactStats.DetailGradientOverLimit),
|
|
MeshArtifactStats.MaximumDetailGradient,
|
|
MeshArtifactStats.DetailGradientOverLimit==0?"PASS":"FAIL");
|
|
std::printf("[MeshSurfaceArtifactAudit] levels=0,1 tiles=%llu surface_vertices=%llu edge_vertices=%llu midpoint_band_vertices=%llu midpoint_share=%.2f%% visible_skirt_triangles=%llu hidden_same_lod_skirt_triangles=%llu exposed_skirt_triangles=%llu reverse_side_solid=%llu max_air_density=%.3f vox repeated_offset_patches=%llu field_residual_patches=%llu normal_mismatch_patches=%llu gradient_fallback_patches=%llu fallback_gradient_samples=%llu high_detail_vertices=%llu/%llu high_detail_patches=%llu max_detail_displacement=%.3f vox max_patch_dominance=%.3f status=%s\n",
|
|
static_cast<unsigned long long>(MeshArtifactStats.Tiles),
|
|
static_cast<unsigned long long>(MeshArtifactStats.SurfaceVertices),
|
|
static_cast<unsigned long long>(MeshArtifactStats.EdgeVertices),
|
|
static_cast<unsigned long long>(MeshArtifactStats.MidpointVertices),MidpointShare,
|
|
static_cast<unsigned long long>(MeshArtifactStats.SkirtTriangles),
|
|
static_cast<unsigned long long>(MeshArtifactStats.HiddenSameLodSkirtTriangles),
|
|
static_cast<unsigned long long>(MeshArtifactStats.ExposedSkirtTriangles),
|
|
static_cast<unsigned long long>(MeshArtifactStats.ReverseSideSolidSkirtTriangles),
|
|
MeshArtifactStats.MaximumAirSkirtDensity,
|
|
static_cast<unsigned long long>(MeshArtifactStats.RepeatedOffsetPatches),
|
|
static_cast<unsigned long long>(MeshArtifactStats.FieldMismatchPatches),
|
|
static_cast<unsigned long long>(MeshArtifactStats.NormalMismatchPatches),
|
|
static_cast<unsigned long long>(MeshArtifactStats.GradientFallbackPatches),
|
|
static_cast<unsigned long long>(MeshArtifactStats.GradientFallbackSamples),
|
|
static_cast<unsigned long long>(MeshArtifactStats.HighDetailDisplacementSamples),
|
|
static_cast<unsigned long long>(MeshArtifactStats.DetailDisplacementSamples),
|
|
static_cast<unsigned long long>(MeshArtifactStats.DetailDisplacementPatches),
|
|
MeshArtifactStats.MaximumDetailDisplacement,
|
|
MeshArtifactStats.MaximumPatchDominance,
|
|
MeshArtifactStats.ExposedSkirtTriangles==0&&MeshArtifactStats.RepeatedOffsetPatches==0
|
|
&&MeshArtifactStats.FieldMismatchPatches==0
|
|
&&MeshArtifactStats.NormalMismatchPatches==0
|
|
&&MeshArtifactStats.GradientFallbackPatches==0
|
|
&&MeshArtifactStats.DetailDisplacementPatches==0
|
|
&&bRootAuditPass
|
|
&&MeshArtifactStats.DetailGradientOverLimit==0?"PASS":"FAIL");
|
|
std::printf("[CoarsePanelAudit] tiles=%llu planar_open_components=%llu off_surface_panels=%llu panel_triangles=%llu max_source_error=%.3f vox status=%s\n",
|
|
static_cast<unsigned long long>(CoarsePanelStats.Tiles),
|
|
static_cast<unsigned long long>(CoarsePanelStats.PlanarOpenComponents),
|
|
static_cast<unsigned long long>(CoarsePanelStats.OffSurfacePanels),
|
|
static_cast<unsigned long long>(CoarsePanelStats.Triangles),
|
|
CoarsePanelStats.MaximumSourceErrorVoxels,
|
|
CoarsePanelStats.OffSurfacePanels==0?"PASS":"FAIL");
|
|
}
|
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
|
{
|
|
if(TilesByLevel[Level]==0)continue;
|
|
if(bHashDensity||!BakeCachePath.empty())
|
|
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB vertices=%llu indices=%llu density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
|
|
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
|
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
|
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
|
|
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
|
|
static_cast<double>(MeshPayloadAllocatedBytesByLevel[Level])/1048576.0,
|
|
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
|
|
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
|
static_cast<unsigned long long>(IndicesByLevel[Level]),
|
|
static_cast<unsigned long long>(HashTileRows(HashRows[Level])),
|
|
static_cast<unsigned long long>(HashTileRows(MeshHashRows[Level])),
|
|
static_cast<unsigned long long>(HashTileRows(PayloadHashRows[Level])),
|
|
static_cast<unsigned long long>(HashTileRows(TopologyHashRows[Level])));
|
|
else
|
|
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB vertices=%llu indices=%llu hash=off\n",
|
|
Level,static_cast<unsigned long long>(TilesByLevel[Level]),
|
|
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
|
|
FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
|
|
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
|
|
static_cast<double>(MeshPayloadAllocatedBytesByLevel[Level])/1048576.0,
|
|
static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
|
|
static_cast<unsigned long long>(VerticesByLevel[Level]),
|
|
static_cast<unsigned long long>(IndicesByLevel[Level]));
|
|
}
|
|
if(!BakeCachePath.empty())
|
|
{
|
|
std::string CacheError;
|
|
if(!FStandaloneMeshCache::Save(BakeCachePath,CacheMetadata,CacheTiles,CacheError))
|
|
{
|
|
std::fprintf(stderr,"[MeshCacheBake] failed: %s\n",CacheError.c_str());
|
|
return 8;
|
|
}
|
|
const uint64 Bytes=std::filesystem::file_size(BakeCachePath);
|
|
std::printf("[MeshCacheBake] saved %zu tiles, %.2f MiB to %s\n",CacheTiles.size(),
|
|
static_cast<double>(Bytes)/1048576.0,BakeCachePath.c_str());
|
|
}
|
|
if(Finished!=DesiredTotal||FullDetailFinished!=FullDetailExpected)return 6;
|
|
if(bShardAudit&&(MeshSheetStats.ThinSheets!=0||MeshSheetStats.SmallComponentsInView!=0))return 13;
|
|
if(bShardAudit&&CoreWorld&&(MeshArtifactStats.ExposedSkirtTriangles!=0
|
|
||MeshArtifactStats.RepeatedOffsetPatches!=0||MeshArtifactStats.FieldMismatchPatches!=0
|
|
||MeshArtifactStats.NormalMismatchPatches!=0||MeshArtifactStats.GradientFallbackPatches!=0
|
|
||MeshArtifactStats.CurvedEdgeRootPatches!=0
|
|
||MeshArtifactStats.CurvedEdgeRootBadVertices>std::max<uint64>(
|
|
64,MeshArtifactStats.CurvedEdgeChecks*40u/1000u)
|
|
||MeshArtifactStats.DetailGradientOverLimit!=0))return 13;
|
|
if(bShardAudit&&CoreWorld&&CoarsePanelStats.OffSurfacePanels!=0)return 13;
|
|
if(FarAudit)
|
|
{
|
|
FarAudit->Finalize();
|
|
FarAudit->Print();
|
|
if(!FarAudit->Passing())return 14;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int RunEditTransactionBenchmark(FStandaloneOwnerWorld& World,VoxelForgeCore::World& Shape,
|
|
const FVector& CameraPositionVoxels,int32 WorkerCount)
|
|
{
|
|
using Clock=std::chrono::steady_clock;
|
|
// Place the spell across the LOD0/LOD1 clipmap seam so one edit exercises every visible
|
|
// level it intersects, instead of timing only the center LOD0 tiles.
|
|
const double X=CameraPositionVoxels.X+3.0*CHUNK_SIZE;
|
|
const double Y=CameraPositionVoxels.Y,Z=CameraPositionVoxels.Z;
|
|
FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraPositionVoxels);
|
|
const auto CoreDesired=Shape.desiredTiles(
|
|
{CameraPositionVoxels.X,CameraPositionVoxels.Y,CameraPositionVoxels.Z});
|
|
Desired.Tiles.clear();Desired.Tiles.reserve(CoreDesired.size());
|
|
std::vector<VoxelForgeCore::TileKey> CoreKeys;CoreKeys.reserve(CoreDesired.size());
|
|
for(const auto& Key:CoreDesired)
|
|
{
|
|
Desired.Tiles.emplace_back(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
CoreKeys.push_back(Key);
|
|
}
|
|
VoxelForgeCore::EditTransaction Transaction;Transaction.sequence=1;Transaction.brushes.resize(3);
|
|
Transaction.brushes[0].shape=VoxelForgeCore::EditBrush::Shape::sphere;
|
|
Transaction.brushes[0].operation=VoxelForgeCore::EditBrush::Operation::carve;
|
|
Transaction.brushes[0].a={X,Y,Z};Transaction.brushes[0].radius=20.0;
|
|
Transaction.brushes[1].shape=VoxelForgeCore::EditBrush::Shape::roundedBox;
|
|
Transaction.brushes[1].operation=VoxelForgeCore::EditBrush::Operation::carve;
|
|
Transaction.brushes[1].a={X,Y,Z+12.0};Transaction.brushes[1].halfExtent={11.0,11.0,4.0};
|
|
Transaction.brushes[1].rounding=3.0;
|
|
Transaction.brushes[2].shape=VoxelForgeCore::EditBrush::Shape::capsule;
|
|
Transaction.brushes[2].operation=VoxelForgeCore::EditBrush::Operation::addRock;
|
|
Transaction.brushes[2].a={X-22.0,Y,Z-8.0};Transaction.brushes[2].b={X+22.0,Y,Z-8.0};
|
|
Transaction.brushes[2].radius=3.0;
|
|
const auto EditStart=Clock::now();
|
|
std::string EditError;
|
|
const auto Edit=Shape.applyEditTransaction(Transaction,CoreKeys,EditError);
|
|
if(!Edit){std::fprintf(stderr,"[EditTransaction] rejected: %s\n",EditError.c_str());return 9;}
|
|
std::vector<FVoxelTileKey> Affected;Affected.reserve(128);
|
|
std::array<uint64,MaxLevels+1> AffectedByLevel{},FinishedByLevel{};
|
|
for(const auto& LodTiles:Edit->affectedByLod)
|
|
{
|
|
for(const auto& Key:LodTiles)
|
|
{
|
|
const FVoxelTileKey Tile(FIntVector(Key.coordinate.x,Key.coordinate.y,Key.coordinate.z),Key.lod);
|
|
Affected.push_back(Tile);
|
|
if(Tile.Level>=0&&Tile.Level<=MaxLevels)++AffectedByLevel[Tile.Level];
|
|
}
|
|
}
|
|
if(Affected.empty())
|
|
{
|
|
std::fprintf(stderr,"[EditTransaction] failed: no desired tiles intersect the edit bounds.\n");
|
|
return 9;
|
|
}
|
|
FStandaloneStreamingGenerator Streamer(World,WorkerCount,false,nullptr,-1,
|
|
false,false,false,false,&Shape);
|
|
Streamer.SetDesiredTileFilter(Affected);
|
|
const auto Start=Clock::now();
|
|
const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels);
|
|
int32 Finished=0;
|
|
double WorkerSeconds=0.0;
|
|
while(Finished<Update.DesiredTiles)
|
|
{
|
|
std::vector<FStandaloneCpuTile> Ready=Streamer.TakeReady();
|
|
if(Ready.empty()){std::this_thread::sleep_for(std::chrono::milliseconds(1));continue;}
|
|
for(const FStandaloneCpuTile& Tile:Ready)
|
|
{
|
|
++Finished;WorkerSeconds+=Tile.WorkerSeconds;
|
|
if(Tile.Key.Level>=0&&Tile.Key.Level<=MaxLevels)++FinishedByLevel[Tile.Key.Level];
|
|
}
|
|
}
|
|
const double RemeshWall=std::chrono::duration<double>(Clock::now()-Start).count();
|
|
Streamer.Stop();
|
|
std::printf("[EditTransaction] operation=crater_batch transaction_count=1 brushes=3 diameter=10.0m offset_from_player=%.2fm affected_tiles=%d/%zu workers=%d cache_hits=%llu remesh_wall=%.3fs worker_sum=%.3fs edit_to_ready=%.3fs status=%s\n",
|
|
3.0*CHUNK_SIZE*VOXEL_SIZE/100.0,
|
|
Finished,Affected.size(),WorkerCount,static_cast<unsigned long long>(Streamer.GetCacheHits()),
|
|
RemeshWall,WorkerSeconds,std::chrono::duration<double>(Clock::now()-EditStart).count(),
|
|
Finished==static_cast<int32>(Affected.size())?"PASS":"FAIL");
|
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
|
if(AffectedByLevel[Level])
|
|
std::printf("[EditTransactionLevel] lod=%d tiles=%llu remeshed=%llu status=%s\n",Level,
|
|
static_cast<unsigned long long>(AffectedByLevel[Level]),
|
|
static_cast<unsigned long long>(FinishedByLevel[Level]),
|
|
AffectedByLevel[Level]==FinishedByLevel[Level]?"PASS":"FAIL");
|
|
return Finished==static_cast<int32>(Affected.size())?0:10;
|
|
}
|
|
|
|
bool MakeRegionBuildRequest(std::span<const VoxelForgeCore::TileKey> tiles,
|
|
const VoxelForgeCore::WorldConfiguration& config,int32 workers,
|
|
VoxelForgeCore::DetailVariant detail,bool roundTunnels,bool enhancedWalls,
|
|
VoxelForgeCore::ShapeBuildRequest& request,std::string& error)
|
|
{
|
|
FIntVector fineMin(MAX_int32,MAX_int32,MAX_int32),fineMax(MIN_int32,MIN_int32,MIN_int32);
|
|
FIntVector clipMin(MAX_int32,MAX_int32,MAX_int32),clipMax(MIN_int32,MIN_int32,MIN_int32);
|
|
for(const auto& key:tiles)
|
|
{
|
|
if(key.lod<0||key.lod>8){error="desired tile has an invalid LOD";return false;}
|
|
const int32 extent=CHUNK_SIZE<<key.lod;
|
|
const FIntVector origin(
|
|
static_cast<int32>(static_cast<int64>(key.coordinate.x)*extent),
|
|
static_cast<int32>(static_cast<int64>(key.coordinate.y)*extent),
|
|
static_cast<int32>(static_cast<int64>(key.coordinate.z)*extent));
|
|
for(int32 axis=0;axis<3;++axis)
|
|
{
|
|
(&clipMin.X)[axis]=std::min((&clipMin.X)[axis],(&origin.X)[axis]);
|
|
(&clipMax.X)[axis]=std::max((&clipMax.X)[axis],(&origin.X)[axis]+extent);
|
|
if(key.lod<config.fullResolutionLodCount)
|
|
{
|
|
(&fineMin.X)[axis]=std::min((&fineMin.X)[axis],(&origin.X)[axis]);
|
|
(&fineMax.X)[axis]=std::max((&fineMax.X)[axis],(&origin.X)[axis]+extent);
|
|
}
|
|
}
|
|
}
|
|
if(fineMin.X==MAX_int32||clipMin.X==MAX_int32)
|
|
{error="desired region contains no fine tiles or clip bounds";return false;}
|
|
request.minVoxel={fineMin.X,fineMin.Y,fineMin.Z};
|
|
request.maxVoxel={fineMax.X,fineMax.Y,fineMax.Z};
|
|
request.clipMinVoxel={clipMin.X,clipMin.Y,clipMin.Z};
|
|
request.clipMaxVoxel={clipMax.X,clipMax.Y,clipMax.Z};
|
|
request.workerLimit=static_cast<uint32>(std::clamp(workers,1,8));
|
|
request.detail=detail;request.roundTunnels=roundTunnels;
|
|
request.enhancedTunnelWalls=enhancedWalls;request.includeFarWorld=false;
|
|
return true;
|
|
}
|
|
|
|
int RunTilePathHashAudit(VoxelForgeCore::World& base,
|
|
const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,
|
|
const VoxelForgeCore::JobScheduler& scheduler,const FVector& camera,
|
|
int32 workers,VoxelForgeCore::DetailVariant detail,bool roundTunnels,
|
|
bool enhancedWalls)
|
|
{
|
|
const auto baseTiles=base.desiredTiles({camera.X,camera.Y,camera.Z});
|
|
const auto& config=base.configuration();
|
|
if(baseTiles.empty())
|
|
{std::fprintf(stderr,"[TilePathHashAudit] camera window is empty.\n");return 11;}
|
|
constexpr int32 TargetChunkOffset=40;
|
|
const int32 CameraChunkX=FMath::FloorToInt(camera.X/CHUNK_SIZE);
|
|
const int32 CameraChunkY=FMath::FloorToInt(camera.Y/CHUNK_SIZE);
|
|
const int32 CameraChunkZ=FMath::FloorToInt(camera.Z/CHUNK_SIZE);
|
|
const VoxelForgeCore::TileKey target{{CameraChunkX+TargetChunkOffset,
|
|
CameraChunkY,CameraChunkZ},0};
|
|
const FVector alternateCamera((target.coordinate.x+0.5)*CHUNK_SIZE,camera.Y,camera.Z);
|
|
const auto alternateTiles=base.desiredTiles(
|
|
{alternateCamera.X,alternateCamera.Y,alternateCamera.Z});
|
|
if(alternateTiles.empty())
|
|
{std::fprintf(stderr,"[TilePathHashAudit] target camera window is empty.\n");return 11;}
|
|
|
|
VoxelForgeCore::TileGenerationOptions options;options.hashDensity=true;
|
|
std::string error;
|
|
const std::array<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,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;}
|
|
|
|
VoxelForgeCore::World alternate(snapshot);
|
|
VoxelForgeCore::ShapeBuildRequest alternateRequest;
|
|
if(!MakeRegionBuildRequest(alternateTiles,config,workers,detail,roundTunnels,
|
|
enhancedWalls,alternateRequest,error)
|
|
||!alternate.compile(alternateRequest,scheduler,error))
|
|
{std::fprintf(stderr,"[TilePathHashAudit] alternate camera compile failed: %s\n",error.c_str());return 11;}
|
|
std::size_t pageTilesB=0;
|
|
auto pathB=alternate.generateTile(target,options,error);
|
|
if(!pathB){std::fprintf(stderr,"[TilePathHashAudit] path-B generation failed: %s\n",error.c_str());return 11;}
|
|
const auto Same=[&](const VoxelForgeCore::TileMesh& a,const VoxelForgeCore::TileMesh& b)
|
|
{
|
|
return a.densityHash==b.densityHash&&a.meshHash==b.meshHash
|
|
&&a.payloadHash==b.payloadHash&&a.topologyHash==b.topologyHash;
|
|
};
|
|
const int32 CameraDeltaX=static_cast<int32>(std::lround(alternateCamera.X-camera.X));
|
|
const bool pass=pageTilesA==1&&pageTilesB==0&&Same(*pathA,*pathB);
|
|
std::printf("[TilePathHashAudit] tile=(%d,%d,%d) lod=%d path_a=late_page path_b=initial_base camera_delta_vox=(%d,0,0) page_covered_tiles=(%zu,%zu) density=(0x%016llx,0x%016llx) mesh=(0x%016llx,0x%016llx) payload=(0x%016llx,0x%016llx) topology=(0x%016llx,0x%016llx) status=%s\n",
|
|
target.coordinate.x,target.coordinate.y,target.coordinate.z,target.lod,CameraDeltaX,
|
|
pageTilesA,pageTilesB,
|
|
static_cast<unsigned long long>(pathA->densityHash),
|
|
static_cast<unsigned long long>(pathB->densityHash),
|
|
static_cast<unsigned long long>(pathA->meshHash),
|
|
static_cast<unsigned long long>(pathB->meshHash),
|
|
static_cast<unsigned long long>(pathA->payloadHash),
|
|
static_cast<unsigned long long>(pathB->payloadHash),
|
|
static_cast<unsigned long long>(pathA->topologyHash),
|
|
static_cast<unsigned long long>(pathB->topologyHash),pass?"PASS":"FAIL");
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
int main(int Argc,char** Argv)
|
|
{
|
|
SetPriorityClass(GetCurrentProcess(),BELOW_NORMAL_PRIORITY_CLASS);
|
|
const bool bAllCpuAffinity=std::find_if(Argv+1,Argv+Argc,[](const char* Arg)
|
|
{return std::string_view(Arg)=="--affinity-all-cpu";})!=Argv+Argc;
|
|
const DWORD_PTR RequestedAffinity=static_cast<DWORD_PTR>(bAllCpuAffinity?0xFFFF:0xFF00);
|
|
const BOOL bAffinitySet=SetProcessAffinityMask(GetCurrentProcess(),RequestedAffinity);
|
|
DWORD_PTR ProcessAffinity=0,SystemAffinity=0;
|
|
GetProcessAffinityMask(GetCurrentProcess(),&ProcessAffinity,&SystemAffinity);
|
|
std::printf("[ProcessControls] priority_class=BelowNormal affinity_mask=0x%llX set=%s\n",
|
|
static_cast<unsigned long long>(ProcessAffinity),bAffinitySet?"true":"false");
|
|
const auto ColdProcessStart=std::chrono::steady_clock::now();
|
|
FApp App;App.DataPath=DefaultDataPath().string();App.ColdProcessStart=ColdProcessStart;
|
|
wchar_t ExecutablePath[MAX_PATH]{};
|
|
const DWORD ExecutablePathLength=GetModuleFileNameW(nullptr,ExecutablePath,MAX_PATH);
|
|
if(ExecutablePathLength>0&&ExecutablePathLength<MAX_PATH)
|
|
App.GpuFarShaderPath=std::filesystem::path(ExecutablePath).parent_path().parent_path()
|
|
/"Research"/"OwnerShapeWorld"/"gpu_first_port"/"VoxelForgeGpuMesher.hlsl";
|
|
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,StreamBenchmarkMaxLevel=-1;
|
|
int32 MaxClipLevelOverride=-1;
|
|
int32 ShapeCompareLevel=-1,ShapeCompareTileLimit=0;
|
|
std::string ShapeCompareArea="unlabeled";
|
|
bool bStreamBenchmark=false;
|
|
bool bStreamPipeline=false;
|
|
bool bProfileDetailBand=false;
|
|
bool bPassageDetailAuditOnly=false;
|
|
bool bGridStairAudit=false;
|
|
bool bTilePathHashAudit=false;
|
|
bool bShardAudit=false;
|
|
bool bFarSurvivalAudit=false;
|
|
bool bFarAuditBadShell=false;
|
|
bool bStreamHash=false;
|
|
bool bSkipUvAndColors=false;
|
|
bool bSkipUV=false;
|
|
bool bSkipColors=false;
|
|
bool bSkipMaterialGroups=false;
|
|
bool bEditBenchmark=false;
|
|
bool bRoundWallCompare=false;
|
|
bool bCheckOwnerReliefBands=false;
|
|
int32 RoundCompareTileCount=16;
|
|
std::string BakeMeshCachePath;
|
|
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=="--no-vsync")App.bDisableVSync=true;
|
|
else if(Arg=="--blocking-startup-present")App.bForceBlockingStartupPresent=true;
|
|
else if(Arg=="--cpu-frame-before-gpu")App.bPresentCpuFrameBeforeGpu=true;
|
|
else if(Arg=="--hold-cpu-frame-during-gpu-warmup")
|
|
App.bHoldCpuFrameDuringGpuWarmup=true,App.bPresentCpuFrameBeforeGpu=true;
|
|
else if(Arg=="--gpu-skirt-draw-audit")App.bAuditGpuSkirtDrawArgs=true;
|
|
else if(Arg=="--gpu-skirts-disabled")App.bGpuSkirtEmitEnabled=false;
|
|
else if(Arg=="--gpu-connectors-disabled")App.bGpuConnectorRepairEnabled=false;
|
|
else if(Arg=="--profile-attribution")App.bProfileAttribution=true;
|
|
else if(Arg=="--profile-detail-band")bProfileDetailBand=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=="--shape-world")App.bShapeWorld=true;
|
|
else if(Arg=="--gpu-far")
|
|
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bShapeWorld=true,App.bForceGenerate=true,
|
|
App.bPresentCpuFrameBeforeGpu=true,App.bHoldCpuFrameDuringGpuWarmup=true;
|
|
else if(Arg=="--gpu-far-prewarm")
|
|
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bShapeWorld=true,
|
|
App.bForceGenerate=true,App.bPresentCpuFrameBeforeGpu=true,
|
|
App.bHoldCpuFrameDuringGpuWarmup=true;
|
|
else if(Arg=="--gpu-far-warmup-resources-only")
|
|
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bGpuFarWarmupResourcesOnly=true,
|
|
App.bShapeWorld=true,App.bForceGenerate=true,App.bPresentCpuFrameBeforeGpu=true,
|
|
App.bHoldCpuFrameDuringGpuWarmup=true;
|
|
else if(Arg=="--gpu-far-warmup-normal-priority")
|
|
App.bGpuFarWarmupNormalPriority=true;
|
|
else if(Arg=="--gpu-far-warmup-before-workers")
|
|
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bGpuFarWarmupBeforeWorkers=true,
|
|
App.bShapeWorld=true,App.bForceGenerate=true,App.bPresentCpuFrameBeforeGpu=true,
|
|
App.bHoldCpuFrameDuringGpuWarmup=true;
|
|
else if(Arg=="--gpu-far-warmup-kernel-fences")
|
|
App.bGpuFarWarmupKernelFences=true;
|
|
else if(Arg=="--gpu-far-warmup-only")
|
|
App.bGpuFarEnabled=true,App.bGpuFarShaderPrewarm=true,App.bGpuFarWarmupOnly=true,
|
|
App.bShapeWorld=true,App.bForceGenerate=true;
|
|
else if(Arg.starts_with("--gpu-far-warmup-entry="))
|
|
App.GpuWarmupEntryFilter=std::string(Arg.substr(std::string_view("--gpu-far-warmup-entry=").size()));
|
|
else if(Arg=="--affinity-all-cpu"){}
|
|
else if(Arg=="--gpu-far-no-present-hold")
|
|
App.bDisableGpuWarmupPresentHold=true;
|
|
else if(Arg=="--gpu-far-no-cold-drain")App.bDisableGpuColdStartDrain=true;
|
|
else if(Arg=="--gpu-far-timeline")App.bGpuFarTimelineEnabled=true;
|
|
else if(Arg=="--gpu-far-no-prewarm")
|
|
App.bGpuFarShaderPrewarm=false;
|
|
else if(Arg=="--cpu-far")App.bGpuFarEnabled=false,App.bShapeWorld=true,App.bForceGenerate=true;
|
|
else if(Arg=="--gpu-far-audit")
|
|
{App.bGpuFarEnabled=true;App.bGpuFarAuditEnabled=true;App.bShapeWorld=true;App.bForceGenerate=true;App.bQuitAfterClipmap=true;}
|
|
else if(Arg=="--shape-audit")App.bShapeWorld=true,App.bShapeAuditOnly=true;
|
|
else if(Arg=="--grid-stair-audit")bGridStairAudit=true,App.bShapeWorld=true;
|
|
else if(Arg=="--tile-path-hash-audit")bTilePathHashAudit=true,App.bShapeWorld=true;
|
|
else if(Arg=="--passage-wall-detail-audit")
|
|
{App.bShapeWorld=true;App.bShapeAuditOnly=true;bPassageDetailAuditOnly=true;}
|
|
else if(Arg=="--shape-compare")App.bShapeWorld=true,App.bShapeCompare=true;
|
|
else if(Arg.starts_with("--shape-compare-level="))
|
|
ShapeCompareLevel=std::atoi(std::string(Arg.substr(std::string_view("--shape-compare-level=").size())).c_str());
|
|
else if(Arg.starts_with("--shape-compare-tile-limit="))
|
|
ShapeCompareTileLimit=std::max(0,std::atoi(std::string(Arg.substr(std::string_view("--shape-compare-tile-limit=").size())).c_str()));
|
|
else if(Arg.starts_with("--shape-compare-area="))
|
|
ShapeCompareArea=std::string(Arg.substr(std::string_view("--shape-compare-area=").size()));
|
|
else if(Arg=="--owner-relief-band-audit")bCheckOwnerReliefBands=true;
|
|
else if(Arg.starts_with("--shape-detail="))
|
|
{
|
|
const std::string_view Value=Arg.substr(15);
|
|
if(Value=="smooth")App.ShapeDetailVariant=VoxelForgeCore::DetailVariant::smooth;
|
|
else if(Value=="per-voxel")App.ShapeDetailVariant=VoxelForgeCore::DetailVariant::perVoxel;
|
|
else if(Value!="coherent"){std::fprintf(stderr,"Invalid --shape-detail value: %.*s\n",static_cast<int>(Value.size()),Value.data());return 2;}
|
|
}
|
|
else if(Arg=="--round-tunnels")App.bRoundTunnels=true;
|
|
else if(Arg=="--round-tunnels-legacy")
|
|
{App.bRoundTunnels=true;App.bRoundTunnelEnhanced=false;}
|
|
else if(Arg=="--stream-benchmark")bStreamBenchmark=true;
|
|
else if(Arg=="--stream-pipeline")
|
|
{bStreamPipeline=true;bStreamBenchmark=true;App.bShapeWorld=true;App.bForceGenerate=true;}
|
|
else if(Arg=="--edit-benchmark")
|
|
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
|
|
else if(Arg=="--stream-hash")bStreamHash=true;
|
|
else if(Arg=="--skip-uv-colors")bSkipUvAndColors=true;
|
|
else if(Arg=="--skip-uv")bSkipUV=true;
|
|
else if(Arg=="--skip-colors")bSkipColors=true;
|
|
else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true;
|
|
else if(Arg=="--shard-audit")bShardAudit=true;
|
|
else if(Arg=="--far-survival-audit")bFarSurvivalAudit=true,App.bShapeWorld=true;
|
|
else if(Arg=="--far-audit-bad-shell")
|
|
{bFarAuditBadShell=true;bFarSurvivalAudit=true;App.bShapeWorld=true;}
|
|
else if(Arg=="--compare-round-walls")
|
|
{bRoundWallCompare=true;App.bRoundTunnels=true;}
|
|
else if(Arg.starts_with("--round-compare-tiles="))
|
|
RoundCompareTileCount=std::max(1,std::atoi(std::string(Arg.substr(22)).c_str()));
|
|
else if(Arg=="--force-generate")App.bForceGenerate=true;
|
|
else if(Arg.starts_with("--bake-mesh-cache="))
|
|
{BakeMeshCachePath=std::string(Arg.substr(18));App.bForceGenerate=true;}
|
|
else if(Arg.starts_with("--stream-max-level="))
|
|
StreamBenchmarkMaxLevel=std::atoi(std::string(Arg.substr(19)).c_str());
|
|
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.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="))
|
|
{
|
|
const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str());
|
|
App.WorkerCount=std::clamp(Parsed,1,8);
|
|
}
|
|
else if(Arg.starts_with("--clip-radius="))
|
|
App.ClipRadius=std::clamp(std::atoi(std::string(Arg.substr(14)).c_str()),1,64);
|
|
else if(Arg.starts_with("--capture-tile-limit="))
|
|
App.CaptureTileLimit=std::clamp(std::atoi(
|
|
std::string(Arg.substr(std::string_view("--capture-tile-limit=").size())).c_str()),1,4096);
|
|
else if(Arg.starts_with("--capture-only-lod="))
|
|
App.CaptureOnlyLod=std::clamp(std::atoi(
|
|
std::string(Arg.substr(std::string_view("--capture-only-lod=").size())).c_str()),0,8);
|
|
else if(Arg.starts_with("--capture-only-tile="))
|
|
{
|
|
const std::string Values(Arg.substr(std::string_view("--capture-only-tile=").size()));
|
|
if(std::sscanf(Values.c_str(),"%d,%d,%d,%d",&App.CaptureOnlyTileX,
|
|
&App.CaptureOnlyTileY,&App.CaptureOnlyTileZ,&App.CaptureOnlyTileLod)==4)
|
|
App.bCaptureOnlyTile=true;
|
|
else {std::fprintf(stderr,"Invalid --capture-only-tile value: %s\n",Values.c_str());return 2;}
|
|
}
|
|
else if(Arg.starts_with("--draw-only-tile="))
|
|
{
|
|
const std::string Values(Arg.substr(std::string_view("--draw-only-tile=").size()));
|
|
if(std::sscanf(Values.c_str(),"%d,%d,%d,%d",&App.DrawOnlyTileX,
|
|
&App.DrawOnlyTileY,&App.DrawOnlyTileZ,&App.DrawOnlyTileLod)==4)
|
|
App.bDrawOnlyTile=true;
|
|
else {std::fprintf(stderr,"Invalid --draw-only-tile value: %s\n",Values.c_str());return 2;}
|
|
}
|
|
else if(Arg.starts_with("--capture-output-slot="))
|
|
App.CaptureOutputSlot=std::max(-1,std::atoi(
|
|
std::string(Arg.substr(std::string_view("--capture-output-slot=").size())).c_str()));
|
|
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);
|
|
}
|
|
if(App.bCaptureOnlyTile&&App.bGpuFarEnabled)
|
|
{
|
|
// A single far-tile probe may have no CPU fallback batch to present.
|
|
// Let it submit directly instead of waiting for the normal CPU-first
|
|
// presentation gate that requires a nonempty CPU batch.
|
|
App.bPresentCpuFrameBeforeGpu=false;
|
|
App.bHoldCpuFrameDuringGpuWarmup=false;
|
|
}
|
|
if(App.bDisableGpuWarmupPresentHold)App.bHoldCpuFrameDuringGpuWarmup=false;
|
|
App.bProfileDetailBand=bProfileDetailBand;
|
|
std::string Error;
|
|
std::printf("[ColdTimeline] stage=owner_data_load begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
App.World=std::make_shared<FStandaloneOwnerWorld>();
|
|
{
|
|
std::ifstream JsonFile(App.DataPath,std::ios::binary);
|
|
if(!JsonFile){std::fprintf(stderr,"Owner data load failed: cannot open %s\n",App.DataPath.c_str());return 2;}
|
|
std::string OwnerJsonText{std::istreambuf_iterator<char>(JsonFile),std::istreambuf_iterator<char>{}};
|
|
if(!JsonFile&& !JsonFile.eof())
|
|
{std::fprintf(stderr,"Owner data load failed: read error for %s\n",App.DataPath.c_str());return 2;}
|
|
if(!VFJson::Parser(std::move(OwnerJsonText)).Parse(App.OwnerJsonRoot,Error))
|
|
{std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
|
|
}
|
|
if(!StandaloneOwnerLoader::Resolve(App.OwnerJsonRoot,*App.World,Error))
|
|
{std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
|
|
std::printf("[ColdTimeline] stage=owner_data_load end_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
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;}
|
|
const int32 ExportClipRadius=App.World->ClipRadius;
|
|
const int32 ExportMaxClipLevel=App.World->MaxClipLevel;
|
|
App.World->ClipRadius=App.ClipRadius;
|
|
App.World->Settings->ClipRadius=App.ClipRadius;
|
|
if(App.bRoundTunnels&&!PrepareStandaloneDensityWorld(*App.World,App.Seed,
|
|
App.bRoundTunnels,App.bRoundTunnelEnhanced,Error))
|
|
{std::fprintf(stderr,"Standalone world setup failed: %s\n",Error.c_str());return 2;}
|
|
if(MaxClipLevelOverride>=0)
|
|
{
|
|
App.MaxClipLevelOverride=MaxClipLevelOverride;
|
|
App.World->MaxClipLevel=MaxClipLevelOverride;
|
|
App.World->Settings->MaxClipLevel=MaxClipLevelOverride;
|
|
std::printf("Standalone max clip level override: exported=%d -> max_level=%d.\n",
|
|
ExportMaxClipLevel,MaxClipLevelOverride);
|
|
}
|
|
const bool bNeedFarShapeWorld=bStreamBenchmark
|
|
?(StreamBenchmarkMaxLevel<0||StreamBenchmarkMaxLevel>=App.World->FullResClipLevels)
|
|
:(App.bShapeCompare?ShapeCompareLevel>=App.World->FullResClipLevels
|
|
:(BenchmarkLevel>=App.World->FullResClipLevels));
|
|
if(App.bShapeWorld&&(App.bShapeAuditOnly||bGridStairAudit||bTilePathHashAudit||App.bShapeCompare||bStreamBenchmark||BenchmarkLevel>=0)
|
|
&&!bStreamPipeline
|
|
&&!App.BuildShapeWorldForCamera(Error,bNeedFarShapeWorld))
|
|
{std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;}
|
|
if(bStreamPipeline)
|
|
{
|
|
App.Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(App.OwnerJsonRoot,
|
|
App.Seed,App.ClipRadius,App.MaxClipLevelOverride,App.bRoundTunnels,
|
|
App.bRoundTunnelEnhanced,Error);
|
|
if(!App.Snapshot)
|
|
{std::fprintf(stderr,"Shape interval build failed: %s\n",Error.c_str());return 2;}
|
|
}
|
|
if(App.bShapeAuditOnly)
|
|
{
|
|
if(bPassageDetailAuditOnly)
|
|
return App.ShapeWorld->auditSurfaceFamilies(true)?0:8;
|
|
const bool bSurfacePassing=App.ShapeWorld->auditViewSurface(
|
|
{App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0},
|
|
App.Yaw,App.Pitch);
|
|
return App.ShapeWorld->auditDetailPassing()&&bSurfacePassing?0:7;
|
|
}
|
|
if(bGridStairAudit)
|
|
return App.ShapeWorld->auditGridStairIntegrity()?0:9;
|
|
if(bTilePathHashAudit)
|
|
return RunTilePathHashAudit(*App.ShapeWorld,App.Snapshot,App.CoreScheduler,
|
|
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
|
|
App.WorkerCount,App.ShapeDetailVariant,App.bRoundTunnels,App.bRoundTunnelEnhanced);
|
|
if(App.bShapeCompare)
|
|
return RunOwnerShapeComparison(*App.World,*App.ShapeWorld,App.Position,App.Yaw,App.Pitch,App.WorkerCount,
|
|
ShapeCompareLevel,ShapeCompareTileLimit,ShapeCompareArea,bCheckOwnerReliefBands);
|
|
if(bRoundWallCompare)
|
|
return RunRoundWallComparison(*App.World,App.Position,RoundCompareTileCount);
|
|
if(bStreamBenchmark)
|
|
{
|
|
if(bStreamPipeline)
|
|
return RunPipelinedStreamingBenchmark(App.World,App.Snapshot,
|
|
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
|
|
App.WorkerCount,StreamBenchmarkMaxLevel,App.ShapeDetailVariant,App.CoreScheduler,
|
|
bStreamHash||!BakeMeshCachePath.empty(),bSkipUvAndColors,bSkipMaterialGroups,
|
|
bSkipUV,bSkipColors,ColdProcessStart);
|
|
if(bEditBenchmark)
|
|
return RunEditTransactionBenchmark(*App.World,*App.ShapeWorld,
|
|
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),App.WorkerCount);
|
|
const double FullDetailWidth=static_cast<double>(2*App.ClipRadius+2)
|
|
*CHUNK_SIZE*VOXEL_SIZE/100.0;
|
|
std::printf("Standalone clip override: exported radius=%d (112 m cube) -> radius=%d (%.0f m LOD0 cube), voxel size=25 cm.\n",
|
|
ExportClipRadius,App.ClipRadius,FullDetailWidth);
|
|
if(!App.bForceGenerate&&!App.bShapeWorld)App.LoadMeshCache();
|
|
return RunStreamingBenchmark(*App.World,App.ShapeWorld.get(),
|
|
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
|
|
App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels,
|
|
App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr,
|
|
bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bSkipUV,bSkipColors,bShardAudit,
|
|
bFarSurvivalAudit,bFarAuditBadShell,App.Yaw,App.Pitch,
|
|
BakeMeshCachePath,App.MakeMeshCacheMetadata());
|
|
}
|
|
if(BenchmarkLevel>=0)
|
|
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
|
|
App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash,App.ShapeWorld.get());
|
|
App.World.reset();
|
|
|
|
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
|
|
Class.hCursor=LoadCursor(nullptr,IDC_ARROW);RegisterClassA(&Class);
|
|
RECT Rect{0,0,App.Width,App.Height};AdjustWindowRect(&Rect,WS_OVERLAPPEDWINDOW,FALSE);
|
|
App.Window=CreateWindowA(Class.lpszClassName,"VoxelForge standalone world",WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,
|
|
Rect.right-Rect.left,Rect.bottom-Rect.top,nullptr,nullptr,Class.hInstance,nullptr);
|
|
GApp=&App;
|
|
const auto DeviceInitStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=viewer_device_init begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(DeviceInitStart-ColdProcessStart).count());
|
|
if(!App.Window||!App.InitializeDevice()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","VoxelForge standalone",MB_ICONERROR);return 3;}
|
|
std::printf("[ColdTimeline] stage=viewer_device_init end_ms=%.3f duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-DeviceInitStart).count());
|
|
if(!App.bGpuFarWarmupOnly)
|
|
{
|
|
ShowWindow(App.Window,SW_SHOW);UpdateWindow(App.Window);
|
|
std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; G toggles legacy/detailed round walls; H toggles CPU/GPU far tiles; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swaps base/round tunnels; V swaps owner density/shape intervals; Esc quit.\n");
|
|
std::printf("Owner data: %s; starting at first-strate open-space sample (%.2f, %.2f, %.2f) m.\n",
|
|
App.DataPath.c_str(),App.Position.X,App.Position.Y,App.Position.Z);
|
|
}
|
|
std::fflush(stdout);
|
|
if(App.bGpuFarWarmupOnly)
|
|
{
|
|
const auto WarmupOnlyStart=std::chrono::steady_clock::now();
|
|
std::string WarmupError;
|
|
if(!VoxelForgeGpuFarViewer::QueueFarShaderWarmup(App.Device,App.Context,App.GpuFarShaderPath,
|
|
true,App.GpuWarmupEntryFilter,App.bGpuFarWarmupKernelFences,WarmupError))
|
|
{std::fprintf(stderr,"[GpuShaderWarmupError] queue: %s\n",WarmupError.c_str());return 5;}
|
|
if(!VoxelForgeGpuFarViewer::CompleteFarShaderWarmup(App.Device,App.Context,
|
|
App.GpuFarShaderPath,WarmupError))
|
|
{std::fprintf(stderr,"[GpuShaderWarmupError] completion: %s\n",WarmupError.c_str());return 5;}
|
|
uint64 WarmupDxgiUsage=0,WarmupDxgiBudget=0;
|
|
if(VoxelForgeGpuFarViewer::QueryFarGpuMemory(App.Device,App.Context,App.GpuFarShaderPath,
|
|
WarmupDxgiUsage,WarmupDxgiBudget,WarmupError))
|
|
std::printf("[GpuFarMemory] dxgi_local_sampled_peak_mb=%.3f budget_mb=%.3f\n",
|
|
static_cast<double>(WarmupDxgiUsage)/1048576.0,
|
|
static_cast<double>(WarmupDxgiBudget)/1048576.0);
|
|
else
|
|
std::printf("[GpuFarMemory] dxgi_local_sampled_peak_mb=unavailable reason=%s\n",
|
|
WarmupError.c_str());
|
|
std::printf("[GpuShaderWarmupOnlyComplete] entry=%s total_ms=%.3f\n",
|
|
App.GpuWarmupEntryFilter.empty()?"all":App.GpuWarmupEntryFilter.c_str(),
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-WarmupOnlyStart).count());
|
|
return 0;
|
|
}
|
|
App.StartGpuShaderWarmupAsync();
|
|
if(App.bGpuFarWarmupBeforeWorkers)
|
|
{
|
|
if(App.GpuShaderWarmupThread.joinable())App.GpuShaderWarmupThread.join();
|
|
if(!App.bGpuShaderWarmupQueueSucceeded)
|
|
{std::fprintf(stderr,"[GpuShaderWarmupError] queue: %s\n",App.GpuShaderWarmupQueueError.c_str());return 5;}
|
|
std::string WarmupError;
|
|
if(!VoxelForgeGpuFarViewer::CompleteFarShaderWarmup(App.Device,App.Context,
|
|
App.GpuFarShaderPath,WarmupError))
|
|
{std::fprintf(stderr,"[GpuShaderWarmupError] completion: %s\n",WarmupError.c_str());return 5;}
|
|
std::printf("[GpuShaderWarmupBeforeWorkers] completed_before_generation_workers=true\n");
|
|
}
|
|
App.StartGeneration();
|
|
MSG Message{};
|
|
while(Message.message!=WM_QUIT)
|
|
{
|
|
if(PeekMessage(&Message,nullptr,0,0,PM_REMOVE)){TranslateMessage(&Message);DispatchMessage(&Message);}
|
|
else App.Draw();
|
|
}
|
|
GApp=nullptr;
|
|
return 0;
|
|
}
|