Luna, M1 round 1 of 4 (plan in Standalone/ASSEMBLER_M1_REPORT.md): - Versioned fixed-scale world manifest (the shipped plan format) and the Core block contract: point evaluation, interval-or-unknown, influence bound, detail envelope, channels. - Interval audit: 256 boxes / 6,912 samples, 0 missed, 0 unknown; a deliberately false bound is detected (negative control). - Native Heightfield2D with biome input: 4,096-point hash fae2721c052aec5c identical at W1/W8/W16; point queries 2.059 ms vs 62.115 ms legacy -- but NOT a match yet (mean density error 274 voxels, 2,261 sign mismatches vs the legacy surface). 45 mesh-free Core queries match direct evaluation exactly. Existing cave world unchanged (walkability, 64/64 tiles). - Open: collision readiness, a valid full-view timing (viewer captures were occluded/empty; top-down height maps are the only usable pair). Also carries the design doc's owner decisions (fixed water; player edits unrestricted, route law for automatic changes only) and three plugin files + BuildHost scripts that earlier round commits had missed (also fixed on unreal/core-integration as 2574b56). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
8598 lines
513 KiB
C++
8598 lines
513 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 <d3d11_4.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 <mutex>
|
|
#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;
|
|
ID3D11Buffer* ReadbackCopyVertexBuffer=nullptr;
|
|
ID3D11Buffer* ReadbackCopyIndexBuffer=nullptr;
|
|
uint32 ReadbackCopyIndexCount=0;
|
|
ID3D11Query* CompletionEvent=nullptr;
|
|
VoxelForgeGpuFarViewer::GpuTimingQueries TimingQueries{};
|
|
uint64 VertexCapacityBytes=0;
|
|
uint32 OutputSlot=0,IndirectArgsByteOffset=0,BatchIndex=UINT32_MAX;
|
|
uint32 RequiredSurfaceVertexCount=0;
|
|
int32 InputCells=0,InputStep=0;
|
|
uint64 ProtectedSamples=0,ProtectedThinSamples=0,ProtectedSolidSamples=0;
|
|
uint32 NearProxySamples=0,ConnectorRings=0,ConnectorVertices=0;
|
|
uint8 VisibleSkirtFaceMask=0;
|
|
bool bNearDetailInput=false;
|
|
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;
|
|
uint64 GpuStartTimestamp=0,GpuEndTimestamp=0;
|
|
double GpuDispatchMs=0.0;
|
|
std::chrono::steady_clock::time_point PreparedAt{};
|
|
std::chrono::steady_clock::time_point SubmitStartedAt{};
|
|
std::chrono::steady_clock::time_point SubmittedAt{};
|
|
std::chrono::steady_clock::time_point CompletedAt{};
|
|
std::chrono::steady_clock::time_point ConsumedAt{};
|
|
bool bGpuComplete=false,bTimingResolved=false;
|
|
};
|
|
struct FGpuFarPanelOutputRange
|
|
{
|
|
uint32 BaseVertex=0;
|
|
uint32 CapacityVertices=0;
|
|
};
|
|
struct FGpuFarPanelFreeRange
|
|
{
|
|
uint32 BaseVertex=0;
|
|
uint32 CapacityVertices=0;
|
|
};
|
|
|
|
enum class EGpuWarmupLaunchPolicy
|
|
{
|
|
Auto,
|
|
ForceDeferred,
|
|
ForceEager
|
|
};
|
|
|
|
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
|
|
{
|
|
struct FPreparedGpuFarInputResult
|
|
{
|
|
FVoxelTileKey Key;
|
|
std::optional<VoxelForgeCore::FarFieldTileInput> Input;
|
|
std::string Error;
|
|
std::chrono::steady_clock::time_point FinishedAt{};
|
|
uint64 FinishedFrame=0;
|
|
};
|
|
struct FGpuFarPreparationBatch
|
|
{
|
|
std::once_flag StartOnce;
|
|
std::chrono::steady_clock::time_point DispatchAt{};
|
|
std::chrono::steady_clock::time_point PrepareStart{};
|
|
std::chrono::steady_clock::time_point CpuReadyAt{};
|
|
std::atomic<std::size_t> Remaining{0};
|
|
std::atomic<std::size_t> Failed{0};
|
|
std::atomic<uint64> WorkerNanoseconds{0};
|
|
std::atomic<bool> bCancelled{false};
|
|
uint64 Generation=0;
|
|
std::size_t TileCount=0;
|
|
std::size_t SharedWorkerCount=0;
|
|
};
|
|
|
|
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;
|
|
ID3D11DepthStencilState* DepthDisabledAuditState=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 bWorldAssemblerPrototype=false;
|
|
bool bWorldAssemblerAuditOnly=false;
|
|
bool bShapePipelineActive=false;
|
|
bool bNeighborhoodReadyReported=false;
|
|
bool bShapeAuditOnly=false;
|
|
bool bShapeCompare=false;
|
|
bool bWalkabilityAuditOnly=false;
|
|
bool bFarShapeStagePending=false;
|
|
bool bGpuFarEnabled=false;
|
|
bool bGpuFarPipelineInitialized=false;
|
|
std::thread GpuPipelineStartupThread;
|
|
bool bGpuPipelineStartupAttempted=false,bGpuPipelineStartupSucceeded=false;
|
|
bool bGpuPipelineStartupWasDeferred=false;
|
|
std::string GpuPipelineStartupError;
|
|
bool bDisableGpuColdStartDrain=false;
|
|
bool bGpuFarAuditEnabled=false;
|
|
bool bGpuFarShaderPrewarm=false,bDisableGpuWarmupPresentHold=false;
|
|
bool bGpuFarWarmupResourcesOnly=false;
|
|
bool bGpuFarWarmupNormalPriority=false,bGpuFarWarmupBeforeWorkers=false;
|
|
EGpuWarmupLaunchPolicy GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::Auto;
|
|
bool bGpuWarmupDeferredUntilFirstView=false,bGpuWarmupDeferredStarted=false;
|
|
bool bGpuWarmupCacheKeyAvailable=false;
|
|
bool bGpuWarmupCacheWriteDisabled=false;
|
|
std::filesystem::path GpuWarmupStatePath;
|
|
std::string GpuWarmupCacheKey,GpuWarmupCacheIdentity;
|
|
bool bGpuFarWarmupKernelFences=false;
|
|
bool bGpuFarWarmupOnly=false;
|
|
std::string GpuWarmupEntryFilter;
|
|
std::thread GpuShaderWarmupThread;
|
|
std::thread GpuWarmupPolicyThread;
|
|
std::atomic<bool> bGpuWarmupPolicyResolutionFinished{false};
|
|
bool bGpuWarmupPolicyResolutionStarted=false;
|
|
bool bGpuWarmupLaunchPolicyResolved=false;
|
|
bool bGpuWarmupCacheMarkerMatches=false;
|
|
double GpuWarmupPolicyResolutionMs=0.0;
|
|
std::shared_ptr<FGpuFarPreparationBatch> GpuFarPreparationState;
|
|
std::mutex GpuFarPreparedResultsMutex;
|
|
std::deque<FPreparedGpuFarInputResult> GpuFarPreparedResults;
|
|
std::atomic<uint64> GpuFarPreparationGeneration{0};
|
|
std::atomic<bool> bGpuFarPreparationFinished{false};
|
|
bool bGpuFarPreparationStarted=false,bGpuFarInputsFinalized=false;
|
|
bool bGpuFarPreparationDeferredReported=false;
|
|
bool bGpuFarFirstPreparedInputReported=false;
|
|
bool bGpuWarmupCompleted=false,bGpuFarTimelineEnabled=false;
|
|
bool bGpuFarNoPresent=false,bGpuFarNoPresentReported=false;
|
|
bool bGpuWarmupRoutePending=false;
|
|
bool bGpuShaderWarmupQueueSucceeded=false;
|
|
std::atomic<bool> bGpuShaderWarmupQueueFinished{false};
|
|
bool bGpuWarmupContextProtected=false;
|
|
std::string GpuShaderWarmupQueueError;
|
|
std::chrono::steady_clock::time_point GpuShaderWarmupQueueStartedAt{},GpuShaderWarmupQueueFinishedAt{};
|
|
std::chrono::steady_clock::time_point CpuReadyFrameBeforeGpuAt{};
|
|
bool bGpuFarAuditReported=false;
|
|
bool bGpuNearSkirtsEnabled=true,bGpuConnectorRepairEnabled=true; // The CPU fallback loses remote skirt coverage.
|
|
bool bDisableVSync=false;
|
|
bool bForceBlockingStartupPresent=false;
|
|
bool bPresentCpuFrameBeforeGpu=false;
|
|
bool bCpuReadyFramePresentedBeforeGpu=false;
|
|
bool bQuitAfterClipmapClosePosted=false;
|
|
bool bHoldCpuFrameDuringGpuWarmup=false,bGpuWarmupHoldReported=false;
|
|
bool bGpuWarmupReleaseReported=false,bGpuWarmupPresentSkipReported=false;
|
|
uint64 GpuWarmupHeldPresents=0;
|
|
bool bAuditGpuSkirtDrawArgs=false,bGpuSkirtDrawAuditReported=false;
|
|
bool bGpuFarDirectDrawAudit=false;
|
|
bool bGpuFarReadbackCopyDrawAudit=false;
|
|
bool bGpuFarMeterCopyDrawAudit=false;
|
|
bool bGpuFarCpuMeshDrawAudit=false;
|
|
bool bGpuFarDepthDisabledAudit=false,bGpuFarDepthDisabledAuditReported=false;
|
|
bool bDrawOnlyCpuTriangleAuditReported=false;
|
|
ID3D11Buffer* GpuSkirtArgsStaging=nullptr;
|
|
uint64 GpuSkirtDrawAuditTiles=0,GpuSkirtDrawAuditZeroCounts=0,
|
|
GpuSkirtDrawAuditOverflow=0,GpuSkirtDrawAuditBadStartIndex=0,
|
|
GpuSkirtDrawAuditBadSurfaceArgs=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 GpuFarNearTilesSubmitted=0,GpuFarFarTilesSubmitted=0;
|
|
uint64 GpuFarPreparedNearTileInputs=0,GpuFarPreparedFarTileInputs=0;
|
|
uint64 GpuFarNearLatticeSamples=0,GpuFarNearPackedCandidateSamples=0;
|
|
uint64 GpuFarNearPackedSkippedSamples=0,GpuFarProtectedRouteSamples=0;
|
|
uint64 GpuFarProtectedThinSamples=0,GpuFarProtectedSolidSamples=0;
|
|
uint64 GpuFarProtectedSealSamples=0,GpuFarProtectedOverlaySamples=0;
|
|
uint64 PresentCallCount=0;
|
|
uint64 StartupNonBlockingPresentCalls=0,StartupNonBlockingPresentSkips=0;
|
|
double GpuFarGpuDispatchMs=0.0;
|
|
double GpuFarGpuDispatchMaxMs=0.0;
|
|
FVoxelTileKey GpuFarGpuDispatchMaxKey{};
|
|
bool bGpuFarGpuDispatchMaxKeyValid=false;
|
|
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;
|
|
uint32 GpuFarOutputPoolReservedSlots=0;
|
|
uint32 GpuFarPanelOutputHighWater=0,GpuFarPanelOutputReservedCapacityVertices=0;
|
|
uint64 GpuFarOutputCountChecks=0,GpuFarOutputOverflowCount=0,
|
|
GpuFarConnectorOverflowCount=0;
|
|
uint64 GpuFarOutputCountReadbackFailures=0;
|
|
uint32 GpuFarMaxRequiredVertices=0,GpuFarMaxRequiredOutputSlot=UINT32_MAX;
|
|
FVoxelTileKey GpuFarMaxRequiredVertexKey{};
|
|
std::size_t GpuFarPreReservePoolSlots=0;
|
|
int32 GpuFarPreReserveCoarseTileCells=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,GpuFarBaseCpuFallbackTiles;
|
|
std::vector<FVoxelTileKey> GpuFarCpuFallbackTiles,GpuFarNeedPreparation;
|
|
std::unordered_map<FVoxelTileKey,FGpuFarPanelOutputRange,FStandaloneTileKeyHash> GpuFarPanelOutputRanges;
|
|
std::vector<FGpuFarPanelFreeRange> GpuFarPanelOutputFreeRanges;
|
|
uint64 GpuFarWarmupCpuClaimedTiles=0,GpuFarWarmupGpuTakeoverTiles=0;
|
|
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(GpuWarmupPolicyThread.joinable())GpuWarmupPolicyThread.join();
|
|
if(GpuShaderWarmupThread.joinable())GpuShaderWarmupThread.join();
|
|
StopGeneration(true);ReleaseMeshes();
|
|
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
|
|
if(DepthDisabledAuditState)DepthDisabledAuditState->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.ReadbackCopyVertexBuffer)Tile.ReadbackCopyVertexBuffer->Release();
|
|
if(Tile.ReadbackCopyIndexBuffer)Tile.ReadbackCopyIndexBuffer->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();
|
|
GpuFarPreparationState.reset();
|
|
{std::lock_guard Lock(GpuFarPreparedResultsMutex);GpuFarPreparedResults.clear();}
|
|
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
|
|
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
|
|
GpuFarPanelOutputRanges.clear();GpuFarPanelOutputFreeRanges.clear();
|
|
GpuFarPanelOutputHighWater=GpuFarPanelOutputReservedCapacityVertices=0;
|
|
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
|
|
bGpuFarPreparationDeferredReported=false;
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
bGpuWarmupRoutePending=false;
|
|
GpuFarWarmupCpuClaimedTiles=0;GpuFarWarmupGpuTakeoverTiles=0;
|
|
GpuFarPreparationWallMs=0.0;GpuFarPreparationWorkerMs=0.0;
|
|
bGpuFarFirstPreparedInputReported=false;
|
|
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;
|
|
GpuFarNearTilesSubmitted=GpuFarFarTilesSubmitted=0;
|
|
GpuFarPreparedNearTileInputs=GpuFarPreparedFarTileInputs=0;
|
|
GpuFarNearLatticeSamples=GpuFarNearPackedCandidateSamples=0;
|
|
GpuFarNearPackedSkippedSamples=GpuFarProtectedRouteSamples=0;
|
|
GpuFarProtectedThinSamples=GpuFarProtectedSolidSamples=0;
|
|
GpuFarProtectedSealSamples=GpuFarProtectedOverlaySamples=0;
|
|
GpuFarOutputCountChecks=GpuFarOutputOverflowCount=GpuFarConnectorOverflowCount=0;
|
|
GpuFarOutputCountReadbackFailures=0;GpuFarMaxRequiredVertices=0;
|
|
GpuFarMaxRequiredOutputSlot=UINT32_MAX;GpuFarMaxRequiredVertexKey={};
|
|
GpuFarGpuDispatchMaxMs=0.0;GpuFarSubmissionSpanMs=0.0;GpuFarSubmissionSpanMaxMs=0.0;
|
|
GpuFarGpuDispatchMaxKey={};bGpuFarGpuDispatchMaxKeyValid=false;
|
|
GpuFarLocalMemoryPeakBytes=0;GpuFarLocalMemoryBudgetBytes=0;
|
|
bGpuFarFirstTileReported=false;
|
|
GpuSkirtDrawAuditTiles=GpuSkirtDrawAuditZeroCounts=GpuSkirtDrawAuditOverflow=0;
|
|
GpuSkirtDrawAuditBadStartIndex=GpuSkirtDrawAuditBadSurfaceArgs=0;
|
|
GpuSkirtDrawAuditSumIndices=GpuSkirtDrawAuditMaxIndices=0;
|
|
GpuFarPoolReserveMs=0.0;
|
|
GpuFarSubmissionBatches=0;GpuFarMaxTilesPerSubmissionBatch=0;
|
|
GpuFarOutputPoolReservedSlots=0;
|
|
GpuFarPanelOutputReservedCapacityVertices=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(GpuPipelineStartupThread.joinable())GpuPipelineStartupThread.join();
|
|
GpuFarPreparationState.reset();
|
|
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;
|
|
D3D11_DEPTH_STENCIL_DESC NoDepth{};NoDepth.DepthEnable=FALSE;
|
|
NoDepth.DepthWriteMask=D3D11_DEPTH_WRITE_MASK_ZERO;NoDepth.DepthFunc=D3D11_COMPARISON_ALWAYS;
|
|
if(FAILED(Device->CreateDepthStencilState(&NoDepth,&DepthDisabledAuditState)))return false;
|
|
if(!Resize(Width,Height))return false;
|
|
return true;
|
|
}
|
|
|
|
bool EnsureGpuFarPipeline(std::string& Error)
|
|
{
|
|
if(!bGpuFarEnabled)return true;
|
|
if(GpuPipelineStartupThread.joinable())
|
|
{
|
|
GpuPipelineStartupThread.join();
|
|
if(!bGpuPipelineStartupSucceeded&&!bGpuPipelineStartupWasDeferred)
|
|
{
|
|
Error=GpuPipelineStartupError.empty()
|
|
?"overlapped GPU pipeline initialization failed":GpuPipelineStartupError;
|
|
return false;
|
|
}
|
|
}
|
|
if(bGpuFarPipelineInitialized)return true;
|
|
if(!Device||!Context)
|
|
{Error="D3D device is unavailable for the GPU far pipeline";return false;}
|
|
const auto GpuPipelineStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=gpu_pipeline_init begin_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(GpuPipelineStart-ColdProcessStart).count());
|
|
if(!VoxelForgeGpuFarViewer::InitializeFarPipeline(Device,Context,GpuFarShaderPath,Error))
|
|
{
|
|
std::fprintf(stderr,"[GpuPipelineInitError] %s\n",Error.c_str());
|
|
return false;
|
|
}
|
|
bGpuFarPipelineInitialized=true;
|
|
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 StartGpuPipelineStartup(std::size_t PreReservePoolSlots,int32 CoarseTileCells)
|
|
{
|
|
if(!bGpuFarEnabled||bGpuFarPipelineInitialized||GpuPipelineStartupThread.joinable()
|
|
||bGpuPipelineStartupAttempted||bGpuFarWarmupBeforeWorkers)return;
|
|
if(!Device||!Context)return;
|
|
bGpuPipelineStartupAttempted=true;
|
|
GpuPipelineStartupThread=std::thread([this,PreReservePoolSlots,CoarseTileCells]()
|
|
{
|
|
ResolveGpuWarmupLaunchPolicy(true);
|
|
if(bGpuWarmupDeferredUntilFirstView)
|
|
{
|
|
bGpuPipelineStartupWasDeferred=true;
|
|
std::printf("[GpuPipelineOverlap] status=deferred reason=launch_policy_after_first_CPU_view\n");
|
|
return;
|
|
}
|
|
if(!SetGpuWarmupContextProtection(true))
|
|
{
|
|
bGpuPipelineStartupWasDeferred=true;
|
|
std::printf("[GpuPipelineOverlap] status=deferred reason=immediate_context_protection_unavailable\n");
|
|
return;
|
|
}
|
|
const auto PipelineStart=std::chrono::steady_clock::now();
|
|
std::printf("[GpuPipelineStartup] status=started action=pipeline_init_and_output_pool phase=post_shape_compile_before_cpu_workers\n");
|
|
std::printf("[ColdTimeline] stage=gpu_pipeline_init begin_ms=%.3f phase=post_shape_compile_before_cpu_workers\n",
|
|
std::chrono::duration<double,std::milli>(PipelineStart-ColdProcessStart).count());
|
|
if(!VoxelForgeGpuFarViewer::InitializeFarPipeline(Device,Context,GpuFarShaderPath,
|
|
GpuPipelineStartupError))
|
|
{
|
|
bGpuPipelineStartupSucceeded=false;
|
|
SetGpuWarmupContextProtection(false);
|
|
std::fprintf(stderr,"[GpuPipelineInitError] overlapped: %s\n",
|
|
GpuPipelineStartupError.c_str());
|
|
return;
|
|
}
|
|
bGpuFarPipelineInitialized=true;
|
|
bGpuPipelineStartupSucceeded=true;
|
|
std::printf("[ColdTimeline] stage=gpu_pipeline_init end_ms=%.3f duration_ms=%.3f prewarm=%s phase=post_shape_compile_before_cpu_workers\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()-PipelineStart).count(),
|
|
bGpuFarShaderPrewarm?(bGpuFarWarmupResourcesOnly?"async_resources_only":"async_dispatch")
|
|
:"disabled");
|
|
if(PreReservePoolSlots>0)
|
|
{
|
|
const auto PoolStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool_precompile begin_ms=%.3f slots=%zu phase=post_shape_compile_before_cpu_workers\n",
|
|
ColdTimelineMilliseconds(PoolStart),PreReservePoolSlots);
|
|
std::string PoolError;
|
|
if(VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
|
|
static_cast<uint32>(PreReservePoolSlots),static_cast<uint32>(CoarseTileCells),PoolError,0))
|
|
{
|
|
GpuFarOutputCapacitySlots=static_cast<uint32>(PreReservePoolSlots);
|
|
GpuFarOutputPoolReservedSlots=GpuFarOutputCapacitySlots;
|
|
GpuFarPanelOutputReservedCapacityVertices=0;
|
|
const double PoolMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-PoolStart).count();
|
|
GpuFarPoolReserveMs+=PoolMs;
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool_precompile end_ms=%.3f duration_ms=%.3f reserved_slots=%u phase=post_shape_compile_before_cpu_workers\n",
|
|
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),PoolMs,
|
|
GpuFarOutputPoolReservedSlots);
|
|
}
|
|
else
|
|
std::fprintf(stderr,"[GpuFarPoolError] precompile reservation deferred: %s\n",
|
|
PoolError.c_str());
|
|
}
|
|
// Shader warmup issues a long queue of GPU work and can throttle the
|
|
// CPU-heavy detail pass and initial meshing. Pipeline and pool setup
|
|
// start after shape compilation, before the CPU stream workers.
|
|
if(!bGpuFarShaderPrewarm||bGpuWarmupCompleted)
|
|
SetGpuWarmupContextProtection(false);
|
|
});
|
|
}
|
|
|
|
bool BuildGpuWarmupCacheKey()
|
|
{
|
|
GpuWarmupCacheKey.clear();
|
|
GpuWarmupCacheIdentity.clear();
|
|
if(!Device||GpuFarShaderPath.empty())return false;
|
|
|
|
IDXGIDevice* DxgiDevice=nullptr;
|
|
if(FAILED(Device->QueryInterface(__uuidof(IDXGIDevice),
|
|
reinterpret_cast<void**>(&DxgiDevice)))||!DxgiDevice)return false;
|
|
IDXGIAdapter* Adapter=nullptr;
|
|
const HRESULT AdapterResult=DxgiDevice->GetAdapter(&Adapter);
|
|
DxgiDevice->Release();
|
|
if(FAILED(AdapterResult)||!Adapter)return false;
|
|
|
|
DXGI_ADAPTER_DESC AdapterDesc{};
|
|
const HRESULT DescResult=Adapter->GetDesc(&AdapterDesc);
|
|
LARGE_INTEGER DriverVersion{};
|
|
const HRESULT DriverResult=Adapter->CheckInterfaceSupport(
|
|
__uuidof(IDXGIDevice),&DriverVersion);
|
|
Adapter->Release();
|
|
if(FAILED(DescResult)||FAILED(DriverResult))return false;
|
|
|
|
static constexpr std::array<const char*,14> WarmupEntries={
|
|
"EvaluateFarFieldCS","EvaluateNearFieldCS","ClassifyCS","ClassifyFarCS",
|
|
"ScanGroupsCS","EmitCS","ApplyNearFieldDetailCS","EmitNearCS",
|
|
"BuildFarConnectorRingsCS","EmitFarConnectorPanelsCS","ClassifyFarSkirtsCS",
|
|
"ScanFarSkirtsCS","EmitFarSkirtsCS","NoiseProbeCS"};
|
|
uint64 ShaderSetHash=14695981039346656037ull;
|
|
const auto MixText=[&](std::string_view Text)
|
|
{
|
|
for(const unsigned char Byte:Text)
|
|
{ShaderSetHash^=Byte;ShaderSetHash*=1099511628211ull;}
|
|
ShaderSetHash^=0xffu;ShaderSetHash*=1099511628211ull;
|
|
};
|
|
const auto MixFile=[&](const std::filesystem::path& Path,bool bRequired)
|
|
{
|
|
std::error_code Ec;
|
|
const bool bExists=std::filesystem::is_regular_file(Path,Ec)&&!Ec;
|
|
MixText(Path.filename().string());
|
|
if(!bExists)
|
|
{
|
|
MixText("missing");
|
|
return !bRequired;
|
|
}
|
|
const uint64 FileHash=HashStandaloneFile(Path.string());
|
|
if(FileHash==0)return false;
|
|
const uint64 FileSize=std::filesystem::file_size(Path,Ec);
|
|
if(Ec)return false;
|
|
MixText(std::to_string(FileSize));
|
|
MixText(std::to_string(FileHash));
|
|
return true;
|
|
};
|
|
|
|
MixText("gpu-far-warmup-signature-v2");
|
|
const std::filesystem::path ShaderImplementation=GpuFarShaderPath.parent_path()
|
|
.parent_path()/"gpu_field_port"/"GpuFieldPort.cpp";
|
|
const std::filesystem::path ViewerImplementation=GpuFarShaderPath.parent_path()
|
|
.parent_path()/"gpu_field_port"/"GpuFarViewer.h";
|
|
if(!MixFile(GpuFarShaderPath,true)||!MixFile(ShaderImplementation,true)
|
|
||!MixFile(ViewerImplementation,true))return false;
|
|
const std::filesystem::path PrecompiledDirectory=
|
|
VoxelForgeGpuFarViewer::ExecutablePrecompiledShaderDirectory(GpuFarShaderPath);
|
|
for(const char* Entry:WarmupEntries)
|
|
if(!MixFile(PrecompiledDirectory/(std::string(Entry)+".cso"),true))return false;
|
|
|
|
// Step 29 changes only shader lookup and CPU/GPU routing. When every
|
|
// warmup bytecode file is identical to Step 28's pinned bundle, retain
|
|
// its marker identity so an already-warmed driver can skip redundant
|
|
// startup dispatches. A changed or incomplete bytecode set keeps the
|
|
// normal content-derived identity and warmup path.
|
|
if(PrecompiledDirectory.filename()=="VoxelForgeGpuFarStep29")
|
|
{
|
|
bool bStep28BytecodeIdentical=true;
|
|
const std::filesystem::path Step28Directory=PrecompiledDirectory.parent_path();
|
|
for(const char* Entry:WarmupEntries)
|
|
{
|
|
const std::filesystem::path Current=PrecompiledDirectory/(std::string(Entry)+".cso");
|
|
const std::filesystem::path Step28=Step28Directory/(std::string(Entry)+".cso");
|
|
std::error_code CurrentEc,Step28Ec;
|
|
const uint64 CurrentSize=std::filesystem::file_size(Current,CurrentEc);
|
|
const uint64 Step28Size=std::filesystem::file_size(Step28,Step28Ec);
|
|
if(CurrentEc||Step28Ec||CurrentSize!=Step28Size
|
|
||HashStandaloneFile(Current.string())!=HashStandaloneFile(Step28.string()))
|
|
{bStep28BytecodeIdentical=false;break;}
|
|
}
|
|
if(bStep28BytecodeIdentical)
|
|
{
|
|
ShaderSetHash=0x6B94E565889F96F2ull;
|
|
std::printf("[GpuWarmupBytecodeAlias] applied=step28_identical_shader_set entries=%zu\n",
|
|
WarmupEntries.size());
|
|
}
|
|
}
|
|
|
|
std::ostringstream Identity;
|
|
Identity<<"adapter_luid="<<std::hex<<std::uppercase
|
|
<<static_cast<uint32>(AdapterDesc.AdapterLuid.HighPart)<<":"<<AdapterDesc.AdapterLuid.LowPart
|
|
<<" vendor="<<AdapterDesc.VendorId<<" device="<<AdapterDesc.DeviceId
|
|
<<" subsystem="<<AdapterDesc.SubSysId<<" revision="<<AdapterDesc.Revision
|
|
<<" driver_version=0x"<<static_cast<uint64>(DriverVersion.QuadPart)
|
|
<<" shader_set=0x"<<ShaderSetHash;
|
|
GpuWarmupCacheIdentity=Identity.str();
|
|
GpuWarmupCacheKey="VF_GPU_WARMUP_V1 "+GpuWarmupCacheIdentity;
|
|
bGpuWarmupCacheKeyAvailable=true;
|
|
return true;
|
|
}
|
|
|
|
void StartGpuWarmupPolicyResolutionAsync()
|
|
{
|
|
if(!bGpuFarEnabled||!bGpuFarShaderPrewarm||bGpuWarmupPolicyResolutionStarted)return;
|
|
bGpuWarmupPolicyResolutionStarted=true;
|
|
bGpuWarmupPolicyResolutionFinished.store(false,std::memory_order_relaxed);
|
|
bGpuWarmupLaunchPolicyResolved=false;
|
|
bGpuWarmupCacheMarkerMatches=false;
|
|
GpuWarmupPolicyResolutionMs=0.0;
|
|
if(bGpuFarWarmupBeforeWorkers
|
|
||GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceDeferred)
|
|
bGpuWarmupDeferredUntilFirstView=true;
|
|
else if(GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceEager)
|
|
bGpuWarmupDeferredUntilFirstView=false;
|
|
else
|
|
bGpuWarmupDeferredUntilFirstView=true;
|
|
|
|
GpuWarmupPolicyThread=std::thread([this]
|
|
{
|
|
const auto ResolveStart=std::chrono::steady_clock::now();
|
|
bool bCacheMarkerMatches=false;
|
|
try
|
|
{
|
|
const bool bKeyAvailable=BuildGpuWarmupCacheKey();
|
|
if(bKeyAvailable&&!GpuWarmupStatePath.empty())
|
|
{
|
|
std::ifstream Marker(GpuWarmupStatePath,std::ios::binary);
|
|
std::string SavedKey;
|
|
if(Marker&&std::getline(Marker,SavedKey))
|
|
bCacheMarkerMatches=SavedKey==GpuWarmupCacheKey;
|
|
}
|
|
}
|
|
catch(const std::exception& Error)
|
|
{
|
|
std::fprintf(stderr,"[GpuWarmupPolicyError] %s\n",Error.what());
|
|
}
|
|
bGpuWarmupCacheMarkerMatches=bCacheMarkerMatches;
|
|
GpuWarmupPolicyResolutionMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ResolveStart).count();
|
|
bGpuWarmupPolicyResolutionFinished.store(true,std::memory_order_release);
|
|
});
|
|
std::printf("[ColdTimeline] event=gpu_warmup_policy_async_started at_ms=%.3f default_route=%s\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
|
bGpuWarmupDeferredUntilFirstView?"after_first_CPU_view":"startup_overlap");
|
|
}
|
|
|
|
bool ResolveGpuWarmupLaunchPolicy(bool bWaitForResolution=false)
|
|
{
|
|
if(!bGpuFarEnabled||!bGpuFarShaderPrewarm)return true;
|
|
if(!bGpuWarmupPolicyResolutionStarted)StartGpuWarmupPolicyResolutionAsync();
|
|
if(bGpuWarmupLaunchPolicyResolved)return true;
|
|
if(!bGpuWarmupPolicyResolutionFinished.load(std::memory_order_acquire))
|
|
{
|
|
if(!bWaitForResolution)return false;
|
|
if(GpuWarmupPolicyThread.joinable())GpuWarmupPolicyThread.join();
|
|
}
|
|
else if(GpuWarmupPolicyThread.joinable())GpuWarmupPolicyThread.join();
|
|
|
|
if(!bGpuWarmupPolicyResolutionFinished.load(std::memory_order_acquire))return false;
|
|
if(bGpuFarWarmupBeforeWorkers)
|
|
{
|
|
bGpuWarmupDeferredUntilFirstView=false;
|
|
std::printf("[GpuWarmupLaunchDecision] mode=before_workers cache_marker=%s key_available=%s identity=%s resolution_ms=%.3f\n",
|
|
bGpuWarmupCacheMarkerMatches?"match":"miss",
|
|
bGpuWarmupCacheKeyAvailable?"yes":"no",
|
|
bGpuWarmupCacheKeyAvailable?GpuWarmupCacheIdentity.c_str():"unavailable",
|
|
GpuWarmupPolicyResolutionMs);
|
|
bGpuWarmupLaunchPolicyResolved=true;
|
|
return true;
|
|
}
|
|
|
|
const bool bDeferredRouteWasAlreadyStarted=bGpuWarmupDeferredStarted;
|
|
if(GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceDeferred)
|
|
bGpuWarmupDeferredUntilFirstView=true;
|
|
else if(GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceEager)
|
|
bGpuWarmupDeferredUntilFirstView=false;
|
|
else
|
|
bGpuWarmupDeferredUntilFirstView=!bGpuWarmupCacheMarkerMatches
|
|
||bDeferredRouteWasAlreadyStarted;
|
|
|
|
const char* Mode=GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceDeferred
|
|
?"forced_cold_start":(GpuWarmupLaunchPolicy==EGpuWarmupLaunchPolicy::ForceEager
|
|
?"forced_cached_start":"auto");
|
|
std::printf("[GpuWarmupLaunchDecision] mode=%s cache_marker=%s route=%s key_available=%s state_path=%s identity=%s resolution_ms=%.3f\n",
|
|
Mode,bGpuWarmupCacheMarkerMatches?"match":"miss",
|
|
bGpuWarmupDeferredUntilFirstView?"after_first_CPU_view":"startup_overlap",
|
|
bGpuWarmupCacheKeyAvailable?"yes":"no",GpuWarmupStatePath.string().c_str(),
|
|
bGpuWarmupCacheKeyAvailable?GpuWarmupCacheIdentity.c_str():"unavailable",
|
|
GpuWarmupPolicyResolutionMs);
|
|
bGpuWarmupLaunchPolicyResolved=true;
|
|
if(!bGpuWarmupDeferredUntilFirstView&&bGpuWarmupCacheMarkerMatches)
|
|
{
|
|
// This exact adapter/driver/shader key was recorded only after a
|
|
// successful warmup. Replaying tiny dispatches every process start
|
|
// competes with shape and tile workers without changing the pipeline.
|
|
bGpuWarmupCompleted=true;
|
|
std::printf("[GpuWarmupCacheHit] action=skip_redundant_dispatches identity=%s\n",
|
|
GpuWarmupCacheIdentity.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MarkGpuWarmupCacheReady()
|
|
{
|
|
if(bGpuWarmupCacheWriteDisabled)
|
|
{
|
|
std::printf("[GpuWarmupCacheState] result=write_disabled policy=command_line\n");
|
|
return;
|
|
}
|
|
if(!bGpuFarShaderPrewarm||bGpuFarWarmupResourcesOnly
|
|
||!GpuWarmupEntryFilter.empty()||GpuWarmupStatePath.empty())return;
|
|
ResolveGpuWarmupLaunchPolicy(true);
|
|
if(!bGpuWarmupCacheKeyAvailable)return;
|
|
const std::filesystem::path TemporaryPath=GpuWarmupStatePath.wstring()
|
|
+L"."+std::to_wstring(GetCurrentProcessId())+L".tmp";
|
|
{
|
|
std::ofstream Marker(TemporaryPath,std::ios::binary|std::ios::trunc);
|
|
if(!Marker)
|
|
{
|
|
std::fprintf(stderr,"[GpuWarmupCacheState] write_failed path=%s reason=open_failed\n",
|
|
TemporaryPath.string().c_str());
|
|
return;
|
|
}
|
|
Marker<<GpuWarmupCacheKey;
|
|
Marker.flush();
|
|
if(!Marker.good())
|
|
{
|
|
std::fprintf(stderr,"[GpuWarmupCacheState] write_failed path=%s reason=flush_failed\n",
|
|
TemporaryPath.string().c_str());
|
|
return;
|
|
}
|
|
}
|
|
if(!MoveFileExW(TemporaryPath.c_str(),GpuWarmupStatePath.c_str(),
|
|
MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH))
|
|
{
|
|
const DWORD Error=GetLastError();
|
|
DeleteFileW(TemporaryPath.c_str());
|
|
std::fprintf(stderr,"[GpuWarmupCacheState] write_failed path=%s win32_error=%lu\n",
|
|
GpuWarmupStatePath.string().c_str(),static_cast<unsigned long>(Error));
|
|
return;
|
|
}
|
|
std::printf("[GpuWarmupCacheState] result=recorded path=%s identity=%s\n",
|
|
GpuWarmupStatePath.string().c_str(),GpuWarmupCacheIdentity.c_str());
|
|
}
|
|
|
|
bool SetGpuWarmupContextProtection(bool bProtected)
|
|
{
|
|
if(!Context)return false;
|
|
if(bGpuWarmupContextProtected==bProtected)return true;
|
|
ID3D11Multithread* Multithread=nullptr;
|
|
if(FAILED(Context->QueryInterface(__uuidof(ID3D11Multithread),
|
|
reinterpret_cast<void**>(&Multithread)))||!Multithread)return false;
|
|
Multithread->SetMultithreadProtected(bProtected?TRUE:FALSE);
|
|
Multithread->Release();
|
|
bGpuWarmupContextProtected=bProtected;
|
|
return true;
|
|
}
|
|
|
|
void StartGpuShaderWarmupAsync(bool bShapeCompileOverlap=false)
|
|
{
|
|
if(!bGpuFarShaderPrewarm||bGpuWarmupCompleted||GpuShaderWarmupThread.joinable())return;
|
|
bGpuShaderWarmupQueueSucceeded=false;GpuShaderWarmupQueueError.clear();
|
|
bGpuShaderWarmupQueueFinished.store(false,std::memory_order_relaxed);
|
|
GpuShaderWarmupQueueStartedAt=std::chrono::steady_clock::now();
|
|
GpuShaderWarmupQueueFinishedAt={};
|
|
const bool bContextProtected=SetGpuWarmupContextProtection(true);
|
|
if(!bContextProtected)
|
|
std::fprintf(stderr,"[GpuWarmupContextProtection] enable_failed; async immediate-context protection is unavailable\n");
|
|
GpuShaderWarmupThread=std::thread([this]()
|
|
{
|
|
const bool bPreserveAuditPriority=bGpuFarWarmupBeforeWorkers||bGpuFarAuditEnabled;
|
|
const int WarmupPriority=bGpuFarWarmupNormalPriority?THREAD_PRIORITY_HIGHEST:
|
|
(bPreserveAuditPriority?THREAD_PRIORITY_BELOW_NORMAL:THREAD_PRIORITY_NORMAL);
|
|
const bool bPrioritySet=SetThreadPriority(GetCurrentThread(),WarmupPriority)!=0;
|
|
const char* RelativePriority=bGpuFarWarmupNormalPriority?"Highest":
|
|
(bPreserveAuditPriority?"BelowNormal":"Normal");
|
|
std::printf("[GpuShaderWarmupThread] priority=%s process=BelowNormal thread_relative=%s purpose=%s\n",
|
|
bPrioritySet?RelativePriority:"set_failed",RelativePriority,
|
|
bGpuWarmupDeferredUntilFirstView?"post_first_CPU_view":
|
|
(bPreserveAuditPriority?"audit_or_before_workers":"cache_candidate_startup"));
|
|
std::fflush(stdout);
|
|
bGpuShaderWarmupQueueSucceeded=VoxelForgeGpuFarViewer::QueueFarShaderWarmup(
|
|
Device,Context,GpuFarShaderPath,!bGpuFarWarmupResourcesOnly,GpuWarmupEntryFilter,
|
|
bGpuFarWarmupKernelFences,
|
|
GpuShaderWarmupQueueError);
|
|
GpuShaderWarmupQueueFinishedAt=std::chrono::steady_clock::now();
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[ColdTimeline] event=gpu_warmup_queue_finished at_ms=%.3f queue_ms=%.3f success=%s\n",
|
|
ColdTimelineMilliseconds(GpuShaderWarmupQueueFinishedAt),
|
|
std::chrono::duration<double,std::milli>(
|
|
GpuShaderWarmupQueueFinishedAt-GpuShaderWarmupQueueStartedAt).count(),
|
|
bGpuShaderWarmupQueueSucceeded?"yes":"no");
|
|
bGpuShaderWarmupQueueFinished.store(true,std::memory_order_release);
|
|
});
|
|
std::printf("[GpuShaderWarmup] queue_thread=started overlap=%s\n",
|
|
bGpuWarmupDeferredUntilFirstView?"after_first_CPU_view":
|
|
(bGpuFarWarmupBeforeWorkers?"none_before_generation_workers":
|
|
(bShapeCompileOverlap?"shape_compile":"CPU_world_configuration")));
|
|
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);
|
|
}
|
|
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;
|
|
}
|
|
|
|
void PopulateGpuVisualDetailBandPlan(VoxelForgeCore::ShapeBuildRequest& Request,
|
|
const std::vector<FVoxelTileKey>& TileKeys) const
|
|
{
|
|
if(!bGpuFarEnabled||!bProfileDetailBand)return;
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> TileSet;
|
|
TileSet.reserve(TileKeys.size()*2+1);
|
|
for(const FVoxelTileKey& Key:TileKeys)TileSet.insert(Key);
|
|
for(const FVoxelTileKey& Key:TileKeys)
|
|
{
|
|
if(Key.Level<0||Key.Level>=World->FullResClipLevels)continue;
|
|
const FIntVector Origin=Key.OriginVoxels();
|
|
VoxelForgeCore::DetailBandTileRegion Region;
|
|
Region.originVoxel={Origin.X,Origin.Y,Origin.Z};
|
|
Region.cells=CHUNK_SIZE;
|
|
Region.step=std::max(1,Key.ExtentVoxels()/CHUNK_SIZE);
|
|
const uint8 VisibleSkirts=World->bGenerateSkirts
|
|
?ComputeVisibleSkirtFaceMask(Key,TileSet):0;
|
|
if(VisibleSkirts==0||bGpuNearSkirtsEnabled)
|
|
Request.gpuVisualDetailTiles.push_back(Region);
|
|
else Request.cpuVisualDetailTiles.push_back(Region);
|
|
}
|
|
}
|
|
|
|
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;
|
|
PopulateGpuVisualDetailBandPlan(Request,
|
|
bNeighborhoodOnly?InitialNeighborhoodTiles:Desired.Tiles);
|
|
Request.validatePassageRoutes=!bDeferWalkabilityValidation;
|
|
if(bDeferWalkabilityValidation)
|
|
std::printf("[DiagnosticDeferred] walkability=full_headless_or_route_audit workers=8 samples=5642\n");
|
|
GpuFarPreReservePoolSlots=0;
|
|
GpuFarPreReserveCoarseTileCells=World->CoarseTileCells;
|
|
const bool bCanPreReservePool=bGpuFarEnabled&&bIncludeFarWorld&&!bNeighborhoodOnly
|
|
&&!bCaptureOnlyTile&&!(CaptureTileLimit>0&&!CapturePath.empty());
|
|
if(bCanPreReservePool)
|
|
{
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PoolDesiredSet;
|
|
PoolDesiredSet.reserve(Desired.Tiles.size()*2+1);
|
|
for(const FVoxelTileKey& Key:Desired.Tiles)PoolDesiredSet.insert(Key);
|
|
const int32 ShapeMin[3]={Min.X-2,Min.Y-2,Min.Z-2};
|
|
const int32 ShapeMax[3]={Max.X+2,Max.Y+2,Max.Z+2};
|
|
for(const FVoxelTileKey& Key:Desired.Tiles)
|
|
{
|
|
if(Key.Level<0)continue;
|
|
if(Key.Level<World->FullResClipLevels)
|
|
{
|
|
if(!World->bGenerateSkirts||bGpuNearSkirtsEnabled
|
|
||ComputeVisibleSkirtFaceMask(Key,PoolDesiredSet)==0)
|
|
++GpuFarPreReservePoolSlots;
|
|
continue;
|
|
}
|
|
if(Key.Level>8)continue;
|
|
const int32 Cells=std::max(1,World->CoarseTileCells);
|
|
if(Cells>32)continue;
|
|
const int32 Extent=CHUNK_SIZE<<Key.Level;
|
|
const int32 Step=std::max(1,Extent/Cells);
|
|
const FIntVector Origin=Key.OriginVoxels();
|
|
const int32 Origins[3]={Origin.X,Origin.Y,Origin.Z};
|
|
bool bOutsideNearRegion=false;
|
|
for(int32 Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double First=std::ceil((static_cast<double>(ShapeMin[Axis])-Origins[Axis])/Step);
|
|
const double Last=std::floor((static_cast<double>(ShapeMax[Axis])-Origins[Axis])/Step);
|
|
if(First>Cells+1||Last< -1){bOutsideNearRegion=true;break;}
|
|
}
|
|
if(bOutsideNearRegion)++GpuFarPreReservePoolSlots;
|
|
}
|
|
std::printf("[GpuOutputPoolPreReservePlan] desired=%zu candidate_slots=%zu status=geometry_match\n",
|
|
Desired.Tiles.size(),GpuFarPreReservePoolSlots);
|
|
}
|
|
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());
|
|
if(bIncludeFarWorld&&!bNeighborhoodOnly)
|
|
StartGpuPipelineStartup(GpuFarPreReservePoolSlots,
|
|
GpuFarPreReserveCoarseTileCells);
|
|
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,
|
|
FullDesiredCopy=FullDesired]()
|
|
{
|
|
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;
|
|
PopulateGpuVisualDetailBandPlan(Request,FullDesiredCopy);
|
|
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();
|
|
bQuitAfterClipmapClosePosted=false;
|
|
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))
|
|
{
|
|
if(GpuPipelineStartupThread.joinable())GpuPipelineStartupThread.join();
|
|
std::fprintf(stderr,"World load failed: %s\n",Error.c_str());
|
|
MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;
|
|
}
|
|
if(GpuPipelineStartupThread.joinable())GpuPipelineStartupThread.join();
|
|
std::printf("[ColdTimeline] stage=generation_setup configured_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
ResolveGpuWarmupLaunchPolicy(false);
|
|
const bool bPipelineCanStartNow=!bGpuWarmupDeferredUntilFirstView
|
|
||bGpuWarmupDeferredStarted;
|
|
if(bGpuFarEnabled&&bPipelineCanStartNow&&!EnsureGpuFarPipeline(Error))
|
|
{MessageBoxA(Window,Error.c_str(),"VoxelForge GPU pipeline",MB_ICONERROR);return;}
|
|
if(bGpuFarShaderPrewarm&&!bGpuFarWarmupBeforeWorkers
|
|
&&!bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupCompleted)
|
|
StartGpuShaderWarmupAsync();
|
|
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;
|
|
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||BatchIndex!=UINT32_MAX)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&&Tile.BatchIndex==UINT32_MAX)
|
|
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)
|
|
{
|
|
Batch.ConsumedAt=ResidentAt;
|
|
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));
|
|
std::printf("[GpuBatchTimeline] event=consumed batch=%u at_ms=%.3f frame=%llu tile_count=%u submit_to_gpu_complete_ms=%.3f gpu_complete_to_consumed_ms=%.3f output=direct_gpu_buffers\n",
|
|
Tile.BatchIndex,ColdTimelineMilliseconds(ResidentAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
|
|
ColdTimelineMilliseconds(Batch.CompletedAt)-ColdTimelineMilliseconds(Batch.SubmittedAt),
|
|
ColdTimelineMilliseconds(ResidentAt)-ColdTimelineMilliseconds(Batch.CompletedAt));
|
|
}
|
|
}
|
|
}
|
|
|
|
void ReportGpuBatchTimelineSummary()
|
|
{
|
|
if(!bGpuFarTimelineEnabled)return;
|
|
uint64 FirstGpuStart=0,CommonFrequency=0;
|
|
for(const FGpuFarBatch& Batch:GpuFarBatches)
|
|
{
|
|
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuStartTimestamp)
|
|
{
|
|
FirstGpuStart=Batch.GpuStartTimestamp;
|
|
CommonFrequency=Batch.TimestampFrequency;
|
|
break;
|
|
}
|
|
}
|
|
uint64 PreviousGpuEnd=0,PreviousFrequency=0;
|
|
for(std::size_t I=0;I<GpuFarBatches.size();++I)
|
|
{
|
|
const FGpuFarBatch& Batch=GpuFarBatches[I];
|
|
const auto AtMs=[this](const std::chrono::steady_clock::time_point& At)
|
|
{
|
|
return At.time_since_epoch().count()!=0
|
|
?ColdTimelineMilliseconds(At):-1.0;
|
|
};
|
|
double GpuStartFromFirstMs=-1.0,GpuEndFromFirstMs=-1.0,GpuGapBeforeMs=-1.0;
|
|
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuStartTimestamp
|
|
&&CommonFrequency==Batch.TimestampFrequency&&FirstGpuStart)
|
|
{
|
|
GpuStartFromFirstMs=(static_cast<double>(Batch.GpuStartTimestamp)
|
|
-static_cast<double>(FirstGpuStart))*1000.0/Batch.TimestampFrequency;
|
|
GpuEndFromFirstMs=(static_cast<double>(Batch.GpuEndTimestamp)
|
|
-static_cast<double>(FirstGpuStart))*1000.0/Batch.TimestampFrequency;
|
|
if(PreviousGpuEnd&&PreviousFrequency==Batch.TimestampFrequency)
|
|
GpuGapBeforeMs=(static_cast<double>(Batch.GpuStartTimestamp)
|
|
-static_cast<double>(PreviousGpuEnd))*1000.0/Batch.TimestampFrequency;
|
|
}
|
|
std::printf("[GpuBatchTimeline] event=summary batch=%zu tiles=%u cpu_batch_ready_ms=%.3f cpu_submit_begin_ms=%.3f cpu_submit_end_ms=%.3f gpu_start_tick=%llu gpu_end_tick=%llu timestamp_frequency_hz=%llu gpu_start_from_first_ms=%.3f gpu_end_from_first_ms=%.3f gpu_gap_from_previous_end_ms=%.3f gpu_busy_ms=%.3f completion_observed_ms=%.3f consumed_ms=%.3f submit_to_completion_observed_ms=%.3f completion_to_consumed_ms=%.3f timing_resolved=%s\n",
|
|
I,Batch.TileCount,AtMs(Batch.PreparedAt),AtMs(Batch.SubmitStartedAt),
|
|
AtMs(Batch.SubmittedAt),static_cast<unsigned long long>(Batch.GpuStartTimestamp),
|
|
static_cast<unsigned long long>(Batch.GpuEndTimestamp),
|
|
static_cast<unsigned long long>(Batch.TimestampFrequency),GpuStartFromFirstMs,
|
|
GpuEndFromFirstMs,GpuGapBeforeMs,Batch.GpuDispatchMs,AtMs(Batch.CompletedAt),
|
|
AtMs(Batch.ConsumedAt),Batch.CompletedAt.time_since_epoch().count()!=0
|
|
?AtMs(Batch.CompletedAt)-AtMs(Batch.SubmittedAt):-1.0,
|
|
Batch.ConsumedAt.time_since_epoch().count()!=0
|
|
&&Batch.CompletedAt.time_since_epoch().count()!=0
|
|
?AtMs(Batch.ConsumedAt)-AtMs(Batch.CompletedAt):-1.0,
|
|
Batch.bTimingResolved?"yes":"no");
|
|
if(Batch.bTimingResolved&&Batch.TimestampFrequency&&Batch.GpuEndTimestamp)
|
|
{PreviousGpuEnd=Batch.GpuEndTimestamp;PreviousFrequency=Batch.TimestampFrequency;}
|
|
}
|
|
}
|
|
|
|
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];
|
|
const uint32 CurrentBatchIndex=Tile.BatchIndex;
|
|
if(!Batch.bGpuComplete&&Batch.CompletionEvent&&Context
|
|
&&Context->GetData(Batch.CompletionEvent,nullptr,0,D3D11_ASYNC_GETDATA_DONOTFLUSH)==S_OK)
|
|
{
|
|
std::vector<uint32> OutputSlots;
|
|
std::vector<FVoxelTileKey> BatchOutputKeys;
|
|
OutputSlots.reserve(Batch.Keys.size());
|
|
BatchOutputKeys.reserve(Batch.Keys.size());
|
|
for(const FVoxelTileKey& BatchKey:Batch.Keys)
|
|
{
|
|
const auto BatchTileIt=GpuFarTiles.find(BatchKey);
|
|
if(BatchTileIt==GpuFarTiles.end())continue;
|
|
OutputSlots.push_back(BatchTileIt->second.OutputSlot);
|
|
BatchOutputKeys.push_back(BatchKey);
|
|
}
|
|
std::vector<uint32> RequiredVertexCounts;
|
|
std::vector<uint32> ConnectorOverflowFlags;
|
|
std::string CountReadbackError;
|
|
const bool bOutputCountsRead=
|
|
VoxelForgeGpuFarViewer::ReadFarTileOutputStatuses(Device,Context,
|
|
GpuFarShaderPath,OutputSlots,RequiredVertexCounts,
|
|
ConnectorOverflowFlags,CountReadbackError);
|
|
std::vector<FVoxelTileKey> OverflowFallbackKeys;
|
|
if(!bOutputCountsRead)
|
|
{
|
|
++GpuFarOutputCountReadbackFailures;
|
|
std::fprintf(stderr,"[GpuFarSlotCountReadback] status=FAIL batch=%u tiles=%zu error=%s action=cpu_fallback_batch\n",
|
|
CurrentBatchIndex,BatchOutputKeys.size(),CountReadbackError.c_str());
|
|
OverflowFallbackKeys=BatchOutputKeys;
|
|
}
|
|
else
|
|
{
|
|
GpuFarOutputCountChecks+=RequiredVertexCounts.size();
|
|
for(std::size_t OutputIndex=0;OutputIndex<RequiredVertexCounts.size();++OutputIndex)
|
|
{
|
|
const uint32 Required=RequiredVertexCounts[OutputIndex];
|
|
const FVoxelTileKey& BatchKey=BatchOutputKeys[OutputIndex];
|
|
auto BatchTileIt=GpuFarTiles.find(BatchKey);
|
|
if(BatchTileIt==GpuFarTiles.end())continue;
|
|
FGpuFarTile& CheckedTile=BatchTileIt->second;
|
|
CheckedTile.RequiredSurfaceVertexCount=Required;
|
|
if(Required>GpuFarMaxRequiredVertices)
|
|
{
|
|
GpuFarMaxRequiredVertices=Required;
|
|
GpuFarMaxRequiredVertexKey=BatchKey;
|
|
GpuFarMaxRequiredOutputSlot=CheckedTile.OutputSlot;
|
|
}
|
|
if(Required>VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile)
|
|
{
|
|
++GpuFarOutputOverflowCount;
|
|
OverflowFallbackKeys.push_back(BatchKey);
|
|
std::fprintf(stderr,"[GpuFarSlotOverflow] key=(%d,%d,%d,lod%d) output_slot=%u required_vertices=%u capacity_vertices=%u batch=%u action=cpu_fallback\n",
|
|
BatchKey.Coord.X,BatchKey.Coord.Y,BatchKey.Coord.Z,BatchKey.Level,
|
|
CheckedTile.OutputSlot,Required,
|
|
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile,CurrentBatchIndex);
|
|
}
|
|
if(OutputIndex<ConnectorOverflowFlags.size()
|
|
&&ConnectorOverflowFlags[OutputIndex]!=0)
|
|
{
|
|
++GpuFarConnectorOverflowCount;
|
|
if(std::find(OverflowFallbackKeys.begin(),OverflowFallbackKeys.end(),BatchKey)
|
|
==OverflowFallbackKeys.end())OverflowFallbackKeys.push_back(BatchKey);
|
|
std::fprintf(stderr,"[GpuFarPanelOverflow] key=(%d,%d,%d,lod%d) output_slot=%u batch=%u action=cpu_fallback\n",
|
|
BatchKey.Coord.X,BatchKey.Coord.Y,BatchKey.Coord.Z,BatchKey.Level,
|
|
CheckedTile.OutputSlot,CurrentBatchIndex);
|
|
}
|
|
}
|
|
}
|
|
for(const FVoxelTileKey& FallbackKey:OverflowFallbackKeys)
|
|
{
|
|
GpuFarEligibleTiles.erase(FallbackKey);
|
|
GpuFarPreparedInputs.erase(FallbackKey);
|
|
GpuFarPreparationErrors.erase(FallbackKey);
|
|
GpuFarPreparedAt.erase(FallbackKey);
|
|
ReleaseGpuFarPanelOutputRange(FallbackKey);
|
|
auto FallbackTileIt=GpuFarTiles.find(FallbackKey);
|
|
if(FallbackTileIt==GpuFarTiles.end())continue;
|
|
const uint32 ReusableSlot=FallbackTileIt->second.OutputSlot;
|
|
ReleaseGpuFarTile(FallbackTileIt->second);
|
|
GpuFarTiles.erase(FallbackTileIt);
|
|
GpuFarFreeOutputSlots.push_back(ReusableSlot);
|
|
}
|
|
if(!OverflowFallbackKeys.empty())
|
|
{
|
|
Batch.TileCount-=static_cast<uint32>(std::min<std::size_t>(
|
|
Batch.TileCount,OverflowFallbackKeys.size()));
|
|
AddGpuFarCpuFallbackTiles(OverflowFallbackKeys);
|
|
}
|
|
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",
|
|
CurrentBatchIndex,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",
|
|
CurrentBatchIndex,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,CurrentBatchIndex);
|
|
RecordGpuCrossingTileReady(BatchKey);
|
|
ReportGpuTileResident(BatchKey,BatchTileIt->second);
|
|
}
|
|
}
|
|
if(!GpuFarEligibleTiles.contains(Key))return false;
|
|
}
|
|
if(Batch.bGpuComplete&&!Batch.bTimingResolved)
|
|
{
|
|
if(VoxelForgeGpuFarViewer::ResolveGpuTiming(Context,Batch.TimingQueries,
|
|
Batch.GpuDispatchMs,&Batch.TimestampFrequency,
|
|
&Batch.GpuStartTimestamp,&Batch.GpuEndTimestamp))
|
|
{
|
|
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;
|
|
ReportGpuFarSlowDispatchTile(Key,Tile,TileDispatchMs);
|
|
GpuFarGpuDispatchMs+=TileDispatchMs;
|
|
if(TileDispatchMs>GpuFarGpuDispatchMaxMs)
|
|
{
|
|
GpuFarGpuDispatchMaxMs=TileDispatchMs;
|
|
GpuFarGpuDispatchMaxKey=Key;bGpuFarGpuDispatchMaxKeyValid=true;
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
std::vector<uint32> RequiredVertexCounts;
|
|
std::vector<uint32> ConnectorOverflowFlags;
|
|
std::string CountReadbackError;
|
|
const bool bOutputCountRead=VoxelForgeGpuFarViewer::ReadFarTileOutputStatuses(
|
|
Device,Context,GpuFarShaderPath,{Tile.OutputSlot},RequiredVertexCounts,
|
|
ConnectorOverflowFlags,CountReadbackError);
|
|
const bool bOverflow=!bOutputCountRead||RequiredVertexCounts.empty()
|
|
||RequiredVertexCounts[0]>VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile
|
|
||ConnectorOverflowFlags.empty()||ConnectorOverflowFlags[0]!=0;
|
|
if(!bOutputCountRead)
|
|
{
|
|
++GpuFarOutputCountReadbackFailures;
|
|
std::fprintf(stderr,"[GpuFarSlotCountReadback] status=FAIL key=(%d,%d,%d,lod%d) output_slot=%u error=%s action=cpu_fallback\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.OutputSlot,
|
|
CountReadbackError.c_str());
|
|
}
|
|
else if(!RequiredVertexCounts.empty())
|
|
{
|
|
const uint32 Required=RequiredVertexCounts[0];
|
|
++GpuFarOutputCountChecks;Tile.RequiredSurfaceVertexCount=Required;
|
|
if(Required>GpuFarMaxRequiredVertices)
|
|
{
|
|
GpuFarMaxRequiredVertices=Required;GpuFarMaxRequiredVertexKey=Key;
|
|
GpuFarMaxRequiredOutputSlot=Tile.OutputSlot;
|
|
}
|
|
if(Required>VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile)
|
|
{
|
|
++GpuFarOutputOverflowCount;
|
|
std::fprintf(stderr,"[GpuFarSlotOverflow] key=(%d,%d,%d,lod%d) output_slot=%u required_vertices=%u capacity_vertices=%u action=cpu_fallback\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.OutputSlot,Required,
|
|
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile);
|
|
}
|
|
if(!ConnectorOverflowFlags.empty()&&ConnectorOverflowFlags[0]!=0)
|
|
{
|
|
++GpuFarConnectorOverflowCount;
|
|
std::fprintf(stderr,"[GpuFarPanelOverflow] key=(%d,%d,%d,lod%d) output_slot=%u action=cpu_fallback\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.OutputSlot);
|
|
}
|
|
}
|
|
if(bOverflow)
|
|
{
|
|
const uint32 ReusableSlot=Tile.OutputSlot;
|
|
GpuFarEligibleTiles.erase(Key);GpuFarPreparedInputs.erase(Key);
|
|
GpuFarPreparationErrors.erase(Key);GpuFarPreparedAt.erase(Key);
|
|
ReleaseGpuFarPanelOutputRange(Key);
|
|
ReleaseGpuFarTile(Tile);GpuFarTiles.erase(Key);
|
|
GpuFarFreeOutputSlots.push_back(ReusableSlot);
|
|
AddGpuFarCpuFallbackTiles({Key});
|
|
return false;
|
|
}
|
|
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;
|
|
ReportGpuFarSlowDispatchTile(Key,Tile,AsyncDispatchMs);
|
|
GpuFarGpuDispatchMs+=AsyncDispatchMs;
|
|
if(AsyncDispatchMs>GpuFarGpuDispatchMaxMs)
|
|
{
|
|
GpuFarGpuDispatchMaxMs=AsyncDispatchMs;
|
|
GpuFarGpuDispatchMaxKey=Key;bGpuFarGpuDispatchMaxKeyValid=true;
|
|
}
|
|
}
|
|
Tile.bTimingResolved=true;
|
|
}
|
|
}
|
|
if(Tile.bGpuComplete)
|
|
{
|
|
RecordGpuCrossingTileReady(Key);
|
|
ReportGpuTileResident(Key,Tile);
|
|
}
|
|
return Tile.bGpuComplete;
|
|
}
|
|
return ResidentTiles.contains(Key);
|
|
}
|
|
|
|
void PrepareGpuFarInputTile(const FVoxelTileKey& Key,uint8 VisibleSkirtFaceMask,
|
|
const std::shared_ptr<FGpuFarPreparationBatch>& Batch)
|
|
{
|
|
if(Batch->bCancelled.load(std::memory_order_acquire))return;
|
|
std::call_once(Batch->StartOnce,[&]()
|
|
{
|
|
Batch->PrepareStart=std::chrono::steady_clock::now();
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[ColdTimeline] event=gpu_input_export_workers_started at_ms=%.3f dispatch_to_start_ms=%.3f cpu_ready_to_start_ms=%.3f frame=%llu shared_workers=%zu\n",
|
|
ColdTimelineMilliseconds(Batch->PrepareStart),
|
|
std::chrono::duration<double,std::milli>(
|
|
Batch->PrepareStart-Batch->DispatchAt).count(),
|
|
Batch->CpuReadyAt.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double,std::milli>(
|
|
Batch->PrepareStart-Batch->CpuReadyAt).count():-1.0,
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),
|
|
Batch->SharedWorkerCount);
|
|
std::printf("[ColdTimeline] stage=gpu_input_export begin_ms=%.3f tiles=%zu shared_workers=%zu\n",
|
|
ColdTimelineMilliseconds(Batch->PrepareStart),Batch->TileCount,
|
|
Batch->SharedWorkerCount);
|
|
});
|
|
|
|
const auto WorkerStart=std::chrono::steady_clock::now();
|
|
const VoxelForgeCore::TileKey CoreKey{{Key.Coord.X,Key.Coord.Y,Key.Coord.Z},Key.Level};
|
|
FPreparedGpuFarInputResult Result;Result.Key=Key;
|
|
try
|
|
{
|
|
Result.Input=Key.Level<World->FullResClipLevels
|
|
?ShapeWorld->exportGpuFieldTile(CoreKey,Result.Error)
|
|
:ShapeWorld->exportFarFieldTile(CoreKey,Result.Error);
|
|
if(Result.Input&&VisibleSkirtFaceMask!=0)
|
|
{
|
|
Result.Input->visibleSkirtFaceMask=VisibleSkirtFaceMask;
|
|
Result.Input->skirtDepthVoxels=std::max(1.0f,
|
|
static_cast<float>(World->SkirtCells))*Result.Input->step;
|
|
}
|
|
if(Result.Input&&Key.Level>=World->FullResClipLevels)
|
|
{
|
|
if(!bGpuConnectorRepairEnabled)
|
|
{
|
|
Result.Input->connectorPanels.clear();
|
|
Result.Input->connectorRingCount=0;
|
|
Result.Input->connectorVertexCount=0;
|
|
}
|
|
}
|
|
}
|
|
catch(const std::exception& Error) { Result.Error=Error.what(); }
|
|
catch(...) { Result.Error="unknown input preparation failure"; }
|
|
if(Batch->bCancelled.load(std::memory_order_acquire))
|
|
{
|
|
Batch->Remaining.fetch_sub(1,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
Result.FinishedAt=std::chrono::steady_clock::now();
|
|
Result.FinishedFrame=ColdTimelineFrame();
|
|
if(!Result.Input)Batch->Failed.fetch_add(1,std::memory_order_relaxed);
|
|
const uint64 WorkerNanoseconds=static_cast<uint64>(std::chrono::duration_cast<
|
|
std::chrono::nanoseconds>(Result.FinishedAt-WorkerStart).count());
|
|
Batch->WorkerNanoseconds.fetch_add(WorkerNanoseconds,std::memory_order_relaxed);
|
|
{
|
|
std::lock_guard Lock(GpuFarPreparedResultsMutex);
|
|
if(Batch->bCancelled.load(std::memory_order_acquire)
|
|
||Batch->Generation!=GpuFarPreparationGeneration.load(std::memory_order_acquire))
|
|
{
|
|
Batch->Remaining.fetch_sub(1,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
GpuFarPreparedResults.push_back(std::move(Result));
|
|
}
|
|
if(Batch->Remaining.fetch_sub(1,std::memory_order_acq_rel)==1)
|
|
{
|
|
const auto PrepareEnd=std::chrono::steady_clock::now();
|
|
const double PreparationMs=std::chrono::duration<double,std::milli>(
|
|
PrepareEnd-Batch->PrepareStart).count();
|
|
const double WorkerSumMs=static_cast<double>(
|
|
Batch->WorkerNanoseconds.load(std::memory_order_relaxed))/1.0e6;
|
|
GpuFarPreparationWallMs+=PreparationMs;
|
|
GpuFarPreparationWorkerMs+=WorkerSumMs;
|
|
std::printf("[GpuFarInputPrepare] tiles=%zu workers=%zu mode=shared_priority_queue wall_ms=%.3f worker_sum_ms=%.3f failed=%zu\n",
|
|
Batch->TileCount,Batch->SharedWorkerCount,PreparationMs,WorkerSumMs,
|
|
Batch->Failed.load(std::memory_order_relaxed));
|
|
std::printf("[ColdTimeline] stage=gpu_input_export end_ms=%.3f duration_ms=%.3f worker_sum_ms=%.3f\n",
|
|
ColdTimelineMilliseconds(PrepareEnd),PreparationMs,WorkerSumMs);
|
|
std::fflush(stdout);
|
|
std::lock_guard Lock(GpuFarPreparedResultsMutex);
|
|
if(!Batch->bCancelled.load(std::memory_order_acquire)
|
|
&&Batch->Generation==GpuFarPreparationGeneration.load(std::memory_order_acquire))
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_release);
|
|
}
|
|
}
|
|
|
|
void ReportGpuFarSlowDispatchTile(const FVoxelTileKey& Key,const FGpuFarTile& Tile,
|
|
double DispatchMs)const
|
|
{
|
|
if(!bGpuFarTimelineEnabled||DispatchMs<5.0)return;
|
|
std::printf("[GpuFarSlowDispatchTile] key=(%d,%d,%d,lod%d) dispatch_ms=%.6f output_slot=%u output_chunk=%u cells=%d step=%d near_detail=%d protected=%llu thin=%llu solid=%llu near_proxy_samples=%u skirt_faces=0x%02x connector_rings=%u connector_vertices=%u\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,DispatchMs,Tile.OutputSlot,
|
|
Tile.OutputSlot/VoxelForgeGpuFarViewer::PooledOutputSlotsPerChunk,Tile.InputCells,
|
|
Tile.InputStep,Tile.bNearDetailInput?1:0,
|
|
static_cast<unsigned long long>(Tile.ProtectedSamples),
|
|
static_cast<unsigned long long>(Tile.ProtectedThinSamples),
|
|
static_cast<unsigned long long>(Tile.ProtectedSolidSamples),Tile.NearProxySamples,
|
|
static_cast<unsigned>(Tile.VisibleSkirtFaceMask),Tile.ConnectorRings,
|
|
Tile.ConnectorVertices);
|
|
}
|
|
|
|
void CoalesceGpuFarPanelOutputFreeRanges()
|
|
{
|
|
std::sort(GpuFarPanelOutputFreeRanges.begin(),GpuFarPanelOutputFreeRanges.end(),
|
|
[](const FGpuFarPanelFreeRange& A,const FGpuFarPanelFreeRange& B)
|
|
{return A.BaseVertex<B.BaseVertex;});
|
|
std::vector<FGpuFarPanelFreeRange> Merged;
|
|
Merged.reserve(GpuFarPanelOutputFreeRanges.size());
|
|
for(const FGpuFarPanelFreeRange& Range:GpuFarPanelOutputFreeRanges)
|
|
{
|
|
if(Range.CapacityVertices==0)continue;
|
|
if(!Merged.empty()&&static_cast<uint64>(Merged.back().BaseVertex)
|
|
+Merged.back().CapacityVertices==Range.BaseVertex)
|
|
Merged.back().CapacityVertices+=Range.CapacityVertices;
|
|
else Merged.push_back(Range);
|
|
}
|
|
GpuFarPanelOutputFreeRanges=std::move(Merged);
|
|
while(!GpuFarPanelOutputFreeRanges.empty())
|
|
{
|
|
const FGpuFarPanelFreeRange& Last=GpuFarPanelOutputFreeRanges.back();
|
|
if(static_cast<uint64>(Last.BaseVertex)+Last.CapacityVertices
|
|
!=GpuFarPanelOutputHighWater)break;
|
|
GpuFarPanelOutputHighWater=Last.BaseVertex;
|
|
GpuFarPanelOutputFreeRanges.pop_back();
|
|
}
|
|
}
|
|
|
|
void ReleaseGpuFarPanelOutputRange(const FVoxelTileKey& Key)
|
|
{
|
|
const auto It=GpuFarPanelOutputRanges.find(Key);
|
|
if(It==GpuFarPanelOutputRanges.end())return;
|
|
if(It->second.CapacityVertices!=0)
|
|
GpuFarPanelOutputFreeRanges.push_back({It->second.BaseVertex,It->second.CapacityVertices});
|
|
GpuFarPanelOutputRanges.erase(It);
|
|
CoalesceGpuFarPanelOutputFreeRanges();
|
|
}
|
|
|
|
bool AllocateGpuFarPanelOutputRange(std::uint32_t CapacityVertices,
|
|
FGpuFarPanelOutputRange& Out)
|
|
{
|
|
if(CapacityVertices==0){Out={};return true;}
|
|
for(auto It=GpuFarPanelOutputFreeRanges.begin();It!=GpuFarPanelOutputFreeRanges.end();++It)
|
|
{
|
|
if(It->CapacityVertices<CapacityVertices)continue;
|
|
Out={It->BaseVertex,CapacityVertices};
|
|
It->BaseVertex+=CapacityVertices;
|
|
It->CapacityVertices-=CapacityVertices;
|
|
if(It->CapacityVertices==0)GpuFarPanelOutputFreeRanges.erase(It);
|
|
return true;
|
|
}
|
|
if(static_cast<uint64>(GpuFarPanelOutputHighWater)+CapacityVertices
|
|
>std::numeric_limits<uint32>::max())return false;
|
|
Out={GpuFarPanelOutputHighWater,CapacityVertices};
|
|
GpuFarPanelOutputHighWater+=CapacityVertices;
|
|
return true;
|
|
}
|
|
|
|
bool ReserveGpuFarPanelOutputPool(std::string& Error)
|
|
{
|
|
for(auto It=GpuFarPanelOutputRanges.begin();It!=GpuFarPanelOutputRanges.end();)
|
|
{
|
|
if(GpuFarEligibleTiles.contains(It->first)){++It;continue;}
|
|
const FVoxelTileKey Key=It->first;++It;
|
|
ReleaseGpuFarPanelOutputRange(Key);
|
|
}
|
|
for(const FVoxelTileKey& Key:GpuFarQueue)
|
|
{
|
|
const auto InputIt=GpuFarPreparedInputs.find(Key);
|
|
if(InputIt==GpuFarPreparedInputs.end())
|
|
{Error="prepared input is missing while reserving its panel output";return false;}
|
|
const VoxelForgeCore::FarFieldTileInput& Input=InputIt->second;
|
|
const uint64 RequiredCapacity64=static_cast<uint64>(Input.connectorVertexCount)
|
|
+(Input.visibleSkirtFaceMask!=0
|
|
?VoxelForgeGpuFarViewer::NearSkirtOutputVertexCapacityPerTile:0u);
|
|
if(RequiredCapacity64>std::numeric_limits<uint32>::max())
|
|
{Error="per-tile panel output capacity exceeds the 32-bit vertex limit";return false;}
|
|
const uint32 RequiredCapacity=static_cast<uint32>(RequiredCapacity64);
|
|
auto RangeIt=GpuFarPanelOutputRanges.find(Key);
|
|
if(RangeIt!=GpuFarPanelOutputRanges.end()
|
|
&&RangeIt->second.CapacityVertices>=RequiredCapacity)continue;
|
|
if(RangeIt!=GpuFarPanelOutputRanges.end())ReleaseGpuFarPanelOutputRange(Key);
|
|
FGpuFarPanelOutputRange Range{};
|
|
if(!AllocateGpuFarPanelOutputRange(RequiredCapacity,Range))
|
|
{Error="panel output pool exceeds the 32-bit vertex limit";return false;}
|
|
GpuFarPanelOutputRanges.emplace(Key,Range);
|
|
}
|
|
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
|
|
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),Error,
|
|
GpuFarPanelOutputHighWater))return false;
|
|
GpuFarOutputPoolReservedSlots=std::max(GpuFarOutputPoolReservedSlots,
|
|
GpuFarOutputCapacitySlots);
|
|
GpuFarPanelOutputReservedCapacityVertices=std::max(
|
|
GpuFarPanelOutputReservedCapacityVertices,GpuFarPanelOutputHighWater);
|
|
std::printf("[GpuPanelOutputPool] queued_tiles=%zu live_ranges=%zu capacity_vertices=%u high_water=%u status=reserved\n",
|
|
GpuFarQueue.size(),GpuFarPanelOutputRanges.size(),
|
|
GpuFarPanelOutputReservedCapacityVertices,GpuFarPanelOutputHighWater);
|
|
return true;
|
|
}
|
|
|
|
void ScheduleGpuFarTiles(const std::vector<FVoxelTileKey>& Keys)
|
|
{
|
|
if(GpuFarPreparationState)
|
|
GpuFarPreparationState->bCancelled.store(true,std::memory_order_release);
|
|
if(Streamer)Streamer->ClearPriorityTasks();
|
|
GpuFarPreparationState.reset();
|
|
{
|
|
std::lock_guard Lock(GpuFarPreparedResultsMutex);
|
|
GpuFarPreparationGeneration.fetch_add(1,std::memory_order_acq_rel);
|
|
GpuFarPreparedResults.clear();
|
|
}
|
|
GpuFarQueue.clear();GpuFarQueued.clear();GpuFarEligibleTiles.clear();
|
|
GpuFarPreparedInputs.clear();GpuFarPreparationErrors.clear();GpuFarPreparedAt.clear();
|
|
GpuFarCandidateTiles.clear();GpuFarBaseCpuFallbackTiles.clear();
|
|
GpuFarCpuFallbackTiles.clear();GpuFarNeedPreparation.clear();
|
|
bGpuFarPreparationStarted=false;bGpuFarInputsFinalized=false;
|
|
bGpuFarPreparationDeferredReported=false;
|
|
bGpuFarFirstPreparedInputReported=false;
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
bGpuWarmupRoutePending=false;
|
|
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld())return;
|
|
std::vector<FVoxelTileKey> CpuTiles;CpuTiles.reserve(Keys.size());
|
|
std::vector<FVoxelTileKey> Eligible;Eligible.reserve(Keys.size());
|
|
uint64 NearGpuSkirtTiles=0,SkirtTransitionFallbackTiles=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(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&&!bGpuNearSkirtsEnabled)
|
|
{
|
|
// Keep the costly near skirt tiles on CPU; measured GPU savings did not cover their input and dispatch cost.
|
|
CpuTiles.push_back(Key);
|
|
++SkirtTransitionFallbackTiles;
|
|
}
|
|
else
|
|
{
|
|
if(VisibleSkirtFaces!=0&&NearGpuEligible)++NearGpuSkirtTiles;
|
|
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=GpuFarPanelOutputRanges.begin();It!=GpuFarPanelOutputRanges.end();)
|
|
{
|
|
if(CurrentEligible.contains(It->first)){++It;continue;}
|
|
const FVoxelTileKey Key=It->first;++It;
|
|
ReleaseGpuFarPanelOutputRange(Key);
|
|
}
|
|
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(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);
|
|
GpuFarBaseCpuFallbackTiles=CpuTiles;
|
|
GpuFarCpuFallbackTiles=std::move(CpuTiles);
|
|
bGpuWarmupRoutePending=bGpuFarShaderPrewarm&&!bGpuFarAuditEnabled
|
|
&&!bGpuWarmupCompleted&&!bGpuFarWarmupBeforeWorkers
|
|
&&!GpuFarCandidateTiles.empty();
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[ColdTimeline] event=gpu_output_pool_candidates at_ms=%.3f slots=%u candidates=%zu warmup_handoff_pending=%s\n",
|
|
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),
|
|
GpuFarOutputCapacitySlots,GpuFarCandidateTiles.size(),
|
|
bGpuWarmupRoutePending?"yes":"no");
|
|
if(bGpuWarmupRoutePending)
|
|
{
|
|
GpuFarCpuFallbackTiles.reserve(GpuFarCpuFallbackTiles.size()+GpuFarCandidateTiles.size());
|
|
GpuFarCpuFallbackTiles.insert(GpuFarCpuFallbackTiles.end(),
|
|
GpuFarCandidateTiles.begin(),GpuFarCandidateTiles.end());
|
|
}
|
|
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 warmup_cpu_fallback=%s\n",
|
|
GpuFarCandidateTiles.size(),NearGpuTiles,GpuFarCpuFallbackTiles.size(),
|
|
bGpuWarmupRoutePending?"yes":"no");
|
|
std::printf("[GpuSkirtSeamRouting] near_skirt_gpu_tiles=%llu near_cpu_fallback_tiles=%llu\n",
|
|
static_cast<unsigned long long>(NearGpuSkirtTiles),
|
|
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;
|
|
if(bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupDeferredStarted)return;
|
|
if(GpuFarCandidateTiles.empty())
|
|
{
|
|
bGpuFarPreparationStarted=true;
|
|
bGpuFarInputsFinalized=true;
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
if(GpuFarOutputPoolReservedSlots<GpuFarOutputCapacitySlots)
|
|
{
|
|
const auto ReserveStart=std::chrono::steady_clock::now();
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool begin_ms=%.3f slots=%u reason=reserve_before_warmup_handoff\n",
|
|
ColdTimelineMilliseconds(ReserveStart),GpuFarOutputCapacitySlots);
|
|
std::string PoolError;
|
|
if(!VoxelForgeGpuFarViewer::ReserveFarOutputPool(Device,Context,GpuFarShaderPath,
|
|
GpuFarOutputCapacitySlots,static_cast<uint32>(World->CoarseTileCells),PoolError,0))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarPoolError] %s\n",PoolError.c_str());
|
|
bGpuFarEnabled=false;bGpuFarPreparationStarted=true;
|
|
GpuFarOutputCapacitySlots=0;GpuFarOutputPoolReservedSlots=0;
|
|
GpuFarPanelOutputReservedCapacityVertices=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;
|
|
}
|
|
const double ReserveMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ReserveStart).count();
|
|
GpuFarPoolReserveMs+=ReserveMs;
|
|
GpuFarOutputPoolReservedSlots=GpuFarOutputCapacitySlots;
|
|
std::printf("[ColdTimeline] stage=gpu_output_pool end_ms=%.3f duration_ms=%.3f reserved_slots=%u\n",
|
|
ColdTimelineMilliseconds(std::chrono::steady_clock::now()),ReserveMs,
|
|
GpuFarOutputPoolReservedSlots);
|
|
}
|
|
if(bGpuWarmupRoutePending&&!bGpuWarmupCompleted)
|
|
{
|
|
if(!bGpuFarPreparationDeferredReported)
|
|
{
|
|
bGpuFarPreparationDeferredReported=true;
|
|
std::printf("[GpuFarPrepDeferred] reason=shader_warmup_cpu_tiles_own_candidates tiles=%zu output_pool_reserved_slots=%u\n",
|
|
GpuFarCandidateTiles.size(),GpuFarOutputPoolReservedSlots);
|
|
}
|
|
return;
|
|
}
|
|
bGpuFarPreparationStarted=true;
|
|
const auto PoolEnd=std::chrono::steady_clock::now();
|
|
const auto MemorySampleStart=std::chrono::steady_clock::now();
|
|
SampleGpuFarMemory();
|
|
const double MemorySampleMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-MemorySampleStart).count();
|
|
if(GpuFarNeedPreparation.empty())
|
|
{
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_relaxed);
|
|
return;
|
|
}
|
|
const auto PreparationListStart=std::chrono::steady_clock::now();
|
|
std::vector<FVoxelTileKey> NeedPreparation=GpuFarNeedPreparation;
|
|
std::stable_sort(NeedPreparation.begin(),NeedPreparation.end(),
|
|
[&](const FVoxelTileKey& A,const FVoxelTileKey& B){return GpuFarKeyLess(A,B);});
|
|
const double PreparationListMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-PreparationListStart).count();
|
|
const auto DispatchAt=std::chrono::steady_clock::now();
|
|
auto Batch=std::make_shared<FGpuFarPreparationBatch>();
|
|
Batch->DispatchAt=DispatchAt;Batch->TileCount=NeedPreparation.size();
|
|
Batch->CpuReadyAt=CpuReadyFrameBeforeGpuAt;
|
|
Batch->SharedWorkerCount=Streamer?static_cast<std::size_t>(Streamer->GetWorkerCount()):0;
|
|
Batch->Generation=GpuFarPreparationGeneration.load(std::memory_order_acquire);
|
|
Batch->Remaining.store(NeedPreparation.size(),std::memory_order_relaxed);
|
|
GpuFarPreparationState=Batch;
|
|
bGpuFarPreparationFinished.store(false,std::memory_order_relaxed);
|
|
if(bGpuFarTimelineEnabled)
|
|
std::printf("[ColdTimeline] event=gpu_input_export_priority_queue at_ms=%.3f after_pool_ms=%.3f cpu_ready_to_queue_ms=%.3f memory_sample_ms=%.3f list_copy_sort_ms=%.3f tiles=%zu shared_workers=%zu\n",
|
|
ColdTimelineMilliseconds(DispatchAt),
|
|
std::chrono::duration<double,std::milli>(DispatchAt-PoolEnd).count(),
|
|
CpuReadyFrameBeforeGpuAt.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double,std::milli>(DispatchAt-CpuReadyFrameBeforeGpuAt).count():-1.0,
|
|
MemorySampleMs,PreparationListMs,NeedPreparation.size(),Batch->SharedWorkerCount);
|
|
constexpr std::size_t GpuInputPriorityBurst=3;
|
|
std::printf("[GpuFarSchedulingPolicy] workers=%zu gpu_input_priority_burst=%zu cpu_mesh_jobs_per_burst=1 mode=shared_worker_pool\n",
|
|
Batch->SharedWorkerCount,GpuInputPriorityBurst);
|
|
std::vector<std::function<void()>> Tasks;Tasks.reserve(NeedPreparation.size());
|
|
for(const FVoxelTileKey& Key:NeedPreparation)
|
|
{
|
|
const uint8 VisibleSkirtFaceMask=World->bGenerateSkirts&&bGpuNearSkirtsEnabled
|
|
?ComputeVisibleSkirtFaceMask(Key,DesiredTileSet):0;
|
|
Tasks.emplace_back([this,Key,VisibleSkirtFaceMask,Batch]()
|
|
{PrepareGpuFarInputTile(Key,VisibleSkirtFaceMask,Batch);});
|
|
}
|
|
if(!Streamer||!Streamer->QueuePriorityTasks(std::move(Tasks)))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarInputPrepareError] shared worker queue is unavailable\n");
|
|
GpuFarPreparationState.reset();bGpuFarEnabled=false;
|
|
bGpuFarInputsFinalized=true;
|
|
bGpuFarPreparationFinished.store(true,std::memory_order_release);
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
}
|
|
}
|
|
|
|
bool GpuFarKeyLess(const FVoxelTileKey& A,const FVoxelTileKey& B)const
|
|
{
|
|
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;
|
|
}
|
|
|
|
void SortGpuFarQueue()
|
|
{
|
|
std::sort(GpuFarQueue.begin(),GpuFarQueue.end(),
|
|
[&](const FVoxelTileKey& A,const FVoxelTileKey& B){return GpuFarKeyLess(A,B);});
|
|
GpuFarQueued.clear();for(const FVoxelTileKey& Key:GpuFarQueue)GpuFarQueued.insert(Key);
|
|
}
|
|
|
|
bool CompleteGpuShaderWarmupAndRoute()
|
|
{
|
|
if(!bGpuFarShaderPrewarm||bGpuWarmupCompleted)return bGpuFarEnabled;
|
|
if(!bGpuShaderWarmupQueueFinished.load(std::memory_order_acquire))return false;
|
|
if(bGpuFarTimelineEnabled)
|
|
{
|
|
const auto ObservedAt=std::chrono::steady_clock::now();
|
|
const double CpuReadyToObservedMs=CpuReadyFrameBeforeGpuAt.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double,std::milli>(ObservedAt-CpuReadyFrameBeforeGpuAt).count():-1.0;
|
|
std::printf("[ColdTimeline] event=gpu_warmup_completion_observed at_ms=%.3f queue_finished_at_ms=%.3f observe_delay_ms=%.3f cpu_ready_frame_presented=%s cpu_ready_to_observed_ms=%.3f frame=%llu\n",
|
|
ColdTimelineMilliseconds(ObservedAt),ColdTimelineMilliseconds(GpuShaderWarmupQueueFinishedAt),
|
|
std::chrono::duration<double,std::milli>(ObservedAt-GpuShaderWarmupQueueFinishedAt).count(),
|
|
CpuReadyFrameBeforeGpuAt.time_since_epoch().count()!=0?"yes":"no",
|
|
CpuReadyToObservedMs,static_cast<unsigned long long>(ColdTimelineFrame()));
|
|
}
|
|
const auto WarmupStart=std::chrono::steady_clock::now();
|
|
if(GpuShaderWarmupThread.joinable())GpuShaderWarmupThread.join();
|
|
if(!bGpuShaderWarmupQueueSucceeded)
|
|
{
|
|
std::fprintf(stderr,"[GpuShaderWarmupError] queue: %s\n",
|
|
GpuShaderWarmupQueueError.empty()?"warmup queue failed":GpuShaderWarmupQueueError.c_str());
|
|
bGpuFarEnabled=false;bGpuWarmupRoutePending=false;
|
|
SetGpuWarmupContextProtection(false);
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
return false;
|
|
}
|
|
std::string WarmupError;
|
|
if(!VoxelForgeGpuFarViewer::CompleteFarShaderWarmup(Device,Context,
|
|
GpuFarShaderPath,WarmupError))
|
|
{
|
|
std::fprintf(stderr,"[GpuShaderWarmupError] %s\n",WarmupError.c_str());
|
|
bGpuFarEnabled=false;bGpuWarmupRoutePending=false;
|
|
SetGpuWarmupContextProtection(false);
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
return false;
|
|
}
|
|
bGpuWarmupCompleted=true;
|
|
MarkGpuWarmupCacheReady();
|
|
SetGpuWarmupContextProtection(false);
|
|
std::printf("[GpuShaderWarmupComplete] completion_wall_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-WarmupStart).count());
|
|
|
|
if(!bGpuWarmupRoutePending||!Streamer)return true;
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
std::vector<FVoxelTileKey> RetainedCandidates;
|
|
const FStandaloneStreamingUpdate Update=Streamer->RestrictDesiredTileFilterKeepingStarted(
|
|
GpuFarBaseCpuFallbackTiles,GpuFarCandidateTiles,ResidentKeys,RetainedCandidates);
|
|
for(const auto& Pair:Update.CancelledByCrossing)
|
|
{
|
|
auto WorkIt=CrossingWork.find(Pair.first);
|
|
if(WorkIt==CrossingWork.end())continue;
|
|
FCrossingWork& Work=WorkIt->second;
|
|
Work.PendingJobs=Work.PendingJobs>=Pair.second?Work.PendingJobs-Pair.second:0;
|
|
Work.CancelledQueuedJobs+=Pair.second;
|
|
}
|
|
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> CpuOwned(
|
|
RetainedCandidates.begin(),RetainedCandidates.end());
|
|
const std::size_t CandidateCountBefore=GpuFarCandidateTiles.size();
|
|
GpuFarCandidateTiles.erase(std::remove_if(GpuFarCandidateTiles.begin(),GpuFarCandidateTiles.end(),
|
|
[&](const FVoxelTileKey& Key){return CpuOwned.contains(Key);}),GpuFarCandidateTiles.end());
|
|
GpuFarNeedPreparation.erase(std::remove_if(GpuFarNeedPreparation.begin(),GpuFarNeedPreparation.end(),
|
|
[&](const FVoxelTileKey& Key){return CpuOwned.contains(Key);}),GpuFarNeedPreparation.end());
|
|
GpuFarCpuFallbackTiles=GpuFarBaseCpuFallbackTiles;
|
|
GpuFarCpuFallbackTiles.insert(GpuFarCpuFallbackTiles.end(),
|
|
RetainedCandidates.begin(),RetainedCandidates.end());
|
|
GpuFarWarmupCpuClaimedTiles=RetainedCandidates.size();
|
|
GpuFarWarmupGpuTakeoverTiles=GpuFarCandidateTiles.size();
|
|
bGpuWarmupRoutePending=false;
|
|
const bool bHandoffValid=RetainedCandidates.size()+GpuFarCandidateTiles.size()==CandidateCountBefore;
|
|
std::printf("[GpuWarmupHandoff] cpu_started_ready_or_resident=%zu gpu_takeover_unstarted=%zu cancelled_cpu_queue_jobs=%d cpu_filter_tiles=%zu status=%s\n",
|
|
RetainedCandidates.size(),GpuFarCandidateTiles.size(),Update.CancelledQueuedJobs,
|
|
GpuFarCpuFallbackTiles.size(),bHandoffValid?"PASS":"FAIL");
|
|
return true;
|
|
}
|
|
|
|
bool FinalizeGpuFarPreparation()
|
|
{
|
|
if(bGpuFarInputsFinalized)return true;
|
|
if(!bGpuFarPreparationStarted)return false;
|
|
std::deque<FPreparedGpuFarInputResult> ReadyResults;
|
|
{
|
|
std::lock_guard Lock(GpuFarPreparedResultsMutex);
|
|
ReadyResults.swap(GpuFarPreparedResults);
|
|
}
|
|
std::vector<FVoxelTileKey> Failed;
|
|
bool bQueueChanged=false;
|
|
for(FPreparedGpuFarInputResult& Result:ReadyResults)
|
|
{
|
|
const FVoxelTileKey& Key=Result.Key;
|
|
if(Result.Input)
|
|
{
|
|
if(Result.Input->nearDetailEnabled)
|
|
{
|
|
++GpuFarPreparedNearTileInputs;
|
|
GpuFarNearLatticeSamples+=Result.Input->nearPackedProxy.size();
|
|
GpuFarNearPackedCandidateSamples+=Result.Input->nearPackedCandidateSampleCount;
|
|
GpuFarNearPackedSkippedSamples+=Result.Input->nearPackedSkippedSampleCount;
|
|
}
|
|
else ++GpuFarPreparedFarTileInputs;
|
|
GpuFarProtectedRouteSamples+=Result.Input->protectedSampleCount;
|
|
GpuFarProtectedThinSamples+=Result.Input->protectedThinSampleCount;
|
|
GpuFarProtectedSolidSamples+=Result.Input->protectedSolidSampleCount;
|
|
GpuFarProtectedSealSamples+=Result.Input->protectedSealSampleCount;
|
|
GpuFarProtectedOverlaySamples+=Result.Input->protectedSolidOverlaySampleCount;
|
|
GpuFarPreparedInputs.emplace(Key,std::move(*Result.Input));
|
|
GpuFarPreparedAt.emplace(Key,Result.FinishedAt);
|
|
GpuFarEligibleTiles.insert(Key);
|
|
if(!GpuFarTiles.contains(Key)&&GpuFarQueued.insert(Key).second)
|
|
{
|
|
GpuFarQueue.push_back(Key);
|
|
bQueueChanged=true;
|
|
}
|
|
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(Result.FinishedAt),
|
|
static_cast<unsigned long long>(Result.FinishedFrame));
|
|
if(!bGpuFarFirstPreparedInputReported)
|
|
{
|
|
bGpuFarFirstPreparedInputReported=true;
|
|
std::printf("[ColdTimeline] event=gpu_first_input_ready key=(%d,%d,%d,lod%d) at_ms=%.3f\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
ColdTimelineMilliseconds(Result.FinishedAt));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GpuFarPreparationErrors.emplace(Key,Result.Error.empty()
|
|
?"far-field input export failed":std::move(Result.Error));
|
|
Failed.push_back(Key);
|
|
}
|
|
}
|
|
if(bQueueChanged)SortGpuFarQueue();
|
|
if(!Failed.empty())
|
|
{
|
|
for(const FVoxelTileKey& Key:Failed)
|
|
{
|
|
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()?"far-field input export failed":Error->second.c_str());
|
|
}
|
|
AddGpuFarCpuFallbackTiles(Failed);
|
|
}
|
|
if(!bGpuFarPreparationFinished.load(std::memory_order_acquire))
|
|
return !GpuFarQueue.empty();
|
|
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;
|
|
}
|
|
for(const FVoxelTileKey& Key:GpuFarCandidateTiles)
|
|
{
|
|
if(GpuFarTiles.contains(Key))
|
|
{GpuFarEligibleTiles.insert(Key);continue;}
|
|
if(GpuFarEligibleTiles.contains(Key)||GpuFarPreparationErrors.contains(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);
|
|
if(GpuFarQueued.insert(Key).second)GpuFarQueue.push_back(Key);
|
|
}
|
|
if(!GpuFarQueue.empty())SortGpuFarQueue();
|
|
AddGpuFarCpuFallbackTiles(Failed);
|
|
std::string PanelPoolError;
|
|
if(!ReserveGpuFarPanelOutputPool(PanelPoolError))
|
|
{
|
|
std::fprintf(stderr,"[GpuFarPoolError] panel-output reservation failed: %s\n",
|
|
PanelPoolError.c_str());
|
|
bGpuFarEnabled=false;GpuFarQueue.clear();GpuFarQueued.clear();
|
|
GpuFarEligibleTiles.clear();
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
bGpuFarInputsFinalized=true;
|
|
return false;
|
|
}
|
|
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(double MaxPumpBudgetMilliseconds=8.0)
|
|
{
|
|
if(!bGpuFarEnabled||!ShapeWorld||!ShapeWorld->hasFarWorld()||!Device||!Context)return;
|
|
ResolveGpuWarmupLaunchPolicy(false);
|
|
if(bGpuWarmupDeferredUntilFirstView&&!bGpuWarmupDeferredStarted)
|
|
{
|
|
// On an ordinary first-launch cache miss, let the complete CPU
|
|
// clipmap reach the front buffer before starting cold shader
|
|
// warmup. Starting after only the first CPU frame competes with
|
|
// the remaining CPU mesh work and regresses startup. The survival
|
|
// audit has GPU-owned candidates and needs the first CPU frame to
|
|
// break its CPU/GPU readiness cycle.
|
|
const bool bDeferredWarmupGateSatisfied=bGpuFarAuditEnabled
|
|
?bCpuReadyFramePresentedBeforeGpu:bWholeViewPresented;
|
|
if(!bDeferredWarmupGateSatisfied)return;
|
|
std::string DeferredPipelineError;
|
|
if(!EnsureGpuFarPipeline(DeferredPipelineError))
|
|
{
|
|
std::fprintf(stderr,"[GpuPipelineInitError] deferred_after_CPU_ready_gate: %s\n",
|
|
DeferredPipelineError.c_str());
|
|
bGpuFarEnabled=false;bGpuWarmupDeferredStarted=true;
|
|
AddGpuFarCpuFallbackTiles(DesiredTiles);
|
|
return;
|
|
}
|
|
bGpuWarmupDeferredStarted=true;
|
|
std::printf("[GpuWarmupDeferredStart] at_ms=%.3f gate=%s\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count(),
|
|
bGpuFarAuditEnabled?"first_CPU_frame_presented":"whole_CPU_view_presented");
|
|
StartGpuShaderWarmupAsync();
|
|
}
|
|
std::string PipelineError;
|
|
if(!EnsureGpuFarPipeline(PipelineError))
|
|
{bGpuFarEnabled=false;AddGpuFarCpuFallbackTiles(DesiredTiles);return;}
|
|
if(bGpuFarShaderPrewarm&&!bGpuWarmupDeferredUntilFirstView
|
|
&&!bGpuWarmupCompleted&&!GpuShaderWarmupThread.joinable())
|
|
StartGpuShaderWarmupAsync();
|
|
StartGpuFarPreparation();
|
|
if(!CompleteGpuShaderWarmupAndRoute())return;
|
|
StartGpuFarPreparation();
|
|
if(!bGpuFarEnabled||!FinalizeGpuFarPreparation()||!bGpuFarInputsFinalized
|
|
||GpuFarQueue.empty())return;
|
|
const bool bColdDrainBeforeWholeView=!bInitialClipmapReported&&!bGpuFarAuditEnabled
|
|
&&!bDisableGpuColdStartDrain&&bGpuFarWarmupBeforeWorkers;
|
|
constexpr std::size_t StartupFirstGpuBatchTarget=32;
|
|
constexpr std::size_t StartupGpuBatchTarget=384;
|
|
constexpr double StartupGpuPumpBudgetMilliseconds=16.0;
|
|
const bool bWaitForStreamingGpuBatch=!bColdDrainBeforeWholeView
|
|
&&!bGpuFarAuditEnabled&&!bInitialClipmapReported;
|
|
const std::size_t GpuPumpTileLimit=bGpuFarAuditEnabled?StartupFirstGpuBatchTarget
|
|
:(GpuFarSubmissionBatches==0?StartupFirstGpuBatchTarget:StartupGpuBatchTarget);
|
|
const double GpuPumpBudgetMilliseconds=std::min(MaxPumpBudgetMilliseconds,
|
|
bWaitForStreamingGpuBatch?StartupGpuPumpBudgetMilliseconds:4.0);
|
|
if(bWaitForStreamingGpuBatch
|
|
&&!bGpuFarPreparationFinished.load(std::memory_order_acquire)
|
|
&&GpuFarQueue.size()<StartupFirstGpuBatchTarget)return;
|
|
if(bWaitForStreamingGpuBatch&&GpuFarSubmissionBatches==0)
|
|
std::printf("[GpuFarStreamingPolicy] first_batch_tiles=%zu start_threshold=%zu later_batch_limit=%zu per_draw_budget_ms=80.0 per_batch_build_budget_ms=%.1f submit_batches_per_draw=until_budget_or_input_starvation order=lod_then_camera_distance\n",
|
|
GpuFarQueue.size(),StartupFirstGpuBatchTarget,StartupGpuBatchTarget,
|
|
StartupGpuPumpBudgetMilliseconds);
|
|
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
|
|
&&!(bAuditGpuSkirtDrawArgs&&bDrawOnlyTile);
|
|
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():GpuPumpTileLimit);
|
|
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;
|
|
const bool bAuditDrawOnlyGpuTile=bAuditGpuSkirtDrawArgs&&bDrawOnlyTile
|
|
&&IsDrawOnlyTile(Key);
|
|
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;
|
|
Tile.InputCells=PreparedInput->cells;Tile.InputStep=PreparedInput->step;
|
|
Tile.bNearDetailInput=PreparedInput->nearDetailEnabled;
|
|
Tile.ProtectedSamples=PreparedInput->protectedSampleCount;
|
|
Tile.ProtectedThinSamples=PreparedInput->protectedThinSampleCount;
|
|
Tile.ProtectedSolidSamples=PreparedInput->protectedSolidSampleCount;
|
|
Tile.NearProxySamples=static_cast<uint32>(std::min<std::size_t>(
|
|
PreparedInput->nearPackedProxy.size(),std::numeric_limits<uint32>::max()));
|
|
Tile.ConnectorRings=PreparedInput->connectorRingCount;
|
|
Tile.ConnectorVertices=PreparedInput->connectorVertexCount;
|
|
Tile.VisibleSkirtFaceMask=PreparedSkirtFaceMask;
|
|
const auto PanelRangeIt=GpuFarPanelOutputRanges.find(Key);
|
|
if(PanelRangeIt==GpuFarPanelOutputRanges.end())
|
|
{
|
|
GpuFarFreeOutputSlots.push_back(Tile.OutputSlot);
|
|
std::fprintf(stderr,"[GpuFarPoolError] panel-output range is missing for key=(%d,%d,%d,lod%d)\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level);
|
|
GpuFarPanelOutputRanges.erase(Key);
|
|
bGpuFarEnabled=false;
|
|
break;
|
|
}
|
|
const FGpuFarPanelOutputRange PanelRange=PanelRangeIt->second;
|
|
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)||bAuditDrawOnlyGpuTile)?&AuditMesh:nullptr,
|
|
&EnqueueMs,&GpuDispatchMs,Error,true,
|
|
PanelRange.BaseVertex,PanelRange.CapacityVertices))
|
|
{
|
|
GpuFarFreeOutputSlots.push_back(Tile.OutputSlot);
|
|
ReleaseGpuFarPanelOutputRange(Key);
|
|
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(bAuditDrawOnlyGpuTile)
|
|
std::printf("[GpuPooledOutputReadback] key=(%d,%d,%d,lod%d) output_slot=%u indirect_offset=%u surface_indices=%u output_vertices=%zu output_indices=%zu sequential_indices=%d indirect_args_valid=%d vertex_stride_bytes=%u status=%s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,Tile.OutputSlot,
|
|
Tile.IndirectArgsByteOffset,AuditMesh.surfaceVertexCount,
|
|
AuditMesh.vertices.size(),AuditMesh.indices.size(),
|
|
AuditMesh.sequentialIndices,AuditMesh.indirectArgsValid,
|
|
static_cast<unsigned>(sizeof(FGpuVertex)),
|
|
AuditMesh.vertices.size()==AuditMesh.indices.size()
|
|
&&AuditMesh.sequentialIndices&&AuditMesh.indirectArgsValid?"PASS":"FAIL");
|
|
if(bAuditDrawOnlyGpuTile)
|
|
{
|
|
std::string CpuTriangleError;
|
|
const FVector TriangleAuditCameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
const FStandaloneDesiredTileSet TriangleAuditDesired=
|
|
StandaloneWorldMesher::BuildDesiredSet(*World,TriangleAuditCameraVoxels);
|
|
int32 CpuBandMin=MIN_int32,CpuBandMax=MAX_int32;
|
|
if(World->StrateContentCutMinLevel<9&&TriangleAuditDesired.bHasStrate)
|
|
{
|
|
CpuBandMin=TriangleAuditDesired.StrateBottomChunkZ*CHUNK_SIZE;
|
|
CpuBandMax=(TriangleAuditDesired.StrateTopChunkZ+1)*CHUNK_SIZE;
|
|
}
|
|
VoxelForgeCore::TileGenerationOptions CpuTriangleOptions;
|
|
CpuTriangleOptions.bandMinZVoxel=CpuBandMin;
|
|
CpuTriangleOptions.bandMaxZVoxel=CpuBandMax;
|
|
auto CpuTriangleMesh=ShapeWorld->generateTile(CoreKey,
|
|
CpuTriangleOptions,CpuTriangleError);
|
|
if(!CpuTriangleMesh)
|
|
{
|
|
std::printf("[GpuCpuTriangleAudit] key=(%d,%d,%d,lod%d) status=FAIL error=%s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,CpuTriangleError.c_str());
|
|
}
|
|
else
|
|
{
|
|
using FPoint=std::array<double,3>;
|
|
using FTriangle=std::array<FPoint,3>;
|
|
std::vector<FTriangle> CpuTriangles,GpuTriangles,CpuTriangleNormals,
|
|
GpuTriangleNormals,CpuSkirtTriangles,GpuSkirtTriangles;
|
|
std::size_t CpuTriangleCount=0,CpuVisibleSkirtTriangleCount=0;
|
|
const std::size_t GpuSurfaceVertexCount=std::min<std::size_t>(
|
|
AuditMesh.surfaceVertexCount,AuditMesh.vertices.size());
|
|
const std::size_t GpuSkirtStart=std::min<std::size_t>(
|
|
GpuSurfaceVertexCount+AuditMesh.connectorPanelVertexCount,
|
|
AuditMesh.vertices.size());
|
|
const std::size_t GpuSkirtEnd=std::min<std::size_t>(
|
|
GpuSkirtStart+AuditMesh.skirtVertexCount,AuditMesh.vertices.size());
|
|
const std::size_t GpuTriangleCount=GpuSurfaceVertexCount/3;
|
|
CpuTriangles.reserve(CpuTriangleMesh->indices.size()/3);
|
|
GpuTriangles.reserve(GpuTriangleCount);
|
|
CpuSkirtTriangles.reserve(CpuTriangleMesh->groups.skirtTriangleCount);
|
|
GpuSkirtTriangles.reserve((GpuSkirtEnd-GpuSkirtStart)/3);
|
|
for(std::size_t I=0;I+2<CpuTriangleMesh->indices.size();I+=3)
|
|
{
|
|
const std::size_t TriangleIndex=I/3;
|
|
const uint8 Faces=TriangleIndex<CpuTriangleMesh->skirtFaceMasks.size()
|
|
?CpuTriangleMesh->skirtFaceMasks[TriangleIndex]:0;
|
|
const auto IA=CpuTriangleMesh->indices[I];
|
|
const auto IB=CpuTriangleMesh->indices[I+1];
|
|
const auto IC=CpuTriangleMesh->indices[I+2];
|
|
if(IA>=CpuTriangleMesh->vertices.size()||IB>=CpuTriangleMesh->vertices.size()
|
|
||IC>=CpuTriangleMesh->vertices.size())continue;
|
|
const auto& A=CpuTriangleMesh->vertices[IA].position;
|
|
const auto& B=CpuTriangleMesh->vertices[IB].position;
|
|
const auto& C=CpuTriangleMesh->vertices[IC].position;
|
|
if(Faces==0)
|
|
{
|
|
CpuTriangles.push_back({FPoint{A.x,A.y,A.z},FPoint{B.x,B.y,B.z},
|
|
FPoint{C.x,C.y,C.z}});
|
|
const auto& NA=CpuTriangleMesh->vertices[IA].normal;
|
|
const auto& NB=CpuTriangleMesh->vertices[IB].normal;
|
|
const auto& NC=CpuTriangleMesh->vertices[IC].normal;
|
|
CpuTriangleNormals.push_back({FPoint{NA.x,NA.y,NA.z},
|
|
FPoint{NB.x,NB.y,NB.z},FPoint{NC.x,NC.y,NC.z}});
|
|
++CpuTriangleCount;
|
|
}
|
|
else if((Faces&PreparedSkirtFaceMask)!=0)
|
|
{
|
|
CpuSkirtTriangles.push_back({FPoint{A.x,A.y,A.z},FPoint{B.x,B.y,B.z},
|
|
FPoint{C.x,C.y,C.z}});
|
|
++CpuVisibleSkirtTriangleCount;
|
|
}
|
|
}
|
|
for(std::size_t I=0;I+2<GpuSurfaceVertexCount;I+=3)
|
|
{
|
|
const auto& A=AuditMesh.vertices[I].position;
|
|
const auto& B=AuditMesh.vertices[I+1].position;
|
|
const auto& C=AuditMesh.vertices[I+2].position;
|
|
GpuTriangles.push_back({FPoint{A[0],A[1],A[2]},FPoint{B[0],B[1],B[2]},
|
|
FPoint{C[0],C[1],C[2]}});
|
|
const auto& NA=AuditMesh.vertices[I].normal;
|
|
const auto& NB=AuditMesh.vertices[I+1].normal;
|
|
const auto& NC=AuditMesh.vertices[I+2].normal;
|
|
GpuTriangleNormals.push_back({FPoint{NA[0],NA[1],NA[2]},
|
|
FPoint{NB[0],NB[1],NB[2]},FPoint{NC[0],NC[1],NC[2]}});
|
|
}
|
|
for(std::size_t I=GpuSkirtStart;I+2<GpuSkirtEnd;I+=3)
|
|
{
|
|
const auto& A=AuditMesh.vertices[I].position;
|
|
const auto& B=AuditMesh.vertices[I+1].position;
|
|
const auto& C=AuditMesh.vertices[I+2].position;
|
|
GpuSkirtTriangles.push_back({FPoint{A[0],A[1],A[2]},FPoint{B[0],B[1],B[2]},
|
|
FPoint{C[0],C[1],C[2]}});
|
|
}
|
|
const double Infinity=std::numeric_limits<double>::infinity();
|
|
FPoint CpuSkirtMinimum{Infinity,Infinity,Infinity};
|
|
FPoint CpuSkirtMaximum{-Infinity,-Infinity,-Infinity};
|
|
FPoint GpuSkirtMinimum{Infinity,Infinity,Infinity};
|
|
FPoint GpuSkirtMaximum{-Infinity,-Infinity,-Infinity};
|
|
const auto AccumulateBounds=[](const std::vector<FTriangle>& Triangles,
|
|
FPoint& Minimum,FPoint& Maximum)
|
|
{
|
|
for(const FTriangle& Triangle:Triangles)
|
|
for(const FPoint& Point:Triangle)
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
Minimum[Axis]=std::min(Minimum[Axis],Point[Axis]);
|
|
Maximum[Axis]=std::max(Maximum[Axis],Point[Axis]);
|
|
}
|
|
};
|
|
AccumulateBounds(CpuSkirtTriangles,CpuSkirtMinimum,CpuSkirtMaximum);
|
|
AccumulateBounds(GpuSkirtTriangles,GpuSkirtMinimum,GpuSkirtMaximum);
|
|
constexpr double TriangleVertexToleranceCm=0.05;
|
|
constexpr double TriangleVertexToleranceSq=
|
|
TriangleVertexToleranceCm*TriangleVertexToleranceCm;
|
|
constexpr int TrianglePermutations[6][3]={{0,1,2},{0,2,1},{1,0,2},
|
|
{1,2,0},{2,0,1},{2,1,0}};
|
|
std::vector<uint8> MatchedGpuTriangles(GpuTriangles.size(),0);
|
|
int64 Missing=0,Extra=0;
|
|
double MaxMatchedVertexErrorCm=0.0;
|
|
double MaxMatchedSurfaceNormalDelta=0.0,MinMatchedSurfaceNormalDot=1.0;
|
|
for(std::size_t CpuIndex=0;CpuIndex<CpuTriangles.size();++CpuIndex)
|
|
{
|
|
const FTriangle& CpuTriangle=CpuTriangles[CpuIndex];
|
|
double BestCost=std::numeric_limits<double>::infinity();
|
|
double BestMaxDistanceSq=0.0;
|
|
std::size_t BestGpuIndex=GpuTriangles.size();
|
|
int BestPermutation[3]={0,1,2};
|
|
for(std::size_t GpuIndex=0;GpuIndex<GpuTriangles.size();++GpuIndex)
|
|
{
|
|
if(MatchedGpuTriangles[GpuIndex])continue;
|
|
const FTriangle& GpuTriangle=GpuTriangles[GpuIndex];
|
|
for(const auto& Permutation:TrianglePermutations)
|
|
{
|
|
double Cost=0.0,MaxDistanceSq=0.0;
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
{
|
|
double DistanceSq=0.0;
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double Delta=CpuTriangle[Vertex][Axis]
|
|
-GpuTriangle[Permutation[Vertex]][Axis];
|
|
DistanceSq+=Delta*Delta;
|
|
}
|
|
Cost+=DistanceSq;MaxDistanceSq=std::max(MaxDistanceSq,DistanceSq);
|
|
}
|
|
if(MaxDistanceSq<=TriangleVertexToleranceSq&&Cost<BestCost)
|
|
{
|
|
BestCost=Cost;BestMaxDistanceSq=MaxDistanceSq;
|
|
BestGpuIndex=GpuIndex;
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
BestPermutation[Vertex]=Permutation[Vertex];
|
|
}
|
|
}
|
|
}
|
|
if(BestGpuIndex==GpuTriangles.size())++Missing;
|
|
else
|
|
{
|
|
MatchedGpuTriangles[BestGpuIndex]=1;
|
|
MaxMatchedVertexErrorCm=std::max(MaxMatchedVertexErrorCm,
|
|
std::sqrt(BestMaxDistanceSq));
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
{
|
|
const FPoint& CpuNormal=CpuTriangleNormals[CpuIndex][Vertex];
|
|
const FPoint& GpuNormal=GpuTriangleNormals[BestGpuIndex]
|
|
[BestPermutation[Vertex]];
|
|
double DeltaSq=0.0,CpuLengthSq=0.0,GpuLengthSq=0.0,Dot=0.0;
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double Delta=CpuNormal[Axis]-GpuNormal[Axis];
|
|
DeltaSq+=Delta*Delta;
|
|
CpuLengthSq+=CpuNormal[Axis]*CpuNormal[Axis];
|
|
GpuLengthSq+=GpuNormal[Axis]*GpuNormal[Axis];
|
|
Dot+=CpuNormal[Axis]*GpuNormal[Axis];
|
|
}
|
|
MaxMatchedSurfaceNormalDelta=std::max(MaxMatchedSurfaceNormalDelta,
|
|
std::sqrt(DeltaSq));
|
|
if(CpuLengthSq>1.0e-20&&GpuLengthSq>1.0e-20)
|
|
MinMatchedSurfaceNormalDot=std::min(MinMatchedSurfaceNormalDot,
|
|
Dot/std::sqrt(CpuLengthSq*GpuLengthSq));
|
|
}
|
|
}
|
|
}
|
|
for(const uint8 Matched:MatchedGpuTriangles)if(!Matched)++Extra;
|
|
std::vector<uint8> MatchedGpuSkirts(GpuSkirtTriangles.size(),0);
|
|
int64 MissingSkirts=0,ExtraSkirts=0;
|
|
double MaxMatchedSkirtVertexErrorCm=0.0;
|
|
double MinNearestUnmatchedSkirtErrorCm=Infinity;
|
|
double MaxNearestUnmatchedSkirtErrorCm=0.0;
|
|
for(const FTriangle& CpuTriangle:CpuSkirtTriangles)
|
|
{
|
|
double BestCost=std::numeric_limits<double>::infinity();
|
|
double BestMaxDistanceSq=0.0;
|
|
double NearestMaxDistanceSq=std::numeric_limits<double>::infinity();
|
|
std::size_t BestGpuIndex=GpuSkirtTriangles.size();
|
|
for(std::size_t GpuIndex=0;GpuIndex<GpuSkirtTriangles.size();++GpuIndex)
|
|
{
|
|
if(MatchedGpuSkirts[GpuIndex])continue;
|
|
const FTriangle& GpuTriangle=GpuSkirtTriangles[GpuIndex];
|
|
for(const auto& Permutation:TrianglePermutations)
|
|
{
|
|
double Cost=0.0,MaxDistanceSq=0.0;
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
{
|
|
double DistanceSq=0.0;
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double Delta=CpuTriangle[Vertex][Axis]
|
|
-GpuTriangle[Permutation[Vertex]][Axis];
|
|
DistanceSq+=Delta*Delta;
|
|
}
|
|
Cost+=DistanceSq;MaxDistanceSq=std::max(MaxDistanceSq,DistanceSq);
|
|
}
|
|
NearestMaxDistanceSq=std::min(NearestMaxDistanceSq,MaxDistanceSq);
|
|
if(MaxDistanceSq<=TriangleVertexToleranceSq&&Cost<BestCost)
|
|
{BestCost=Cost;BestMaxDistanceSq=MaxDistanceSq;BestGpuIndex=GpuIndex;}
|
|
}
|
|
}
|
|
if(BestGpuIndex==GpuSkirtTriangles.size())
|
|
{
|
|
++MissingSkirts;
|
|
if(std::isfinite(NearestMaxDistanceSq))
|
|
{
|
|
const double NearestErrorCm=std::sqrt(NearestMaxDistanceSq);
|
|
MinNearestUnmatchedSkirtErrorCm=std::min(
|
|
MinNearestUnmatchedSkirtErrorCm,NearestErrorCm);
|
|
MaxNearestUnmatchedSkirtErrorCm=std::max(
|
|
MaxNearestUnmatchedSkirtErrorCm,NearestErrorCm);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MatchedGpuSkirts[BestGpuIndex]=1;
|
|
MaxMatchedSkirtVertexErrorCm=std::max(MaxMatchedSkirtVertexErrorCm,
|
|
std::sqrt(BestMaxDistanceSq));
|
|
}
|
|
}
|
|
for(const uint8 Matched:MatchedGpuSkirts)if(!Matched)++ExtraSkirts;
|
|
if(!CpuSkirtTriangles.empty()&&!GpuSkirtTriangles.empty())
|
|
{
|
|
const FTriangle& CpuSample=CpuSkirtTriangles.front();
|
|
std::size_t NearestGpu=GpuSkirtTriangles.size();
|
|
int NearestPermutation[3]={0,1,2};
|
|
double NearestMaxDistanceSq=Infinity;
|
|
for(std::size_t GpuIndex=0;GpuIndex<GpuSkirtTriangles.size();++GpuIndex)
|
|
for(const auto& Permutation:TrianglePermutations)
|
|
{
|
|
double MaxDistanceSq=0.0;
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
{
|
|
double DistanceSq=0.0;
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
const double Delta=CpuSample[Vertex][Axis]
|
|
-GpuSkirtTriangles[GpuIndex][Permutation[Vertex]][Axis];
|
|
DistanceSq+=Delta*Delta;
|
|
}
|
|
MaxDistanceSq=std::max(MaxDistanceSq,DistanceSq);
|
|
}
|
|
if(MaxDistanceSq<NearestMaxDistanceSq)
|
|
{
|
|
NearestMaxDistanceSq=MaxDistanceSq;NearestGpu=GpuIndex;
|
|
for(int Vertex=0;Vertex<3;++Vertex)
|
|
NearestPermutation[Vertex]=Permutation[Vertex];
|
|
}
|
|
}
|
|
if(NearestGpu<GpuSkirtTriangles.size())
|
|
std::printf("[GpuCpuTriangleAuditSkirtSample] cpu=(%.3f,%.3f,%.3f)|(%.3f,%.3f,%.3f)|(%.3f,%.3f,%.3f) nearest_gpu=%zu gpu=(%.3f,%.3f,%.3f)|(%.3f,%.3f,%.3f)|(%.3f,%.3f,%.3f) max_error_cm=%.6f\n",
|
|
CpuSample[0][0],CpuSample[0][1],CpuSample[0][2],
|
|
CpuSample[1][0],CpuSample[1][1],CpuSample[1][2],
|
|
CpuSample[2][0],CpuSample[2][1],CpuSample[2][2],NearestGpu,
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[0]][0],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[0]][1],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[0]][2],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[1]][0],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[1]][1],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[1]][2],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[2]][0],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[2]][1],
|
|
GpuSkirtTriangles[NearestGpu][NearestPermutation[2]][2],
|
|
std::sqrt(NearestMaxDistanceSq));
|
|
}
|
|
std::printf("[GpuCpuTriangleAudit] key=(%d,%d,%d,lod%d) cpu_band_min_z_voxel=%d cpu_band_max_z_voxel=%d cpu_surface_triangles=%zu gpu_surface_triangles=%zu cpu_visible_skirt_triangles=%zu gpu_skirt_triangles=%zu cpu_all_skirt_triangles=%u visible_skirt_mask=0x%02x unmatched_cpu=%lld unmatched_gpu=%lld unmatched_cpu_skirts=%lld unmatched_gpu_skirts=%lld tolerance_cm=%.3f max_surface_vertex_error_cm=%.8g max_matched_surface_normal_delta=%.8g min_matched_surface_normal_dot=%.8g max_skirt_vertex_error_cm=%.8g nearest_unmatched_skirt_error_cm_min=%.8g nearest_unmatched_skirt_error_cm_max=%.8g cpu_skirt_bounds_min_cm=(%.6f,%.6f,%.6f) cpu_skirt_bounds_max_cm=(%.6f,%.6f,%.6f) gpu_skirt_bounds_min_cm=(%.6f,%.6f,%.6f) gpu_skirt_bounds_max_cm=(%.6f,%.6f,%.6f) status=%s\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,CpuBandMin,CpuBandMax,
|
|
CpuTriangleCount,GpuTriangleCount,CpuVisibleSkirtTriangleCount,
|
|
GpuSkirtTriangles.size(),
|
|
CpuTriangleMesh->groups.skirtTriangleCount,PreparedSkirtFaceMask,
|
|
static_cast<long long>(Missing),static_cast<long long>(Extra),
|
|
static_cast<long long>(MissingSkirts),static_cast<long long>(ExtraSkirts),
|
|
TriangleVertexToleranceCm,MaxMatchedVertexErrorCm,
|
|
MaxMatchedSurfaceNormalDelta,MinMatchedSurfaceNormalDot,
|
|
MaxMatchedSkirtVertexErrorCm,
|
|
std::isfinite(MinNearestUnmatchedSkirtErrorCm)
|
|
?MinNearestUnmatchedSkirtErrorCm:0.0,
|
|
MaxNearestUnmatchedSkirtErrorCm,
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMinimum[0],
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMinimum[1],
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMinimum[2],
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMaximum[0],
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMaximum[1],
|
|
CpuSkirtTriangles.empty()?0.0:CpuSkirtMaximum[2],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMinimum[0],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMinimum[1],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMinimum[2],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMaximum[0],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMaximum[1],
|
|
GpuSkirtTriangles.empty()?0.0:GpuSkirtMaximum[2],
|
|
Missing==0&&Extra==0&&MissingSkirts==0&&ExtraSkirts==0
|
|
&&CpuTriangleCount==GpuTriangleCount
|
|
&&CpuVisibleSkirtTriangleCount==GpuSkirtTriangles.size()?"PASS":"FAIL");
|
|
if(bGpuFarCpuMeshDrawAudit&&bDrawOnlyTile&&IsDrawOnlyTile(Key))
|
|
{
|
|
std::vector<FGpuVertex> CpuDrawVertices;CpuDrawVertices.reserve(
|
|
CpuTriangleMesh->vertices.size());
|
|
for(const auto& Vertex:CpuTriangleMesh->vertices)
|
|
{
|
|
FGpuVertex Copy{};
|
|
Copy.Position[0]=static_cast<float>(Vertex.position.x*0.01);
|
|
Copy.Position[1]=static_cast<float>(Vertex.position.y*0.01);
|
|
Copy.Position[2]=static_cast<float>(Vertex.position.z*0.01);
|
|
Copy.Normal[0]=static_cast<float>(Vertex.normal.x);
|
|
Copy.Normal[1]=static_cast<float>(Vertex.normal.y);
|
|
Copy.Normal[2]=static_cast<float>(Vertex.normal.z);
|
|
CpuDrawVertices.push_back(Copy);
|
|
}
|
|
std::vector<uint32> CpuDrawIndices;CpuDrawIndices.reserve(
|
|
CpuTriangleMesh->indices.size());
|
|
for(size_t I=0;I+2<CpuTriangleMesh->indices.size();I+=3)
|
|
{
|
|
const size_t TriangleIndex=I/3;
|
|
const uint8 Faces=TriangleIndex<CpuTriangleMesh->skirtFaceMasks.size()
|
|
?CpuTriangleMesh->skirtFaceMasks[TriangleIndex]:0;
|
|
if(Faces!=0&&(Faces&PreparedSkirtFaceMask)==0)continue;
|
|
CpuDrawIndices.push_back(CpuTriangleMesh->indices[I]);
|
|
CpuDrawIndices.push_back(CpuTriangleMesh->indices[I+1]);
|
|
CpuDrawIndices.push_back(CpuTriangleMesh->indices[I+2]);
|
|
}
|
|
if(!MakeBuffer(&Tile.ReadbackCopyVertexBuffer,CpuDrawVertices.data(),
|
|
CpuDrawVertices.size()*sizeof(FGpuVertex),D3D11_BIND_VERTEX_BUFFER)
|
|
||!MakeBuffer(&Tile.ReadbackCopyIndexBuffer,CpuDrawIndices.data(),
|
|
CpuDrawIndices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER))
|
|
{
|
|
std::fprintf(stderr,"[GpuCpuMeshDrawAudit] key=(%d,%d,%d,lod%d) buffer_creation=FAIL\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level);
|
|
bGpuFarEnabled=false;
|
|
}
|
|
else
|
|
{
|
|
Tile.ReadbackCopyIndexCount=static_cast<uint32>(CpuDrawIndices.size());
|
|
std::printf("[GpuCpuMeshDrawAudit] key=(%d,%d,%d,lod%d) surface_triangles=%zu all_skirt_triangles=%u visible_skirt_mask=0x%02x draw_indices=%u vertices=%zu path=cpu_mesh_copy\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,CpuTriangleCount,
|
|
CpuTriangleMesh->groups.skirtTriangleCount,PreparedSkirtFaceMask,
|
|
Tile.ReadbackCopyIndexCount,CpuDrawVertices.size());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if((bGpuFarReadbackCopyDrawAudit||bGpuFarMeterCopyDrawAudit)
|
|
&&bDrawOnlyTile&&IsDrawOnlyTile(Key)
|
|
&&!AuditMesh.vertices.empty()&&AuditMesh.vertices.size()==AuditMesh.indices.size())
|
|
{
|
|
std::vector<FGpuVertex> CopyVertices;CopyVertices.reserve(AuditMesh.vertices.size());
|
|
for(const auto& Vertex:AuditMesh.vertices)
|
|
{
|
|
FGpuVertex Copy{};
|
|
for(int Axis=0;Axis<3;++Axis)
|
|
{
|
|
Copy.Position[Axis]=Vertex.position[Axis]
|
|
*(bGpuFarMeterCopyDrawAudit?0.01f:1.0f);
|
|
Copy.Normal[Axis]=Vertex.normal[Axis];
|
|
}
|
|
CopyVertices.push_back(Copy);
|
|
}
|
|
if(!MakeBuffer(&Tile.ReadbackCopyVertexBuffer,CopyVertices.data(),
|
|
CopyVertices.size()*sizeof(FGpuVertex),D3D11_BIND_VERTEX_BUFFER)
|
|
||!MakeBuffer(&Tile.ReadbackCopyIndexBuffer,AuditMesh.indices.data(),
|
|
AuditMesh.indices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER))
|
|
{
|
|
std::fprintf(stderr,"[GpuReadbackCopyDrawAudit] key=(%d,%d,%d,lod%d) buffer_creation=FAIL\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level);
|
|
bGpuFarEnabled=false;
|
|
}
|
|
else
|
|
{
|
|
Tile.ReadbackCopyIndexCount=static_cast<uint32>(AuditMesh.indices.size());
|
|
std::printf("[GpuReadbackCopyDrawAudit] key=(%d,%d,%d,lod%d) indices=%u vertices=%zu coordinates=%s path=unpooled_readback_copy\n",
|
|
Key.Coord.X,Key.Coord.Y,Key.Coord.Z,Key.Level,
|
|
Tile.ReadbackCopyIndexCount,CopyVertices.size(),
|
|
bGpuFarMeterCopyDrawAudit?"meters":"centimeters");
|
|
}
|
|
}
|
|
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;
|
|
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)
|
|
{
|
|
ReportGpuFarSlowDispatchTile(Key,Tile,GpuDispatchMs);
|
|
GpuFarGpuDispatchMs+=GpuDispatchMs;
|
|
if(GpuDispatchMs>GpuFarGpuDispatchMaxMs)
|
|
{
|
|
GpuFarGpuDispatchMaxMs=GpuDispatchMs;
|
|
GpuFarGpuDispatchMaxKey=Key;bGpuFarGpuDispatchMaxKeyValid=true;
|
|
}
|
|
Tile.bTimingResolved=true;
|
|
}
|
|
++GpuFarTilesSubmitted;
|
|
if(Key.Level<World->FullResClipLevels)++GpuFarNearTilesSubmitted;
|
|
else ++GpuFarFarTilesSubmitted;
|
|
++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<GpuPumpTileLimit&&std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-Start).count()
|
|
<GpuPumpBudgetMilliseconds)));
|
|
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;
|
|
Batch.SubmitStartedAt=std::chrono::steady_clock::now();
|
|
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&&!bDeferredBatch)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=cpu_submit batch=%u begin_ms=%.3f end_ms=%.3f cpu_submit_ms=%.3f frame=%llu tile_count=%u\n",
|
|
BatchIndex,ColdTimelineMilliseconds(Batch.SubmitStartedAt),
|
|
ColdTimelineMilliseconds(Batch.SubmittedAt),
|
|
std::chrono::duration<double,std::milli>(
|
|
Batch.SubmittedAt-Batch.SubmitStartedAt).count(),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount);
|
|
if(bGpuFarTimelineEnabled)std::printf("[GpuBatchTimeline] event=submitted batch=%u at_ms=%.3f frame=%llu tile_count=%u cpu_submit_begin_ms=%.3f\n",
|
|
BatchIndex,ColdTimelineMilliseconds(Batch.SubmittedAt),
|
|
static_cast<unsigned long long>(ColdTimelineFrame()),Batch.TileCount,
|
|
ColdTimelineMilliseconds(Batch.SubmitStartedAt));
|
|
++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)
|
|
{
|
|
std::string GpuInitError;
|
|
if(!EnsureGpuFarPipeline(GpuInitError))
|
|
{bGpuFarEnabled=false;return;}
|
|
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,16)),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,16),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(bAuditGpuSkirtDrawArgs&&bDrawOnlyTile&&IsDrawOnlyTile(Tile)
|
|
&&!bDrawOnlyCpuTriangleAuditReported)
|
|
{
|
|
uint64 SurfaceTriangles=0,AllSkirtTriangles=0,VisibleSkirtTriangles=0;
|
|
for(size_t Triangle=0;Triangle<Resident.Indices.size()/3;++Triangle)
|
|
{
|
|
const uint8 Faces=Triangle<Resident.TriangleSkirtFaceMasks.size()
|
|
?Resident.TriangleSkirtFaceMasks[Triangle]:0;
|
|
if(Faces==0)++SurfaceTriangles;
|
|
else
|
|
{
|
|
++AllSkirtTriangles;
|
|
if((Faces&Resident.ActiveSkirtFaceMask)!=0)++VisibleSkirtTriangles;
|
|
}
|
|
}
|
|
std::printf("[CpuDrawOnlyTileAudit] key=(%d,%d,%d,lod%d) active_skirt_mask=0x%02x surface_triangles=%llu all_skirt_triangles=%llu visible_skirt_triangles=%llu draw_only_indices=%u\n",
|
|
Tile.Coord.X,Tile.Coord.Y,Tile.Coord.Z,Tile.Level,
|
|
static_cast<unsigned>(Resident.ActiveSkirtFaceMask),
|
|
static_cast<unsigned long long>(SurfaceTriangles),
|
|
static_cast<unsigned long long>(AllSkirtTriangles),
|
|
static_cast<unsigned long long>(VisibleSkirtTriangles),
|
|
static_cast<unsigned>(Indices.size()-TileIndexStart));
|
|
bDrawOnlyCpuTriangleAuditReported=true;
|
|
}
|
|
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=!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());
|
|
if(GpuShaderWarmupThread.joinable()
|
|
&&!bGpuShaderWarmupQueueFinished.load(std::memory_order_acquire))
|
|
{
|
|
const bool bPriorityBoosted=SetThreadPriority(
|
|
GpuShaderWarmupThread.native_handle(),THREAD_PRIORITY_HIGHEST)!=0;
|
|
std::printf("[GpuShaderWarmupPriorityBoost] at_ms=%.3f reason=CPU_initial_clipmap_ready result=%s\n",
|
|
Elapsed*1000.0,bPriorityBoosted?"highest":"failed");
|
|
}
|
|
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();
|
|
double CpuMeshWorkerSeconds=0.0;
|
|
uint64 CpuMeshTiles=0,CpuNearMeshTiles=0,CpuFarMeshTiles=0;
|
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
|
{
|
|
CpuMeshWorkerSeconds+=WorkerSecondsByLevel[Level];
|
|
CpuMeshTiles+=GeneratedByLevel[Level];
|
|
if(Level<World->FullResClipLevels)CpuNearMeshTiles+=GeneratedByLevel[Level];
|
|
else CpuFarMeshTiles+=GeneratedByLevel[Level];
|
|
}
|
|
const double AverageGpuMs=GpuFarTilesSubmitted
|
|
?GpuFarGpuDispatchMs/static_cast<double>(GpuFarTilesSubmitted):0.0;
|
|
if(bGpuFarGpuDispatchMaxKeyValid)
|
|
std::printf("[GpuFarMaxDispatchTile] key=(%d,%d,%d,lod%d) dispatch_ms=%.6f\n",
|
|
GpuFarGpuDispatchMaxKey.Coord.X,GpuFarGpuDispatchMaxKey.Coord.Y,
|
|
GpuFarGpuDispatchMaxKey.Coord.Z,GpuFarGpuDispatchMaxKey.Level,
|
|
GpuFarGpuDispatchMaxMs);
|
|
std::printf("[GpuFarModeTiming] mode=%s cpu_mesh_tiles=%llu cpu_mesh_worker_sum_s=%.6f cpu_input_export_worker_sum_s=%.6f gpu_submitted_tiles=%llu gpu_busy_timestamp_sum_ms=%.3f whole_view_wall_s=%.6f warmup_cpu_claimed=%llu warmup_gpu_takeover=%llu\n",
|
|
bGpuFarAuditEnabled?"gpu_audit":"gpu_hybrid",
|
|
static_cast<unsigned long long>(CpuMeshTiles),CpuMeshWorkerSeconds,
|
|
GpuFarPreparationWorkerMs/1000.0,
|
|
static_cast<unsigned long long>(GpuFarTilesSubmitted),GpuFarGpuDispatchMs,
|
|
Elapsed,static_cast<unsigned long long>(GpuFarWarmupCpuClaimedTiles),
|
|
static_cast<unsigned long long>(GpuFarWarmupGpuTakeoverTiles));
|
|
std::printf("[GpuFarFieldConsumerSummary] gpu_near_tiles_submitted=%llu gpu_far_tiles_submitted=%llu gpu_near_inputs_prepared=%llu gpu_far_inputs_prepared=%llu cpu_near_mesh_tiles=%llu cpu_far_mesh_tiles=%llu near_gpu_lattice_samples=%llu near_packed_candidate_samples=%llu near_proxy_samples_omitted_for_gpu=%llu protected_route_samples=%llu protected_thin_sign_samples=%llu protected_solid_samples=%llu protected_stratum_seal_samples=%llu protected_solid_overlay_samples=%llu collision_source=cpu_reference hashes_source=cpu_reference\n",
|
|
static_cast<unsigned long long>(GpuFarNearTilesSubmitted),
|
|
static_cast<unsigned long long>(GpuFarFarTilesSubmitted),
|
|
static_cast<unsigned long long>(GpuFarPreparedNearTileInputs),
|
|
static_cast<unsigned long long>(GpuFarPreparedFarTileInputs),
|
|
static_cast<unsigned long long>(CpuNearMeshTiles),
|
|
static_cast<unsigned long long>(CpuFarMeshTiles),
|
|
static_cast<unsigned long long>(GpuFarNearLatticeSamples),
|
|
static_cast<unsigned long long>(GpuFarNearPackedCandidateSamples),
|
|
static_cast<unsigned long long>(GpuFarNearPackedSkippedSamples),
|
|
static_cast<unsigned long long>(GpuFarProtectedRouteSamples),
|
|
static_cast<unsigned long long>(GpuFarProtectedThinSamples),
|
|
static_cast<unsigned long long>(GpuFarProtectedSolidSamples),
|
|
static_cast<unsigned long long>(GpuFarProtectedSealSamples),
|
|
static_cast<unsigned long long>(GpuFarProtectedOverlaySamples));
|
|
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));
|
|
ReportGpuBatchTimelineSummary();
|
|
}
|
|
else
|
|
{
|
|
double CpuMeshWorkerSeconds=0.0;
|
|
uint64 CpuMeshTiles=0;
|
|
for(int32 Level=0;Level<=MaxLevels;++Level)
|
|
{
|
|
CpuMeshWorkerSeconds+=WorkerSecondsByLevel[Level];
|
|
CpuMeshTiles+=GeneratedByLevel[Level];
|
|
}
|
|
std::printf("[GpuFarModeTiming] mode=cpu cpu_mesh_tiles=%llu cpu_mesh_worker_sum_s=%.6f whole_view_wall_s=%.6f\n",
|
|
static_cast<unsigned long long>(CpuMeshTiles),CpuMeshWorkerSeconds,Elapsed);
|
|
}
|
|
ReportTileTimings();
|
|
if(GpuFarOutputCountChecks>0)
|
|
std::printf("[GpuFarSlotCapacitySummary] checked_tiles=%llu surface_overflow_tiles=%llu panel_overflow_tiles=%llu count_readback_failures=%llu capacity_vertices=%u max_required_vertices=%u max_key=(%d,%d,%d,lod%d) max_slot=%u check_path=normal_after_completion_event\n",
|
|
static_cast<unsigned long long>(GpuFarOutputCountChecks),
|
|
static_cast<unsigned long long>(GpuFarOutputOverflowCount),
|
|
static_cast<unsigned long long>(GpuFarConnectorOverflowCount),
|
|
static_cast<unsigned long long>(GpuFarOutputCountReadbackFailures),
|
|
VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile,
|
|
GpuFarMaxRequiredVertices,GpuFarMaxRequiredVertexKey.Coord.X,
|
|
GpuFarMaxRequiredVertexKey.Coord.Y,GpuFarMaxRequiredVertexKey.Coord.Z,
|
|
GpuFarMaxRequiredVertexKey.Level,GpuFarMaxRequiredOutputSlot);
|
|
std::fflush(stdout);
|
|
if(bQuitAfterClipmap&&CapturePath.empty()
|
|
&&(!bGpuFarEnabled||!bGpuFarShaderPrewarm||bGpuWarmupCompleted))
|
|
{
|
|
bQuitAfterClipmapClosePosted=true;
|
|
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)
|
|
{
|
|
// Startup holds the last CPU front buffer while the GPU route takes over.
|
|
// Allow several ready batches to be submitted in one draw before resuming it.
|
|
constexpr double StartupGpuPumpFrameBudgetMilliseconds=80.0;
|
|
const auto GpuPumpFrameStart=std::chrono::steady_clock::now();
|
|
uint64 PreviousSubmissionCount=GpuFarSubmissionBatches;
|
|
for(;;)
|
|
{
|
|
const double ElapsedGpuPumpMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-GpuPumpFrameStart).count();
|
|
const double RemainingGpuPumpMs=StartupGpuPumpFrameBudgetMilliseconds
|
|
-ElapsedGpuPumpMs;
|
|
if(RemainingGpuPumpMs<=0.0)break;
|
|
PumpGpuFarGeneration(RemainingGpuPumpMs);
|
|
if(bGpuFarAuditEnabled||bInitialClipmapReported
|
|
||GpuFarSubmissionBatches==PreviousSubmissionCount)break;
|
|
PreviousSubmissionCount=GpuFarSubmissionBatches;
|
|
}
|
|
}
|
|
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;
|
|
const bool bDepthDisabledAudit=bGpuFarDepthDisabledAudit&&bDrawOnlyTile
|
|
&&IsDrawOnlyTile(Pair.first);
|
|
if(bDepthDisabledAudit)
|
|
{
|
|
Context->OMSetDepthStencilState(DepthDisabledAuditState,0);
|
|
if(!bGpuFarDepthDisabledAuditReported)
|
|
{
|
|
bGpuFarDepthDisabledAuditReported=true;
|
|
std::printf("[GpuFarDepthDisabledAudit] key=(%d,%d,%d,lod%d) depth_test=disabled path=gpu_tile_draw\n",
|
|
Pair.first.Coord.X,Pair.first.Coord.Y,Pair.first.Coord.Z,Pair.first.Level);
|
|
}
|
|
}
|
|
const bool bDrawReadbackCopy=(bGpuFarReadbackCopyDrawAudit||bGpuFarMeterCopyDrawAudit
|
|
||bGpuFarCpuMeshDrawAudit)
|
|
&&bDrawOnlyTile
|
|
&&IsDrawOnlyTile(Pair.first)&&Tile.ReadbackCopyVertexBuffer
|
|
&&Tile.ReadbackCopyIndexBuffer;
|
|
const bool bForceDirectDraw=bGpuFarDirectDrawAudit&&bDrawOnlyTile
|
|
&&IsDrawOnlyTile(Pair.first);
|
|
uint32 DirectDrawArgs[11]{};
|
|
if(bAuditGpuSkirtDrawArgs&&!Tile.bSkirtDrawArgsAudited
|
|
&&(Tile.VisibleSkirtFaceMask!=0||(bDrawOnlyTile&&IsDrawOnlyTile(Pair.first))))
|
|
{
|
|
if(!GpuSkirtArgsStaging)
|
|
{
|
|
D3D11_BUFFER_DESC StagingDesc{};
|
|
StagingDesc.ByteWidth=VoxelForgeGpuFarViewer::PooledOutputArgsDwordsPerTile*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
|
|
+VoxelForgeGpuFarViewer::PooledOutputArgsDwordsPerTile*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);
|
|
if(bForceDirectDraw)std::copy_n(Args,11,DirectDrawArgs);
|
|
const uint32 LocalOutputSlot=Tile.IndirectArgsByteOffset
|
|
/(VoxelForgeGpuFarViewer::PooledOutputArgsDwordsPerTile*sizeof(uint32));
|
|
const uint32 ExpectedStartIndex=LocalOutputSlot*32768u;
|
|
const uint32 ExpectedSurfaceStartIndex=LocalOutputSlot
|
|
*VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile;
|
|
++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;
|
|
if(Args[0]%3u!=0||Args[0]>VoxelForgeGpuFarViewer::PooledOutputVertexCapacityPerTile
|
|
||Args[1]!=1u||Args[2]!=ExpectedSurfaceStartIndex
|
|
||Args[3]!=0u||Args[4]!=0u)
|
|
++GpuSkirtDrawAuditBadSurfaceArgs;
|
|
std::printf("[GpuSkirtDrawArgs] key=(%d,%d,%d,lod%d) face_mask=0x%02x output_slot=%u surface_indices=%u surface_instances=%u surface_start_index=%u expected_surface_start_index=%u surface_base_vertex=%u surface_start_instance=%u skirt_panel_indices=%u connector_start_index=%u expected_connector_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[1],Args[2],ExpectedSurfaceStartIndex,Args[3],Args[4],
|
|
Args[5],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=bDrawReadbackCopy?sizeof(FGpuVertex):32,Offset=0;
|
|
if(bDrawReadbackCopy)
|
|
{
|
|
if(bGpuFarMeterCopyDrawAudit||bGpuFarCpuMeshDrawAudit)
|
|
{
|
|
C.Padding[0]=1.0f;
|
|
Context->UpdateSubresource(Constants,0,nullptr,&C,0,0);
|
|
Context->VSSetConstantBuffers(0,1,&Constants);
|
|
Context->PSSetConstantBuffers(0,1,&Constants);
|
|
}
|
|
Context->IASetVertexBuffers(0,1,&Tile.ReadbackCopyVertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(Tile.ReadbackCopyIndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
Context->DrawIndexed(Tile.ReadbackCopyIndexCount,0,0);
|
|
}
|
|
else
|
|
{
|
|
Context->IASetVertexBuffers(0,1,&Tile.VertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(Tile.IndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
if(bForceDirectDraw)
|
|
{
|
|
if(DirectDrawArgs[0]&&DirectDrawArgs[1])
|
|
Context->DrawIndexed(DirectDrawArgs[0],DirectDrawArgs[2],
|
|
static_cast<INT>(DirectDrawArgs[3]));
|
|
if(DirectDrawArgs[5]&&Tile.ConnectorVertexBuffer&&Tile.ConnectorIndexBuffer)
|
|
{
|
|
Context->IASetVertexBuffers(0,1,&Tile.ConnectorVertexBuffer,&Stride,&Offset);
|
|
Context->IASetIndexBuffer(Tile.ConnectorIndexBuffer,DXGI_FORMAT_R32_UINT,0);
|
|
Context->DrawIndexed(DirectDrawArgs[5],DirectDrawArgs[7],
|
|
static_cast<INT>(DirectDrawArgs[8]));
|
|
}
|
|
std::printf("[GpuDirectDrawAudit] key=(%d,%d,%d,lod%d) surface_indices=%u surface_start_index=%u surface_base_vertex=%u panel_plus_skirt_indices=%u connector_start_index=%u path=direct\n",
|
|
Pair.first.Coord.X,Pair.first.Coord.Y,Pair.first.Coord.Z,Pair.first.Level,
|
|
DirectDrawArgs[0],DirectDrawArgs[2],DirectDrawArgs[3],DirectDrawArgs[5],DirectDrawArgs[7]);
|
|
}
|
|
else
|
|
{
|
|
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(bDepthDisabledAudit)Context->OMSetDepthStencilState(nullptr,0);
|
|
}
|
|
}
|
|
if(bAuditGpuSkirtDrawArgs&&bInitialClipmapReported&&!bGpuSkirtDrawAuditReported)
|
|
{
|
|
std::printf("[GpuSkirtDrawArgsSummary] tiles=%llu zero_index_counts=%llu overflow=%llu bad_surface_args=%llu bad_connector_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>(GpuSkirtDrawAuditBadSurfaceArgs),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditBadStartIndex),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditSumIndices),
|
|
static_cast<unsigned long long>(GpuSkirtDrawAuditMaxIndices),
|
|
(GpuSkirtDrawAuditTiles>0&&GpuSkirtDrawAuditOverflow==0
|
|
&&GpuSkirtDrawAuditBadStartIndex==0&&GpuSkirtDrawAuditBadSurfaceArgs==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;
|
|
}
|
|
if(bGpuFarEnabled&&bGpuFarShaderPrewarm&&!bGpuWarmupDeferredUntilFirstView
|
|
&&!bGpuWarmupCompleted
|
|
&&!bGpuShaderWarmupQueueFinished.load(std::memory_order_acquire))
|
|
{
|
|
if(!bGpuWarmupPresentSkipReported)
|
|
{
|
|
bGpuWarmupPresentSkipReported=true;
|
|
std::printf("[GpuWarmupPresentSkip] begin_ms=%.3f policy=skip_present_while_cold_shader_queue_is_pending\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
std::fflush(stdout);
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
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 warmup_queue_pending=1\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();
|
|
// The CPU-ready frame is the ownership handoff gate for this route.
|
|
// Keep that one presentation, then suppress presents while GPU work runs.
|
|
const bool bDidPresent=!bGpuFarNoPresent||bDeferGpuUntilCpuFrame;
|
|
const HRESULT PresentResult=bDidPresent
|
|
?SwapChain->Present(PresentSyncInterval,PresentFlags):S_FALSE;
|
|
const auto PresentCallEnd=std::chrono::steady_clock::now();
|
|
if(!bDidPresent&&!bGpuFarNoPresentReported)
|
|
{
|
|
bGpuFarNoPresentReported=true;
|
|
std::printf("[GpuPresentationControl] mode=no_present_after_cpu_handoff first_skipped_frame_ms=%.3f render_commands_still_submitted=1 gpu_tiles_submitted=%llu\n",
|
|
ColdTimelineMilliseconds(PresentCallEnd),
|
|
static_cast<unsigned long long>(GpuFarTilesSubmitted));
|
|
std::fflush(stdout);
|
|
}
|
|
if(bDidPresent&&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(bDidPresent&&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(bDidPresent&&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=bDidPresent?++PresentCallCount:0;
|
|
if(bDidPresent&&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(bDidPresent&&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(bDidPresent&&bDeferGpuUntilCpuFrame&&!StreamingBatches.empty())
|
|
{
|
|
bCpuReadyFramePresentedBeforeGpu=true;
|
|
CpuReadyFrameBeforeGpuAt=PresentCallEnd;
|
|
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;
|
|
if(bQuitAfterClipmap&&CapturePath.empty()&&bInitialClipmapReported
|
|
&&(!bGpuFarEnabled||!bGpuFarShaderPrewarm||bGpuWarmupCompleted)
|
|
&&!bQuitAfterClipmapClosePosted)
|
|
{
|
|
bQuitAfterClipmapClosePosted=true;
|
|
PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
}
|
|
};
|
|
|
|
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)
|
|
{
|
|
const UINT NewWidth=LOWORD(L),NewHeight=HIWORD(L);
|
|
const bool bResizeNeeded=static_cast<int32>(NewWidth)!=GApp->Width
|
|
||static_cast<int32>(NewHeight)!=GApp->Height;
|
|
const auto ResizeStart=std::chrono::steady_clock::now();
|
|
if(bResizeNeeded)GApp->Resize(NewWidth,NewHeight);
|
|
std::printf("[ColdTimeline] event=viewer_window_size width=%u height=%u resized=%s duration_ms=%.3f\n",
|
|
NewWidth,NewHeight,bResizeNeeded?"yes":"no",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ResizeStart).count());
|
|
}
|
|
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,16);
|
|
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,16));
|
|
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,16));
|
|
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,16)),
|
|
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,16)),
|
|
scheduler,error,&analyticForwardCovered,&forwardStats,true)
|
|
||!analyticReverse.compileTileRegion(pageTiles,static_cast<uint32>(std::clamp(workers,1,16)),
|
|
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;
|
|
}
|
|
|
|
std::int64_t WorldAssemblyFixed(double voxels)
|
|
{return static_cast<std::int64_t>(std::llround(voxels*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));}
|
|
|
|
std::int64_t WorldAssemblyWavelength(double frequency,double fallbackVoxels)
|
|
{
|
|
const double wavelength=std::isfinite(frequency)&&frequency>0.0?1.0/frequency:fallbackVoxels;
|
|
return std::max<std::int64_t>(1,WorldAssemblyFixed(wavelength));
|
|
}
|
|
|
|
bool AttachRound1WorldAssemblyPrototype(FStandaloneOwnerWorld& owner,VFJson::Value& root,
|
|
std::string& error,const std::string& manifestOutputPath)
|
|
{
|
|
if(!owner.Manager||!owner.Generator)
|
|
{error="world assembler prototype needs the resolved standalone owner world";return false;}
|
|
int32 surfaceChunkZ=0,topChunkZ=0,bottomChunkZ=0;
|
|
bool foundSurface=false;
|
|
for(int32 chunkZ=1024;chunkZ>=-4096;--chunkZ)
|
|
{
|
|
if(owner.Manager->GetGeneratorTypeForChunk(FIntVector(0,0,chunkZ))
|
|
!=ECaveGeneratorType::SurfaceWorld)continue;
|
|
surfaceChunkZ=chunkZ;
|
|
foundSurface=owner.Manager->GetStrateChunkZBounds(chunkZ,topChunkZ,bottomChunkZ);
|
|
break;
|
|
}
|
|
if(!foundSurface)
|
|
{error="world assembler prototype could not find a finite SurfaceWorld strate";return false;}
|
|
const FIntVector chunk(0,0,surfaceChunkZ);
|
|
const FSurfaceGenerationParams surface=owner.Manager->GetSurfaceParamsForChunk(chunk);
|
|
const FBiomeContext biomeContext=owner.Manager->GetBiomeContextForChunk(chunk);
|
|
const std::int64_t bottomVoxel=static_cast<std::int64_t>(bottomChunkZ)*CHUNK_SIZE;
|
|
const std::int64_t heightVoxels=static_cast<std::int64_t>(topChunkZ-bottomChunkZ+1)*CHUNK_SIZE;
|
|
const double baseHeight=static_cast<double>(bottomVoxel)
|
|
+static_cast<double>(heightVoxels)*surface.BaseGroundRelative;
|
|
const double elevation=std::max(16.0,static_cast<double>(surface.ElevationRange));
|
|
const FBiomeMapParams& map=biomeContext.Map;
|
|
|
|
VoxelForgeCore::WorldAssemblyManifest manifest;
|
|
auto& recipe=manifest.recipe;
|
|
recipe.seed=static_cast<uint32>(owner.Seed);
|
|
recipe.biomeCellSizeSubvoxels=WorldAssemblyFixed(map.CellSize);
|
|
recipe.biomeWarpSubvoxels=WorldAssemblyFixed(map.WarpStrength);
|
|
recipe.biomeWarpWavelengthSubvoxels=WorldAssemblyWavelength(map.WarpFrequency,556.0);
|
|
recipe.biomeBorderBlendSubvoxels=WorldAssemblyFixed(map.BorderBlend);
|
|
VoxelForgeCore::WorldAssemblyBiomeProfileV1 lowland;
|
|
lowland.stableId=0x1001u;
|
|
lowland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight-elevation*0.15);
|
|
lowland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.45);
|
|
lowland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency,800.0);
|
|
lowland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.12*surface.MountainStrength);
|
|
lowland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency,192.0);
|
|
lowland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(1.0,static_cast<double>(surface.SurfaceRoughness)));
|
|
lowland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency,24.0);
|
|
VoxelForgeCore::WorldAssemblyBiomeProfileV1 upland=lowland;
|
|
upland.stableId=0x1002u;
|
|
upland.baseHeightSubvoxels=WorldAssemblyFixed(baseHeight+elevation*0.20);
|
|
upland.continentAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.80);
|
|
upland.continentWavelengthSubvoxels=WorldAssemblyWavelength(surface.ContinentFrequency*0.72,1120.0);
|
|
upland.mountainAmplitudeSubvoxels=WorldAssemblyFixed(elevation*0.42*std::max(0.25,static_cast<double>(surface.MountainStrength)));
|
|
upland.mountainWavelengthSubvoxels=WorldAssemblyWavelength(surface.MountainFrequency*0.75,256.0);
|
|
upland.detailAmplitudeSubvoxels=WorldAssemblyFixed(std::max(2.0,static_cast<double>(surface.SurfaceRoughness)*1.5));
|
|
upland.detailWavelengthSubvoxels=WorldAssemblyWavelength(surface.DetailFrequency*0.8,30.0);
|
|
recipe.biomes={lowland,upland};
|
|
|
|
VoxelForgeCore::WorldAssemblyStratumPlanV1 stratum;
|
|
stratum.strateIndex=owner.Manager->GetStrateIndex(
|
|
static_cast<float>((static_cast<double>(surfaceChunkZ*CHUNK_SIZE+CHUNK_SIZE/2))*VOXEL_SIZE));
|
|
stratum.topChunkZ=topChunkZ;
|
|
stratum.bottomChunkZ=bottomChunkZ;
|
|
stratum.capHeightSubvoxels=WorldAssemblyFixed(static_cast<double>(topChunkZ+1)*CHUNK_SIZE-4.0);
|
|
stratum.capUndulationSubvoxels=0;
|
|
stratum.capWavelengthSubvoxels=WorldAssemblyFixed(512.0);
|
|
manifest.plan.strates.push_back(std::move(stratum));
|
|
if(!manifest.finalize(error))return false;
|
|
const std::string manifestText=manifest.ToJson();
|
|
if(!manifestOutputPath.empty())
|
|
{
|
|
const std::filesystem::path outputPath(manifestOutputPath);
|
|
std::error_code directoryError;
|
|
if(!outputPath.parent_path().empty())
|
|
std::filesystem::create_directories(outputPath.parent_path(),directoryError);
|
|
if(directoryError)
|
|
{error="could not create the world assembly manifest output directory";return false;}
|
|
std::ofstream output(outputPath,std::ios::binary);
|
|
if(!output)
|
|
{error="could not open the world assembly manifest output path";return false;}
|
|
output.write(manifestText.data(),static_cast<std::streamsize>(manifestText.size()));
|
|
if(!output)
|
|
{error="could not write the world assembly manifest payload";return false;}
|
|
}
|
|
VFJson::Value manifestJson;
|
|
if(!VFJson::Parser(manifestText).Parse(manifestJson,error))return false;
|
|
auto* rootObject=std::get_if<VFJson::Value::Object>(&root.Data);
|
|
if(!rootObject)
|
|
{error="owner snapshot root is not a JSON object";return false;}
|
|
(*rootObject)["world_assembly"]=std::move(manifestJson);
|
|
std::printf("[WorldAssemblerPrototype] recipe_version=%u evaluator_version=%u strata=%zu biomes=%zu strate_index=%d chunk_z=%d..%d serialized_bytes=%zu recipe_hash=%016llx plan_hash=%016llx manifest_hash=%016llx source=owner_surface_params_and_biome_map route_points=0 features=0\n",
|
|
manifest.recipe.schemaVersion,manifest.recipe.evaluatorVersion,manifest.plan.strates.size(),
|
|
manifest.recipe.biomes.size(),manifest.plan.strates.front().strateIndex,topChunkZ,bottomChunkZ,
|
|
manifestText.size(),static_cast<unsigned long long>(manifest.recipe.contentHash),
|
|
static_cast<unsigned long long>(manifest.plan.contentHash),
|
|
static_cast<unsigned long long>(manifest.contentHash));
|
|
return true;
|
|
}
|
|
|
|
int RunWorldAssemblerAudit(FStandaloneOwnerWorld& owner,
|
|
const VoxelForgeCore::ResolvedWorldSnapshot& snapshot,const std::string& captureDirectory)
|
|
{
|
|
const auto* manifest=snapshot.worldAssembly();
|
|
if(!manifest||manifest->plan.strates.empty())
|
|
{std::fprintf(stderr,"[WorldAssemblerAudit] no shipped world_assembly payload was resolved.\n");return 10;}
|
|
const auto& plan=manifest->plan.strates.front();
|
|
const VoxelForgeCore::Heightfield2D block(manifest->recipe,plan);
|
|
if(!block.isValid())
|
|
{std::fprintf(stderr,"[WorldAssemblerAudit] Heightfield2D rejected its recipe or plan.\n");return 10;}
|
|
const auto contract=block.contract();
|
|
const auto influence=block.influenceBounds();
|
|
const auto detail=block.detailEnvelope();
|
|
const auto bounds=VoxelForgeCore::AuditHeightfieldBounds(block,256,
|
|
0x574f524c44415331ull^manifest->contentHash);
|
|
std::printf("[WorldAssemblerBlock] name=Heightfield2D evaluator=%u reads=biome writes=height,rock influence_xy=global influence_z_q=%lld..%lld detail_envelope_q=%lld minimum_feature_q=%lld status=%s\n",
|
|
contract.evaluatorVersion,static_cast<long long>(influence.minimumZSubvoxels),
|
|
static_cast<long long>(influence.maximumZSubvoxels),
|
|
static_cast<long long>(detail.maximumAbsoluteDisplacementSubvoxels),
|
|
static_cast<long long>(detail.minimumFeatureSizeSubvoxels),bounds.passed?"PASS":"FAIL");
|
|
std::printf("[WorldAssemblerBoundAudit] boxes=%llu point_samples=%llu missed_bounds=%llu unknown_bounds=%llu deliberately_wrong_bound_detected=%s status=%s\n",
|
|
static_cast<unsigned long long>(bounds.boxesTested),
|
|
static_cast<unsigned long long>(bounds.pointSamples),
|
|
static_cast<unsigned long long>(bounds.missedBounds),
|
|
static_cast<unsigned long long>(bounds.unknownBounds),
|
|
bounds.negativeControlDetected?"true":"false",bounds.passed?"PASS":"FAIL");
|
|
|
|
struct Query {std::int64_t x=0,y=0,z=0;};
|
|
constexpr int side=64;
|
|
std::vector<Query> queries;
|
|
queries.reserve(side*side);
|
|
for(int y=0;y<side;++y)
|
|
for(int x=0;x<side;++x)
|
|
{
|
|
const std::int64_t xq=(static_cast<std::int64_t>(x)-side/2)*128
|
|
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t yq=(static_cast<std::int64_t>(y)-side/2)*128
|
|
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const auto column=block.point(xq,yq,0);
|
|
const std::int64_t offset=(static_cast<std::int64_t>((x+3*y)%9)-4)
|
|
*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
queries.push_back({xq,yq,column.groundHeightSubvoxels+offset});
|
|
}
|
|
std::vector<VoxelForgeCore::HeightfieldPointSample> oneWorker(queries.size());
|
|
const auto evaluate=[&](std::uint32_t workers,
|
|
std::vector<VoxelForgeCore::HeightfieldPointSample>& output)
|
|
{
|
|
const auto begin=std::chrono::steady_clock::now();
|
|
std::vector<std::thread> threads;
|
|
for(std::uint32_t worker=0;worker<workers;++worker)
|
|
{
|
|
const std::size_t first=queries.size()*worker/workers;
|
|
const std::size_t last=queries.size()*(worker+1)/workers;
|
|
auto task=[&,first,last]
|
|
{
|
|
for(std::size_t i=first;i<last;++i)
|
|
output[i]=block.point(queries[i].x,queries[i].y,queries[i].z);
|
|
};
|
|
if(workers==1)task();
|
|
else threads.emplace_back(std::move(task));
|
|
}
|
|
for(auto& thread:threads)thread.join();
|
|
return std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-begin).count();
|
|
};
|
|
const auto hashSamples=[](const std::vector<VoxelForgeCore::HeightfieldPointSample>& samples)
|
|
{
|
|
std::uint64_t hash=14695981039346656037ull;
|
|
const auto add=[&](std::uint64_t value,int bytes)
|
|
{
|
|
for(int i=0;i<bytes;++i){hash^=static_cast<uint8>(value>>(8*i));hash*=1099511628211ull;}
|
|
};
|
|
for(const auto& sample:samples)
|
|
{
|
|
add(static_cast<std::uint64_t>(sample.groundHeightSubvoxels),8);
|
|
add(static_cast<std::uint64_t>(sample.capHeightSubvoxels),8);
|
|
add(sample.biomeId,4);add(sample.neighborBiomeId,4);add(sample.biomeWeightQ16,4);
|
|
add(static_cast<std::uint64_t>(sample.densitySubvoxels),8);
|
|
}
|
|
return hash;
|
|
};
|
|
const double time1=evaluate(1,oneWorker);
|
|
const std::uint64_t hash1=hashSamples(oneWorker);
|
|
std::vector<VoxelForgeCore::HeightfieldPointSample> eightWorkers(queries.size());
|
|
const double time8=evaluate(8,eightWorkers);
|
|
const std::uint64_t hash8=hashSamples(eightWorkers);
|
|
std::vector<VoxelForgeCore::HeightfieldPointSample> sixteenWorkers(queries.size());
|
|
const double time16=evaluate(16,sixteenWorkers);
|
|
const std::uint64_t hash16=hashSamples(sixteenWorkers);
|
|
const bool deterministic=hash1==hash8&&hash1==hash16;
|
|
std::printf("[WorldAssemblerEvaluatorHash] samples=%zu W1=%016llx W8=%016llx W16=%016llx deterministic=%s\n",
|
|
queries.size(),static_cast<unsigned long long>(hash1),
|
|
static_cast<unsigned long long>(hash8),static_cast<unsigned long long>(hash16),
|
|
deterministic?"true":"false");
|
|
std::printf("[WorldAssemblerQueryTiming] samples=%zu native_W1_ms=%.3f native_W8_ms=%.3f native_W16_ms=%.3f scope=point_evaluation_only\n",
|
|
queries.size(),time1,time8,time16);
|
|
|
|
double absoluteSum=0.0,maximumError=0.0;
|
|
std::uint64_t signMismatches=0;
|
|
const auto legacyStart=std::chrono::steady_clock::now();
|
|
for(std::size_t i=0;i<queries.size();++i)
|
|
{
|
|
const float x=static_cast<float>(queries[i].x
|
|
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
const float y=static_cast<float>(queries[i].y
|
|
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
const float z=static_cast<float>(queries[i].z
|
|
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
const float legacyDensity=owner.Generator->GetDensityAt(x,y,z);
|
|
const double error=std::abs(static_cast<double>(oneWorker[i].densitySubvoxels
|
|
/static_cast<double>(VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel))-legacyDensity);
|
|
absoluteSum+=error;maximumError=std::max(maximumError,error);
|
|
if((oneWorker[i].densitySubvoxels<0)!=(legacyDensity<0.0f))
|
|
++signMismatches;
|
|
}
|
|
const double legacyMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-legacyStart).count();
|
|
std::printf("[WorldAssemblerLegacyCompare] samples=%zu mean_abs_density_error=%.4f max_abs_density_error=%.4f sign_mismatches=%llu legacy_point_queries_ms=%.3f status=MEASURED_NOT_PARITY_GATE\n",
|
|
queries.size(),absoluteSum/queries.size(),maximumError,
|
|
static_cast<unsigned long long>(signMismatches),legacyMs);
|
|
const auto centerColumn=block.point(0,0,0);
|
|
const int32 centerGroundVoxel=static_cast<int32>(std::llround(
|
|
static_cast<double>(centerColumn.groundHeightSubvoxels)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel));
|
|
VoxelForgeCore::World coreWorld(snapshot);
|
|
VoxelForgeCore::ShapeBuildRequest coreBuild;
|
|
coreBuild.minVoxel={-32,-32,centerGroundVoxel-32};
|
|
coreBuild.maxVoxel={32,32,centerGroundVoxel+32};
|
|
coreBuild.clipMinVoxel=coreBuild.minVoxel;
|
|
coreBuild.clipMaxVoxel=coreBuild.maxVoxel;
|
|
coreBuild.workerLimit=1;
|
|
coreBuild.validatePassageRoutes=false;
|
|
FStandaloneCoreScheduler scheduler;
|
|
std::string coreError;
|
|
const auto coreCompileStart=std::chrono::steady_clock::now();
|
|
const bool coreCompiled=coreWorld.compile(coreBuild,scheduler,coreError);
|
|
const double coreCompileMs=std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-coreCompileStart).count();
|
|
double coreQueryMaximumError=0.0;
|
|
std::uint64_t coreQuerySamples=0;
|
|
if(coreCompiled)
|
|
{
|
|
for(const std::int64_t dx:{-24ll,0ll,24ll})
|
|
for(const std::int64_t dy:{-24ll,0ll,24ll})
|
|
for(const std::int64_t dz:{-8ll,-1ll,0ll,1ll,8ll})
|
|
{
|
|
const std::int64_t xq=dx*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t yq=dy*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const std::int64_t zq=centerColumn.groundHeightSubvoxels
|
|
+dz*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const double expected=static_cast<double>(block.point(xq,yq,zq).densitySubvoxels)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
const double actual=coreWorld.densityAt({static_cast<double>(xq)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
|
static_cast<double>(yq)/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel,
|
|
static_cast<double>(zq)/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel});
|
|
coreQueryMaximumError=std::max(coreQueryMaximumError,std::abs(expected-actual));
|
|
++coreQuerySamples;
|
|
}
|
|
}
|
|
const bool coreQueryPass=coreCompiled&&coreQuerySamples==45&&coreQueryMaximumError<=1.0/256.0;
|
|
std::printf("[WorldAssemblerCoreQuery] compiled=%s compile_ms=%.3f samples=%llu max_abs_error=%.8f render_meshes_generated=0 readiness=%s status=%s error=%s\n",
|
|
coreCompiled?"true":"false",coreCompileMs,
|
|
static_cast<unsigned long long>(coreQuerySamples),coreQueryMaximumError,
|
|
coreQueryPass?"field_ready_without_meshes":"unavailable",coreQueryPass?"PASS":"FAIL",
|
|
coreError.empty()?"none":coreError.c_str());
|
|
if(!captureDirectory.empty())
|
|
{
|
|
constexpr int width=320,height=200;
|
|
constexpr double spanVoxels=3200.0;
|
|
const std::size_t columnCount=static_cast<std::size_t>(width)*height;
|
|
std::vector<std::uint8_t> ownerPixels(columnCount*3,0);
|
|
std::vector<std::uint8_t> nativePixels(ownerPixels.size(),0);
|
|
std::vector<double> ownerHeights(columnCount,std::numeric_limits<double>::quiet_NaN());
|
|
std::vector<double> nativeHeights(columnCount,0.0);
|
|
double minimumHeight=std::numeric_limits<double>::max();
|
|
double maximumHeight=std::numeric_limits<double>::lowest();
|
|
std::uint64_t ownerColumns=0;
|
|
for(int y=0;y<height;++y)
|
|
for(int x=0;x<width;++x)
|
|
{
|
|
const double xVoxel=(static_cast<double>(x)/(width-1)-0.5)*spanVoxels;
|
|
const double yVoxel=(static_cast<double>(y)/(height-1)-0.5)*spanVoxels;
|
|
const std::size_t column=static_cast<std::size_t>(y)*width+x;
|
|
float ownerGround=0.0f,ownerCap=0.0f;
|
|
if(owner.Generator->GetSurfaceHeightAt(static_cast<float>(xVoxel),
|
|
static_cast<float>(yVoxel),plan.topChunkZ,ownerGround,ownerCap))
|
|
{
|
|
if(std::isfinite(ownerGround))
|
|
{
|
|
ownerHeights[column]=ownerGround;
|
|
minimumHeight=std::min(minimumHeight,static_cast<double>(ownerGround));
|
|
maximumHeight=std::max(maximumHeight,static_cast<double>(ownerGround));
|
|
++ownerColumns;
|
|
}
|
|
}
|
|
const auto native=block.point(
|
|
static_cast<std::int64_t>(std::llround(xVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),
|
|
static_cast<std::int64_t>(std::llround(yVoxel*VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel)),0);
|
|
nativeHeights[column]=static_cast<double>(native.groundHeightSubvoxels)
|
|
/VoxelForgeCore::WorldAssemblySubvoxelsPerVoxel;
|
|
minimumHeight=std::min(minimumHeight,nativeHeights[column]);
|
|
maximumHeight=std::max(maximumHeight,nativeHeights[column]);
|
|
}
|
|
const auto colorHeight=[&](double heightValue,std::vector<std::uint8_t>& pixels,std::size_t offset)
|
|
{
|
|
const double t=std::clamp((heightValue-minimumHeight)
|
|
/std::max(1.0,maximumHeight-minimumHeight),0.0,1.0);
|
|
const std::array<std::array<double,3>,4> stops={{{24,68,78},{61,128,75},
|
|
{166,143,83},{232,229,209}}};
|
|
const double scaled=t*3.0;
|
|
const int segment=std::min(2,static_cast<int>(scaled));
|
|
const double blend=scaled-segment;
|
|
for(int channel=0;channel<3;++channel)
|
|
pixels[offset+channel]=static_cast<std::uint8_t>(std::clamp(std::lround(
|
|
stops[segment][channel]*(1.0-blend)+stops[segment+1][channel]*blend),0l,255l));
|
|
};
|
|
for(std::size_t column=0;column<columnCount;++column)
|
|
{
|
|
const std::size_t offset=column*3;
|
|
if(std::isfinite(ownerHeights[column]))
|
|
colorHeight(ownerHeights[column],ownerPixels,offset);
|
|
colorHeight(nativeHeights[column],nativePixels,offset);
|
|
}
|
|
std::error_code directoryError;
|
|
std::filesystem::create_directories(captureDirectory,directoryError);
|
|
const auto writePpm=[&](const std::filesystem::path& path,const std::vector<std::uint8_t>& pixels)
|
|
{
|
|
std::ofstream output(path,std::ios::binary);
|
|
if(!output)return false;
|
|
output<<"P6\n"<<width<<' '<<height<<"\n255\n";
|
|
output.write(reinterpret_cast<const char*>(pixels.data()),
|
|
static_cast<std::streamsize>(pixels.size()));
|
|
return static_cast<bool>(output);
|
|
};
|
|
const bool ownerSaved=!directoryError&&writePpm(
|
|
std::filesystem::path(captureDirectory)/"owner-reference-height.ppm",ownerPixels);
|
|
const bool nativeSaved=!directoryError&&writePpm(
|
|
std::filesystem::path(captureDirectory)/"native-prototype-height.ppm",nativePixels);
|
|
std::printf("[WorldAssemblerCapturePair] resolution=%dx%d xy_span_vox=%.0f height_range=[%.2f,%.2f] owner_columns=%llu owner_saved=%s native_saved=%s files=owner-reference-height.ppm,native-prototype-height.ppm status=%s\n",
|
|
width,height,spanVoxels,minimumHeight,maximumHeight,static_cast<unsigned long long>(ownerColumns),
|
|
ownerSaved?"true":"false",nativeSaved?"true":"false",
|
|
ownerSaved&&nativeSaved?"PASS":"FAIL");
|
|
}
|
|
std::printf("[WorldAssemblerFullViewTiming] status=NOT_MEASURED_IN_AUDIT_MODE; capture_viewer_runs_report_separately\n");
|
|
return bounds.passed&&deterministic&&coreQueryPass?0:11;
|
|
}
|
|
}
|
|
|
|
int main(int Argc,char** Argv)
|
|
{
|
|
DWORD_PTR ProcessAffinity=0,SystemAffinity=0;
|
|
if(!GetProcessAffinityMask(GetCurrentProcess(),&ProcessAffinity,&SystemAffinity))
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] GetProcessAffinityMask failed.\n");
|
|
return 2;
|
|
}
|
|
DWORD_PTR RequestedAffinity=0;
|
|
DWORD RequestedPriority=0;
|
|
const char* AffinitySource="inherited";
|
|
const char* PrioritySource="inherited";
|
|
for(int32 I=1;I<Argc;++I)
|
|
{
|
|
const std::string_view Arg=Argv[I];
|
|
if(Arg=="--affinity-all-cpu")
|
|
{
|
|
RequestedAffinity=SystemAffinity;
|
|
AffinitySource="legacy_all_cpu_argument";
|
|
}
|
|
else if(Arg.starts_with("--process-affinity="))
|
|
{
|
|
const std::string Value(Arg.substr(std::string_view("--process-affinity=").size()));
|
|
char* End=nullptr;
|
|
const unsigned long long Parsed=std::strtoull(Value.c_str(),&End,0);
|
|
if(Parsed==0||End==Value.c_str()||*End!='\0'
|
|
||Parsed>static_cast<unsigned long long>(std::numeric_limits<DWORD_PTR>::max()))
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] invalid --process-affinity value: %s\n",Value.c_str());
|
|
return 2;
|
|
}
|
|
RequestedAffinity=static_cast<DWORD_PTR>(Parsed);
|
|
AffinitySource="argument";
|
|
}
|
|
else if(Arg.starts_with("--process-priority="))
|
|
{
|
|
const std::string_view Value=Arg.substr(std::string_view("--process-priority=").size());
|
|
if(Value=="Normal")RequestedPriority=NORMAL_PRIORITY_CLASS;
|
|
else if(Value=="BelowNormal")RequestedPriority=BELOW_NORMAL_PRIORITY_CLASS;
|
|
else
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] invalid --process-priority value: %.*s\n",
|
|
static_cast<int>(Value.size()),Value.data());
|
|
return 2;
|
|
}
|
|
PrioritySource="argument";
|
|
}
|
|
}
|
|
bool bAffinitySet=true,bPrioritySet=true;
|
|
if(RequestedAffinity!=0)
|
|
{
|
|
if((RequestedAffinity&~SystemAffinity)!=0)
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] requested affinity mask is outside system mask 0x%llX.\n",
|
|
static_cast<unsigned long long>(SystemAffinity));
|
|
return 2;
|
|
}
|
|
bAffinitySet=SetProcessAffinityMask(GetCurrentProcess(),RequestedAffinity)!=0;
|
|
}
|
|
if(RequestedPriority!=0)
|
|
bPrioritySet=SetPriorityClass(GetCurrentProcess(),RequestedPriority)!=0;
|
|
if(!bAffinitySet||!bPrioritySet)
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] failed to apply requested process controls.\n");
|
|
return 2;
|
|
}
|
|
if(!GetProcessAffinityMask(GetCurrentProcess(),&ProcessAffinity,&SystemAffinity))
|
|
{
|
|
std::fprintf(stderr,"[ProcessControlsError] could not read applied process affinity.\n");
|
|
return 2;
|
|
}
|
|
const DWORD AppliedPriority=GetPriorityClass(GetCurrentProcess());
|
|
const char* PriorityName=AppliedPriority==NORMAL_PRIORITY_CLASS?"Normal":
|
|
(AppliedPriority==BELOW_NORMAL_PRIORITY_CLASS?"BelowNormal":
|
|
(AppliedPriority==ABOVE_NORMAL_PRIORITY_CLASS?"AboveNormal":
|
|
(AppliedPriority==HIGH_PRIORITY_CLASS?"High":
|
|
(AppliedPriority==REALTIME_PRIORITY_CLASS?"Realtime":"Unknown"))));
|
|
std::printf("[ProcessControls] priority_class=%s priority_source=%s affinity_mask=0x%llX affinity_source=%s requested_affinity=0x%llX system_affinity=0x%llX\n",
|
|
PriorityName,PrioritySource,static_cast<unsigned long long>(ProcessAffinity),AffinitySource,
|
|
static_cast<unsigned long long>(RequestedAffinity),static_cast<unsigned long long>(SystemAffinity));
|
|
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";
|
|
App.GpuWarmupStatePath=std::filesystem::path(ExecutablePath).parent_path()
|
|
/"VoxelForgeGpuWarmup.state";
|
|
}
|
|
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";
|
|
std::string WorldAssemblyCaptureDir;
|
|
std::string WorldAssemblyManifestOutputPath;
|
|
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-far-direct-draw-audit")App.bGpuFarDirectDrawAudit=true;
|
|
else if(Arg=="--gpu-far-readback-copy-draw-audit")App.bGpuFarReadbackCopyDrawAudit=true;
|
|
else if(Arg=="--gpu-far-meter-copy-draw-audit")App.bGpuFarMeterCopyDrawAudit=true;
|
|
else if(Arg=="--gpu-far-cpu-mesh-draw-audit")App.bGpuFarCpuMeshDrawAudit=true;
|
|
else if(Arg=="--gpu-far-depth-disabled-audit")App.bGpuFarDepthDisabledAudit=true;
|
|
else if(Arg=="--gpu-near-skirts")App.bGpuNearSkirtsEnabled=true;
|
|
else if(Arg=="--gpu-skirts-disabled")App.bGpuNearSkirtsEnabled=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=="--world-assembler-prototype")
|
|
{App.bWorldAssemblerPrototype=true;App.bShapeWorld=true;}
|
|
else if(Arg=="--world-assembler-audit")
|
|
{App.bWorldAssemblerPrototype=true;App.bWorldAssemblerAuditOnly=true;}
|
|
else if(Arg.starts_with("--world-assembler-capture-dir="))
|
|
WorldAssemblyCaptureDir=std::string(Arg.substr(
|
|
std::string_view("--world-assembler-capture-dir=").size()));
|
|
else if(Arg.starts_with("--world-assembler-manifest-output="))
|
|
WorldAssemblyManifestOutputPath=std::string(Arg.substr(
|
|
std::string_view("--world-assembler-manifest-output=").size()));
|
|
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-force-cold-start")
|
|
App.GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::ForceDeferred;
|
|
else if(Arg=="--gpu-far-force-cached-start")
|
|
App.GpuWarmupLaunchPolicy=EGpuWarmupLaunchPolicy::ForceEager;
|
|
else if(Arg=="--gpu-far-no-warmup-cache-write")
|
|
App.bGpuWarmupCacheWriteDisabled=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-present")App.bGpuFarNoPresent=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=="--walkability-audit")App.bShapeWorld=true,App.bWalkabilityAuditOnly=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,16);
|
|
}
|
|
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);
|
|
}
|
|
if(App.bWorldAssemblerPrototype
|
|
&&!AttachRound1WorldAssemblyPrototype(*App.World,App.OwnerJsonRoot,Error,
|
|
WorldAssemblyManifestOutputPath))
|
|
{std::fprintf(stderr,"World assembler prototype setup failed: %s\n",Error.c_str());return 2;}
|
|
if(App.bWorldAssemblerAuditOnly)
|
|
{
|
|
App.Snapshot=VoxelForgeCore::MakeStandaloneSnapshotFromParsedJson(App.OwnerJsonRoot,
|
|
App.Seed,App.ClipRadius,App.MaxClipLevelOverride,App.bRoundTunnels,
|
|
App.bRoundTunnelEnhanced,Error);
|
|
if(!App.Snapshot)
|
|
{std::fprintf(stderr,"World assembler snapshot setup failed: %s\n",Error.c_str());return 2;}
|
|
return RunWorldAssemblerAudit(*App.World,App.Snapshot,WorldAssemblyCaptureDir);
|
|
}
|
|
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||App.bWalkabilityAuditOnly||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.bWalkabilityAuditOnly)return 0;
|
|
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());
|
|
App.StartGpuWarmupPolicyResolutionAsync();
|
|
if(!App.bGpuFarWarmupOnly)
|
|
{
|
|
const auto WindowShowStart=std::chrono::steady_clock::now();
|
|
ShowWindow(App.Window,SW_SHOW);
|
|
std::printf("[ColdTimeline] stage=viewer_window_show duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-WindowShowStart).count());
|
|
const auto WindowUpdateStart=std::chrono::steady_clock::now();
|
|
UpdateWindow(App.Window);
|
|
std::printf("[ColdTimeline] stage=viewer_window_update duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-WindowUpdateStart).count());
|
|
const auto StartupBannerStart=std::chrono::steady_clock::now();
|
|
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::printf("[ColdTimeline] stage=viewer_startup_banner duration_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-StartupBannerStart).count());
|
|
}
|
|
if(App.bGpuFarEnabled&&(App.bGpuFarWarmupOnly||App.bGpuFarWarmupBeforeWorkers))
|
|
{
|
|
App.ResolveGpuWarmupLaunchPolicy(true);
|
|
std::string GpuInitError;
|
|
if(!App.EnsureGpuFarPipeline(GpuInitError))return 3;
|
|
}
|
|
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;}
|
|
App.MarkGpuWarmupCacheReady();
|
|
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;
|
|
}
|
|
if(App.bGpuFarWarmupBeforeWorkers)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;}
|
|
App.MarkGpuWarmupCacheReady();
|
|
App.SetGpuWarmupContextProtection(false);
|
|
App.bGpuWarmupCompleted=true;
|
|
std::printf("[GpuShaderWarmupBeforeWorkers] completed_before_generation_workers=true\n");
|
|
}
|
|
std::printf("[ColdTimeline] event=generation_setup_call at_ms=%.3f\n",
|
|
std::chrono::duration<double,std::milli>(
|
|
std::chrono::steady_clock::now()-ColdProcessStart).count());
|
|
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;
|
|
}
|