Cause found by profiling: room/SDF graph-cache churn (exact XY bounds + sample order rebuilt it constantly). Region grouping (one LOD3 tile: 1,084 -> 229 builds), taller-tile traversal for LOD6 (36,480 -> 12,160), extent-dependent cache slots, lambdas instead of recursive std::function. Serial ms/tile, 64 nearest tiles per level (before -> after): L0 50.4 -> 50.6, L1 112 -> 110, L2 24 -> 22, L3 223 -> 73, L4 269 -> 134, L5 393 -> 127, L6 283 -> 145. Hashes of all levels unchanged; parity 3,436/3,436 bit-identical. Full clipmap, 16 workers BelowNormal: full detail 66.6 s, all 12,054 tiles 175 s. In-stream L0 tail is high (median 83 ms, mean 198 ms, p95 669 ms) vs 51 ms serial. Streaming: recentre keeps the intersection, generates only missing tiles nearest/finest first; one crossing = exactly 441 new tiles in 6.1 s, no stale results. Lighting: normals already come from the density gradient; the striping was the squared headlamp; broad linear fill + warmer palette. Daytime CPU rule documented in Standalone/README.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
1150 lines
64 KiB
C++
1150 lines
64 KiB
C++
#include "OwnerWorldLoader.h"
|
|
#include "StreamingWorld.h"
|
|
#include "StandaloneResearchTunnels.h"
|
|
#include "VoxelDensityProfile.h"
|
|
|
|
#ifdef TEXT
|
|
#undef TEXT
|
|
#endif
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
|
|
#include <d3d11.h>
|
|
#include <d3dcompiler.h>
|
|
#include <dxgi.h>
|
|
#include <windowsx.h>
|
|
|
|
#include <bit>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <sstream>
|
|
#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))));}
|
|
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;
|
|
uint64 Samples=0;
|
|
double WorkerSeconds=0.0;
|
|
};
|
|
struct FStreamingGpuBatch
|
|
{
|
|
ID3D11Buffer* VertexBuffer=nullptr;
|
|
ID3D11Buffer* IndexBuffer=nullptr;
|
|
UINT IndexCount=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;
|
|
std::chrono::steady_clock::time_point StartTime{};
|
|
bool bReported=false;
|
|
};
|
|
|
|
FStandaloneBatchKey BatchForTile(const FVoxelTileKey& Tile)
|
|
{
|
|
return {Tile.Level,VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord.X,8),
|
|
VoxelClipmapDesiredTiles::FloorDiv(Tile.Coord.Y,8),Tile.Coord.Z};
|
|
}
|
|
|
|
struct FApp
|
|
{
|
|
HWND Window=nullptr;
|
|
ID3D11Device* Device=nullptr;
|
|
ID3D11DeviceContext* Context=nullptr;
|
|
IDXGISwapChain* SwapChain=nullptr;
|
|
ID3D11RenderTargetView* RenderTarget=nullptr;
|
|
ID3D11DepthStencilView* DepthView=nullptr;
|
|
ID3D11VertexShader* VertexShader=nullptr;
|
|
ID3D11PixelShader* PixelShader=nullptr;
|
|
ID3D11InputLayout* InputLayout=nullptr;
|
|
ID3D11Buffer* Constants=nullptr;
|
|
ID3D11RasterizerState* SolidRasterizer=nullptr;
|
|
ID3D11RasterizerState* WireRasterizer=nullptr;
|
|
std::unique_ptr<FStandaloneOwnerWorld> World;
|
|
std::unique_ptr<FStandaloneStreamingGenerator> Streamer;
|
|
std::string DataPath;
|
|
bool bHasCameraOverride=false;
|
|
V3 CameraPositionOverride{};
|
|
float CameraYawOverride=0.0f,CameraPitchOverride=0.0f;
|
|
std::unordered_map<FVoxelTileKey,FResidentTileMesh,FStandaloneTileKeyHash> ResidentTiles;
|
|
std::unordered_map<FStandaloneBatchKey,std::vector<FVoxelTileKey>,FStandaloneBatchKeyHash> BatchMembers;
|
|
std::unordered_map<FStandaloneBatchKey,FStreamingGpuBatch,FStandaloneBatchKeyHash> StreamingBatches;
|
|
std::unordered_set<FStandaloneBatchKey,FStandaloneBatchKeyHash> DirtyBatchSet;
|
|
std::unordered_map<uint64,FCrossingWork> CrossingWork;
|
|
std::vector<FVoxelTileKey> DesiredTiles;
|
|
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 bWireframe=false;
|
|
bool bCaptured=false;
|
|
bool bAutoCaptureWritten=false;
|
|
bool bFullDetailReported=false;
|
|
bool bInitialClipmapReported=false;
|
|
bool bProfileAttribution=false;
|
|
bool bCompareEvaluators=false;
|
|
bool bParityOnly=false;
|
|
bool bDensityHash=false;
|
|
bool bTestOneChunkCrossing=false;
|
|
bool bCrossingMoveStarted=false;
|
|
bool bCrossingMoveFinished=false;
|
|
bool bCrossingClosePosted=false;
|
|
std::chrono::steady_clock::time_point CrossingMoveStopTime{};
|
|
bool bCaptureAfterClipmap=false;
|
|
bool bQuitAfterClipmap=false;
|
|
std::string CapturePath;
|
|
std::chrono::steady_clock::time_point GenerationStart{};
|
|
bool bShowLevel[MaxLevels+1]={true,true,true,true,true,true,true,true,true};
|
|
bool Keys[256]{};
|
|
int32 Seed=0;
|
|
int32 WorkerCount=6;
|
|
int32 Width=1280,Height=800;
|
|
float Yaw=0.0f,Pitch=0.0f,MoveSpeed=8.0f;
|
|
V3 Position{};
|
|
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(){
|
|
StopGeneration(true);ReleaseMeshes();
|
|
if(SolidRasterizer)SolidRasterizer->Release();if(WireRasterizer)WireRasterizer->Release();
|
|
if(Constants)Constants->Release();if(InputLayout)InputLayout->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();
|
|
}
|
|
|
|
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();
|
|
DirtyBatchSet.clear();DesiredTiles.clear();CrossingWork.clear();
|
|
for(int32 I=0;I<=MaxLevels;++I)
|
|
{DesiredByLevel[I]=0;GeneratedByLevel[I]=0;SamplesByLevel[I]=0;WorkerSecondsByLevel[I]=0.0;
|
|
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();
|
|
bGenerationRunning=false;
|
|
}
|
|
|
|
V3 Forward() const
|
|
{return Normalize({std::sin(Yaw)*std::cos(Pitch),std::cos(Yaw)*std::cos(Pitch),std::sin(Pitch)});}
|
|
|
|
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;o.p=mul(float4(i.p,1),VP);o.world=i.p;o.n=normalize(i.n);return o;}
|
|
float4 PS(O i):SV_TARGET{
|
|
float3 N=normalize(i.n);float3 V=normalize(Camera-i.world);float3 L=normalize(-LightDir);
|
|
float Diffuse=saturate(dot(N,L));float Head=saturate(dot(N,V));
|
|
float Dist=length(Camera-i.world);float Atten=1.0-saturate(Dist/95.0);
|
|
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;
|
|
return Resize(Width,Height);
|
|
}
|
|
|
|
size_t PrintParity()
|
|
{
|
|
if(bParityChecked||!World)return 0;
|
|
bParityChecked=true;size_t MismatchCount=0;
|
|
std::map<std::string,size_t> Kinds;
|
|
for(size_t I=0;I<World->DensitySamples.size();++I)
|
|
{
|
|
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::fflush(stdout);
|
|
std::fflush(stdout);
|
|
return MismatchCount;
|
|
}
|
|
|
|
bool ConfigureWorld(std::string& Error)
|
|
{
|
|
World=std::make_unique<FStandaloneOwnerWorld>();
|
|
if(!StandaloneOwnerLoader::Load(DataPath,*World,Error))return false;
|
|
if(!bParityChecked&&Seed==World->Seed&&!bRoundTunnels)PrintParity();
|
|
World->Generator.reset();World->Manager.reset();
|
|
World->Settings->Seed=Seed;World->Seed=Seed;
|
|
if(bRoundTunnels)
|
|
{
|
|
// The research tunnel graph replaces the owner's first-strate corridor graph.
|
|
for(auto& Pair:World->Settings->FixedStrates)
|
|
{
|
|
UVoxelStrateDefinition* Definition=Pair.Value.Get();
|
|
if(Pair.Key==0&&Definition&&Definition->GeneratorType==ECaveGeneratorType::TunnelNetwork)
|
|
{
|
|
Definition->GenerationParams.TunnelDensity=0.0f;
|
|
Definition->GenerationParams.TunnelWarpStrength=0.0f;
|
|
}
|
|
}
|
|
FStandaloneResearchTunnels::Set(true,Position.Z*4.0f);
|
|
}
|
|
else FStandaloneResearchTunnels::Set(false,0.0f);
|
|
World->Manager=std::make_unique<UVoxelStrateManager>();
|
|
if(!World->Manager->Initialize(World->Settings.get(),Seed))
|
|
{Error="standalone copied strate manager failed while applying viewer seed";return false;}
|
|
World->Generator=std::make_unique<UVoxelGenerator>();
|
|
World->Generator->InitializeSettings(World->Settings.get());
|
|
World->Generator->SetStrateManager(World->Manager.get());
|
|
return true;
|
|
}
|
|
|
|
void StartGeneration()
|
|
{
|
|
StopGeneration();
|
|
ReleaseMeshes();
|
|
std::string Error;
|
|
if(!ConfigureWorld(Error))
|
|
{std::fprintf(stderr,"World load failed: %s\n",Error.c_str());MessageBoxA(Window,Error.c_str(),"VoxelForge viewer",MB_ICONERROR);return;}
|
|
const FVector CameraVoxels(Position.X*4.0,Position.Y*4.0,Position.Z*4.0);
|
|
BuildTargetChunk=StandaloneWorldMesher::CameraChunk(CameraVoxels);
|
|
GenerationStart=std::chrono::steady_clock::now();
|
|
bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false;
|
|
if(bProfileAttribution)
|
|
{
|
|
VoxelDensityProfile::SetMode(VoxelDensityProfile::EMode::Attribution,128);
|
|
VoxelDensityProfile::CalibrateTimer();
|
|
VoxelDensityProfile::Reset();
|
|
std::printf("Density stage profiling enabled (Attribution, sample interval 128).\n");
|
|
}
|
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraVoxels);
|
|
DesiredTiles=Desired.Tiles;
|
|
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
|
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
|
Streamer=std::make_unique<FStandaloneStreamingGenerator>(*World,WorkerCount);
|
|
const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex);
|
|
FCrossingWork& InitialWork=CrossingWork[CrossingIndex];
|
|
InitialWork.SubmittedJobs=Update.NewJobs;InitialWork.PendingJobs=Update.NewJobs;
|
|
InitialWork.StartTime=GenerationStart;
|
|
bGenerationRunning=true;bRebuildPending=false;
|
|
std::printf("Streaming seed %d%s: %d desired tiles, %d full-detail tiles, %d workers; upload budget %d batches / %.1f ms per frame.\n",
|
|
Seed,bRoundTunnels?" with research round/winding tunnels":"",Update.DesiredTiles,
|
|
Update.FullResTiles,Streamer->GetWorkerCount(),MaxBatchUploadsPerFrame,BatchUploadBudgetMilliseconds);
|
|
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 MarkBatchDirty(const FStandaloneBatchKey& Key)
|
|
{DirtyBatchSet.insert(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(NewDesired.CenterChunk==BuildTargetChunk)return;
|
|
const FIntVector Previous=BuildTargetChunk;
|
|
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);
|
|
for(const FVoxelTileKey& Key:Leavers)RemoveResidentTile(Key);
|
|
DesiredTiles=std::move(NewDesired.Tiles);
|
|
for(int32 I=0;I<=MaxLevels;++I)DesiredByLevel[I]=0;
|
|
for(const FVoxelTileKey& Tile:DesiredTiles)++DesiredByLevel[Tile.Level];
|
|
std::vector<FVoxelTileKey> ResidentKeys;ResidentKeys.reserve(ResidentTiles.size());
|
|
for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first);
|
|
++CrossingIndex;
|
|
const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex);
|
|
for(const auto& Pair:Update.CancelledByCrossing)
|
|
{
|
|
FCrossingWork& Work=CrossingWork[Pair.first];
|
|
Work.PendingJobs=Work.PendingJobs>=Pair.second?Work.PendingJobs-Pair.second:0;
|
|
Work.CancelledQueuedJobs+=Pair.second;
|
|
}
|
|
FCrossingWork& Work=CrossingWork[CrossingIndex];
|
|
Work.SubmittedJobs=Update.NewJobs;Work.PendingJobs=Update.NewJobs;
|
|
Work.StartTime=std::chrono::steady_clock::now();
|
|
BuildTargetChunk=Update.CenterChunk;bGenerationRunning=true;
|
|
std::printf("[ChunkCrossing] id=%llu center=(%d,%d,%d) delta=(%d,%d,%d) desired=%d retained=%d new_jobs=%d dropped_resident=%zu cancelled_queued=%d full_detail=%d\n",
|
|
static_cast<unsigned long long>(CrossingIndex),Update.CenterChunk.X,Update.CenterChunk.Y,Update.CenterChunk.Z,
|
|
Update.CenterChunk.X-Previous.X,Update.CenterChunk.Y-Previous.Y,Update.CenterChunk.Z-Previous.Z,
|
|
Update.DesiredTiles,Update.RetainedResidentTiles,Update.NewJobs,Leavers.size(),
|
|
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;
|
|
VertexCount+=It->second.Vertices.size();IndexCount+=It->second.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);
|
|
for(const FVoxelTileKey& Tile:Members)
|
|
{
|
|
const auto It=ResidentTiles.find(Tile);if(It==ResidentTiles.end())continue;
|
|
const uint32 Base=static_cast<uint32>(Vertices.size());
|
|
for(const FStandaloneCpuVertex& V:It->second.Vertices)
|
|
Vertices.push_back({{V.Position[0],V.Position[1],V.Position[2]},{V.Normal[0],V.Normal[1],V.Normal[2]}});
|
|
for(const uint32 Index:It->second.Indices)Indices.push_back(Base+Index);
|
|
}
|
|
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();
|
|
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 auto Start=std::chrono::steady_clock::now();int32 Uploaded=0;
|
|
for(const FStandaloneBatchKey& Key:Dirty)
|
|
{
|
|
if(Uploaded>=MaxBatchUploadsPerFrame)break;
|
|
if(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;}
|
|
}
|
|
}
|
|
|
|
bool IsFullDetailUploaded()const
|
|
{
|
|
if(!Streamer||World->FullResClipLevels<=0)return false;
|
|
int32 ResidentFullRes=0;
|
|
for(const auto& Pair:ResidentTiles)if(Pair.first.Level<World->FullResClipLevels)++ResidentFullRes;
|
|
if(ResidentFullRes!=Streamer->FullResDesiredCount())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;
|
|
const double WallSeconds=Work.StartTime.time_since_epoch().count()!=0
|
|
?std::chrono::duration<double>(std::chrono::steady_clock::now()-Work.StartTime).count():0.0;
|
|
std::printf("[ChunkCrossingWork] id=%llu submitted=%llu finished=%llu generated=%llu cancelled_queued=%llu stale=%llu samples=%.2fM worker_sum=%.3fs wall=%.3fs avg=%.3fms\n",
|
|
static_cast<unsigned long long>(Pair.first),
|
|
static_cast<unsigned long long>(Work.SubmittedJobs),
|
|
static_cast<unsigned long long>(Work.FinishedJobs),
|
|
static_cast<unsigned long long>(Work.Tiles),
|
|
static_cast<unsigned long long>(Work.CancelledQueuedJobs),
|
|
static_cast<unsigned long long>(Work.StaleResults),
|
|
static_cast<double>(Work.Samples)/1.0e6,Work.WorkerSeconds,WallSeconds,AverageMs);
|
|
}
|
|
}
|
|
|
|
void PumpGeneration()
|
|
{
|
|
if(bRebuildPending){StartGeneration();return;}
|
|
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(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))
|
|
{++Crossing.StaleResults;continue;}
|
|
++Crossing.Tiles;
|
|
++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds;
|
|
if(bProfileAttribution)for(size_t I=0;I<Tile.ProfileCycles.size();++I)
|
|
ProfileCyclesByLevel[Level][I]+=Tile.ProfileCycles[I];
|
|
TileSecondsByLevel[Level].push_back(Tile.WorkerSeconds);
|
|
FResidentTileMesh Resident;Resident.Vertices=std::move(Tile.Vertices);Resident.Indices=std::move(Tile.Indices);
|
|
Resident.Samples=Tile.DensitySamples;Resident.WorkerSeconds=Tile.WorkerSeconds;
|
|
ResidentTiles.emplace(Tile.Key,std::move(Resident));
|
|
const FStandaloneBatchKey Batch=BatchForTile(Tile.Key);
|
|
BatchMembers[Batch].push_back(Tile.Key);MarkBatchDirty(Batch);
|
|
}
|
|
ReportCrossingWork();
|
|
UploadDirtyBatches();
|
|
if(bTestOneChunkCrossing&&bCrossingMoveFinished&&!bCrossingClosePosted
|
|
&&DirtyBatchSet.empty())
|
|
{
|
|
const auto Crossing=CrossingWork.find(1);
|
|
if(Crossing!=CrossingWork.end()&&Crossing->second.bReported)
|
|
{
|
|
bCrossingClosePosted=true;
|
|
std::printf("[CrossingTestComplete] crossing work and its bounded uploads drained; closing this test viewer.\n");
|
|
std::fflush(stdout);
|
|
PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
}
|
|
if(!bFullDetailReported&&IsFullDetailUploaded())
|
|
{
|
|
bFullDetailReported=true;
|
|
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
|
|
std::printf("[FullDetailReady] %.3f s after spawn; resident=%zu/%d tiles through LOD%d.\n",
|
|
Elapsed,ResidentTiles.size(),Streamer->FullResDesiredCount(),World->FullResClipLevels-1);
|
|
ReportTileTimings();std::fflush(stdout);
|
|
}
|
|
if(!bInitialClipmapReported&&ResidentTiles.size()==DesiredTiles.size()&&DirtyBatchSet.empty())
|
|
{
|
|
bInitialClipmapReported=true;
|
|
const double Elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-GenerationStart).count();
|
|
std::printf("[InitialClipmapReady] %.3f s after spawn; %zu tiles, %llu vertices, %llu indices, %zu GPU batches.\n",
|
|
Elapsed,ResidentTiles.size(),static_cast<unsigned long long>(TotalVertices),
|
|
static_cast<unsigned long long>(TotalIndices),StreamingBatches.size());
|
|
ReportTileTimings();std::fflush(stdout);
|
|
if(bQuitAfterClipmap&&CapturePath.empty())PostMessageA(Window,WM_CLOSE,0,0);
|
|
}
|
|
bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty();
|
|
}
|
|
|
|
void CaptureRenderedFrame()
|
|
{
|
|
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])VisibleIndices+=Pair.second.IndexCount;
|
|
std::ostringstream S;S<<"VoxelForge standalone | seed "<<Seed<<" | "<<(bRoundTunnels?"round/winding research tunnels":"base world")
|
|
<<" | "<<VisibleIndices/3<<" tris | "<<FrameMs<<" ms | "<<(bGenerationRunning?"generating":"ready")
|
|
<<" | pos "<<std::fixed<<std::setprecision(1)<<Position.X<<", "<<Position.Y<<", "<<Position.Z<<" m";
|
|
SetWindowTextA(Window,S.str().c_str());
|
|
}
|
|
|
|
void Draw()
|
|
{
|
|
PumpGeneration();
|
|
const auto Now=std::chrono::steady_clock::now();
|
|
if(bTestOneChunkCrossing&&bFullDetailReported&&!bCrossingMoveStarted&&!bCrossingMoveFinished)
|
|
{
|
|
bCrossingMoveStarted=true;
|
|
CrossingMoveStopTime=Now+std::chrono::milliseconds(350);
|
|
Keys[VK_SHIFT]=true;Keys['W']=true;
|
|
std::printf("[CrossingTestMove] moving forward for 350 ms after full detail.\n");
|
|
std::fflush(stdout);
|
|
}
|
|
if(bCrossingMoveStarted&&Now>=CrossingMoveStopTime)
|
|
{
|
|
Keys[VK_SHIFT]=false;Keys['W']=false;
|
|
bCrossingMoveStarted=false;bCrossingMoveFinished=true;
|
|
std::printf("[CrossingTestMove] movement stopped; waiting for crossing work and uploads.\n");
|
|
std::fflush(stdout);
|
|
}
|
|
const float Dt=std::clamp(std::chrono::duration<float>(Now-PreviousFrame).count(),0.0f,0.1f);
|
|
PreviousFrame=Now;FrameMs=Dt*1000.0;
|
|
const V3 F=Forward(),R=Normalize(Cross(F,{0,0,1})),U=Normalize(Cross(R,F));
|
|
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;
|
|
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]||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount)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);Context->DrawIndexed(L.IndexCount,0,0);
|
|
}
|
|
CaptureRenderedFrame();
|
|
SwapChain->Present(1,0);UpdateTitle();
|
|
}
|
|
};
|
|
|
|
FApp* GApp=nullptr;
|
|
LRESULT CALLBACK WindowProc(HWND H,UINT Message,WPARAM W,LPARAM L)
|
|
{
|
|
if(!GApp)return DefWindowProc(H,Message,W,L);
|
|
switch(Message)
|
|
{
|
|
case WM_SIZE:if(W!=SIZE_MINIMIZED)GApp->Resize(LOWORD(L),HIWORD(L));return 0;
|
|
case WM_KEYDOWN:
|
|
{
|
|
const bool bWasDown=(L&(1ll<<30))!=0;if(W<256)GApp->Keys[W]=true;
|
|
if(W==VK_ESCAPE){if(GApp->bCaptured)GApp->Capture(false);else DestroyWindow(H);}
|
|
if(!bWasDown&&W=='F')GApp->bWireframe=!GApp->bWireframe;
|
|
if(!bWasDown&&W=='T'){GApp->bRoundTunnels=!GApp->bRoundTunnels;GApp->bRebuildPending=true;}
|
|
if(!bWasDown&&(W=='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)
|
|
{
|
|
const FVector CameraVoxels(CameraPosition.X*4.0,CameraPosition.Y*4.0,CameraPosition.Z*4.0);
|
|
const FStandaloneDesiredTileSet Desired=StandaloneWorldMesher::BuildDesiredSet(World,CameraVoxels);
|
|
std::vector<FVoxelTileKey> Tiles;Tiles.reserve(static_cast<size_t>(std::max(Count,1)));
|
|
for(const FVoxelTileKey& Tile:Desired.Tiles)
|
|
if(Tile.Level==Level&&static_cast<int32>(Tiles.size())<Count)Tiles.push_back(Tile);
|
|
if(Tiles.empty())
|
|
{std::fprintf(stderr,"No LOD %d tiles are in the current desired set.\n",Level);return 4;}
|
|
|
|
VoxelGenLOD::InitializeConsoleSwitches();
|
|
UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
|
|
int32 TileCacheWindow=1,TileCacheWindowMinLOD=3,RoomGraphCacheSlots=1;
|
|
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindow=",TileCacheWindow);
|
|
FParse::Value(FCommandLine::Get(),"voxel.TileCacheWindowMinLOD=",TileCacheWindowMinLOD);
|
|
FParse::Value(FCommandLine::Get(),"voxel.RoomGraphCacheSlots=",RoomGraphCacheSlots);
|
|
std::printf("[BenchmarkConfig] tile_cache_window=%d fused_min_lod=%d room_graph_cache_slots=%d command='%s'\n",
|
|
TileCacheWindow,TileCacheWindowMinLOD,RoomGraphCacheSlots,FCommandLine::Get());
|
|
VoxelDensityProfile::SetMode(bProfile?VoxelDensityProfile::EMode::Attribution
|
|
:VoxelDensityProfile::EMode::Disabled,128);
|
|
if(bProfile)VoxelDensityProfile::CalibrateTimer();
|
|
VoxelDensityProfile::Reset();
|
|
const VoxelDensityProfile::FSnapshot Before=VoxelDensityProfile::SnapshotCurrentThread();
|
|
|
|
UVoxelMarchingCubesMesher Mesher;
|
|
Mesher.SetGenerator(World.Generator.get());Mesher.bGenerateSkirts=World.bGenerateSkirts;
|
|
Mesher.SkirtCells=static_cast<float>(World.SkirtCells);Mesher.LODOctaveDrop=World.LODOctaveDrop;
|
|
const int32 BandMin=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
|
|
?Desired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32;
|
|
const int32 BandMax=World.StrateContentCutMinLevel<9&&Desired.bHasStrate
|
|
?(Desired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32;
|
|
if(bCompareEvaluators)
|
|
{
|
|
const bool bOriginalFused=VoxelGenLOD::IsUseFusedEvaluatorEnabled();
|
|
uint64 Mismatches=0,ComparedSamples=0,MeshCountMismatches=0;
|
|
float MaxAbsoluteDifference=0.0f;
|
|
size_t FirstMismatch=SIZE_MAX;
|
|
FVoxelTileKey FirstMismatchTile{};
|
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
|
|
for(const FVoxelTileKey& Tile:Tiles)
|
|
{
|
|
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
|
|
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
|
|
std::vector<float> FusedDensities,OpStackDensities;
|
|
int64 FusedSampleCalls=0,OpStackSampleCalls=0;
|
|
FVoxelMeshData FusedMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
BandMin,BandMax,&FusedSampleCalls,nullptr,nullptr,&FusedDensities);
|
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(false);
|
|
FVoxelMeshData OpStackMesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
BandMin,BandMax,&OpStackSampleCalls,nullptr,nullptr,&OpStackDensities);
|
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(true);
|
|
if(FusedMesh.Vertices.Num()!=OpStackMesh.Vertices.Num()
|
|
|| FusedMesh.Triangles.Num()!=OpStackMesh.Triangles.Num())++MeshCountMismatches;
|
|
const size_t SharedSamples=std::min(FusedDensities.size(),OpStackDensities.size());
|
|
ComparedSamples+=SharedSamples;
|
|
for(size_t I=0;I<SharedSamples;++I)
|
|
{
|
|
if(std::bit_cast<uint32>(FusedDensities[I])!=std::bit_cast<uint32>(OpStackDensities[I]))
|
|
{
|
|
if(FirstMismatch==SIZE_MAX){FirstMismatch=I;FirstMismatchTile=Tile;}
|
|
++Mismatches;
|
|
MaxAbsoluteDifference=std::max(MaxAbsoluteDifference,
|
|
std::abs(FusedDensities[I]-OpStackDensities[I]));
|
|
}
|
|
}
|
|
Mismatches+=static_cast<uint64>(std::max(FusedDensities.size(),OpStackDensities.size())-SharedSamples);
|
|
}
|
|
std::printf("[EvaluatorCompare] LOD=%d tiles=%zu compared_samples=%llu density_mismatches=%llu first_tile=(%d,%d,%d) first_index=%llu max_abs=%.9g mesh_count_mismatch_tiles=%llu original_fused=%d\n",
|
|
Level,Tiles.size(),static_cast<unsigned long long>(ComparedSamples),
|
|
static_cast<unsigned long long>(Mismatches),
|
|
FirstMismatchTile.Coord.X,FirstMismatchTile.Coord.Y,FirstMismatchTile.Coord.Z,
|
|
static_cast<unsigned long long>(FirstMismatch),MaxAbsoluteDifference,
|
|
static_cast<unsigned long long>(MeshCountMismatches),bOriginalFused?1:0);
|
|
VoxelGenLOD::SetUseFusedEvaluatorEnabled(bOriginalFused);
|
|
std::fflush(stdout);
|
|
return Mismatches==0?0:5;
|
|
}
|
|
std::vector<double> TileSeconds;TileSeconds.reserve(Tiles.size());
|
|
uint64 Samples=0,Vertices=0,Indices=0;
|
|
uint64 DensityHash=14695981039346656037ull;
|
|
const auto TotalStart=std::chrono::steady_clock::now();
|
|
for(const FVoxelTileKey& Tile:Tiles)
|
|
{
|
|
const auto TileStart=std::chrono::steady_clock::now();
|
|
const int32 Cells=Level<World.FullResClipLevels?CHUNK_SIZE:FMath::Max(1,World.CoarseTileCells);
|
|
const int32 Step=FMath::Max(1,Tile.ExtentVoxels()/Cells);
|
|
int64 TileSamples=0;
|
|
std::vector<float> DebugDensitySamples;
|
|
FVoxelMeshData Mesh=Mesher.GenerateMesh(Tile.OriginVoxels(),Step,Cells,nullptr,
|
|
BandMin,BandMax,&TileSamples,nullptr,nullptr,
|
|
bDensityHash?&DebugDensitySamples:nullptr);
|
|
Samples+=static_cast<uint64>(FMath::Max<int64>(0,TileSamples));
|
|
Vertices+=static_cast<uint64>(Mesh.Vertices.Num());
|
|
Indices+=static_cast<uint64>(Mesh.Triangles.Num());
|
|
if(bDensityHash)
|
|
{
|
|
for(const float Density:DebugDensitySamples)
|
|
{
|
|
DensityHash^=std::bit_cast<uint32>(Density);
|
|
DensityHash*=1099511628211ull;
|
|
}
|
|
}
|
|
TileSeconds.push_back(std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-TileStart).count());
|
|
}
|
|
const double TotalSeconds=std::chrono::duration<double>(
|
|
std::chrono::steady_clock::now()-TotalStart).count();
|
|
const VoxelDensityProfile::FSnapshot After=VoxelDensityProfile::SnapshotCurrentThread();
|
|
std::sort(TileSeconds.begin(),TileSeconds.end());
|
|
const double Average=TotalSeconds/static_cast<double>(Tiles.size());
|
|
std::printf("[TileBenchmark] LOD=%d generated=%zu samples=%.2fM worker_sum=%.3fs avg=%.3fms p50=%.3fms p95=%.3fms vertices=%llu indices=%llu profiling=%s\n",
|
|
Level,Tiles.size(),static_cast<double>(Samples)/1.0e6,TotalSeconds,Average*1000.0,
|
|
TileSeconds[(TileSeconds.size()-1)*50/100]*1000.0,
|
|
TileSeconds[(TileSeconds.size()-1)*95/100]*1000.0,
|
|
static_cast<unsigned long long>(Vertices),static_cast<unsigned long long>(Indices),
|
|
bProfile?"Attribution":"off");
|
|
if(bDensityHash)
|
|
{
|
|
std::printf("[TileDensityHash] LOD=%d tiles=%zu hash=0x%016llx\n",Level,Tiles.size(),
|
|
static_cast<unsigned long long>(DensityHash));
|
|
}
|
|
if(bProfile)
|
|
{
|
|
std::printf("[TileProfile] LOD=%d",Level);
|
|
for(int32 I=0;I<VoxelDensityProfile::BucketCount;++I)
|
|
{
|
|
const uint64 CycleDelta=After.Cycles[I]>=Before.Cycles[I]
|
|
?After.Cycles[I]-Before.Cycles[I]:0;
|
|
const double Ms=static_cast<double>(CycleDelta)
|
|
*FPlatformTime::GetSecondsPerCycle64()*1000.0;
|
|
if(Ms>0.001)std::printf(" %s=%.3fms",VoxelDensityProfile::BucketName(
|
|
static_cast<VoxelDensityProfile::EBucket>(I)),Ms);
|
|
}
|
|
std::printf("\n");
|
|
const auto CounterDelta=[&](VoxelDensityProfile::ECounter Counter)
|
|
{
|
|
const size_t I=static_cast<size_t>(Counter);
|
|
return After.Counters[I]>=Before.Counters[I]
|
|
?After.Counters[I]-Before.Counters[I]:0;
|
|
};
|
|
std::printf("[TileCounters] sdf_builds=%llu graph_tile_builds=%llu graph_core_builds=%llu op_block_builds=%llu\n",
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfCacheBuild)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTile)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::RoomGraphBuildGeneratorTunnelCore)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::OpBlockBuilds)));
|
|
std::printf("[SdfRebuildCauses] strate=%llu seed=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_policy=%llu tile_policy=%llu tile_identity=%llu bounds_x=%llu bounds_y=%llu tile_window_accepted=%llu tile_window_rejected=%llu\n",
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildStrate)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildSeed)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFingerprint)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildLayout)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildManagerLifetime)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTerrainOps)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildFusedPolicy)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTilePolicy)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildTileIdentity)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsX)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfRebuildBoundsY)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowAccepted)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::SdfTileWindowRejected)));
|
|
std::printf("[TilePolicyKeyChanges] origin=%llu step=%llu cells=%llu fingerprint=%llu layout=%llu manager=%llu terrain_ops=%llu fused_path=%llu context=%llu\n",
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyOrigin)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyStep)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyCells)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFingerprint)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyLayout)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyManager)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyTerrainOps)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyFusedPath)),
|
|
static_cast<unsigned long long>(CounterDelta(VoxelDensityProfile::ECounter::TilePolicyKeyContext)));
|
|
}
|
|
std::fflush(stdout);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
int main(int Argc,char** Argv)
|
|
{
|
|
FApp App;App.DataPath=DefaultDataPath().string();
|
|
std::string JoinedCommandLine;
|
|
for(int32 I=1;I<Argc;++I)
|
|
{if(!JoinedCommandLine.empty())JoinedCommandLine.push_back(' ');JoinedCommandLine+=Argv[I];}
|
|
FCommandLine::Set(JoinedCommandLine);
|
|
int32 BenchmarkLevel=-1,BenchmarkCount=64;
|
|
for(int32 I=1;I<Argc;++I)
|
|
{
|
|
const std::string_view Arg=Argv[I];
|
|
if(Arg.starts_with("--capture="))App.CapturePath=std::string(Arg.substr(10));
|
|
else if(Arg=="--profile-attribution")App.bProfileAttribution=true;
|
|
else if(Arg=="--compare-evaluators")App.bCompareEvaluators=true;
|
|
else if(Arg=="--parity-only")App.bParityOnly=true;
|
|
else if(Arg=="--density-hash")App.bDensityHash=true;
|
|
else if(Arg=="--round-tunnels")App.bRoundTunnels=true;
|
|
else if(Arg=="--test-one-chunk-crossing")App.bTestOneChunkCrossing=true;
|
|
else if(Arg=="--capture-after-clipmap")App.bCaptureAfterClipmap=true;
|
|
else if(Arg=="--quit-after-clipmap")App.bQuitAfterClipmap=true;
|
|
else if(Arg.starts_with("--workers="))
|
|
{
|
|
const int32 Parsed=std::atoi(std::string(Arg.substr(10)).c_str());
|
|
App.WorkerCount=std::clamp(Parsed,1,32);
|
|
}
|
|
else if(Arg.starts_with("--benchmark-level="))
|
|
BenchmarkLevel=std::atoi(std::string(Arg.substr(18)).c_str());
|
|
else if(Arg.starts_with("--benchmark-count="))
|
|
BenchmarkCount=std::max(1,std::atoi(std::string(Arg.substr(18)).c_str()));
|
|
else if(Arg.starts_with("--camera="))
|
|
{
|
|
const std::string Values(Arg.substr(9));
|
|
float X=0,Y=0,Z=0,YawDegrees=0,PitchDegrees=0;
|
|
if(std::sscanf(Values.c_str(),"%f,%f,%f,%f,%f",&X,&Y,&Z,&YawDegrees,&PitchDegrees)==5)
|
|
{
|
|
App.bHasCameraOverride=true;App.CameraPositionOverride={X,Y,Z};
|
|
App.CameraYawOverride=YawDegrees*0.017453292519943295f;
|
|
App.CameraPitchOverride=PitchDegrees*0.017453292519943295f;
|
|
}
|
|
else {std::fprintf(stderr,"Invalid --camera value: %s\n",Values.c_str());return 2;}
|
|
}
|
|
else if(!Arg.empty()&&Arg.front()!='-')App.DataPath=std::string(Arg);
|
|
}
|
|
std::string Error;
|
|
App.World=std::make_unique<FStandaloneOwnerWorld>();
|
|
if(!StandaloneOwnerLoader::Load(App.DataPath,*App.World,Error)){std::fprintf(stderr,"Owner data load failed: %s\n",Error.c_str());return 2;}
|
|
App.Seed=App.World->Seed;
|
|
if(App.bParityOnly)
|
|
{
|
|
VoxelGenLOD::InitializeConsoleSwitches();
|
|
return App.PrintParity()==0?0:1;
|
|
}
|
|
App.Position={static_cast<float>(App.World->ViewerStartVoxels.X*0.25),
|
|
static_cast<float>(App.World->ViewerStartVoxels.Y*0.25),static_cast<float>(App.World->ViewerStartVoxels.Z*0.25)};
|
|
if(App.bHasCameraOverride){App.Position=App.CameraPositionOverride;App.Yaw=App.CameraYawOverride;App.Pitch=App.CameraPitchOverride;}
|
|
if(BenchmarkLevel>=0)
|
|
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
|
|
App.bProfileAttribution,App.bCompareEvaluators,App.bDensityHash);
|
|
App.World.reset();
|
|
|
|
WNDCLASSA Class{};Class.lpfnWndProc=WindowProc;Class.hInstance=GetModuleHandleA(nullptr);Class.lpszClassName="VoxelForgeStandaloneWorld";
|
|
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;
|
|
if(!App.Window||!App.InitializeDevice()){MessageBoxA(nullptr,"Direct3D 11 initialization failed.","VoxelForge standalone",MB_ICONERROR);return 3;}
|
|
ShowWindow(App.Window,SW_SHOW);UpdateWindow(App.Window);
|
|
std::printf("Controls: hold right mouse to look; WASD move; Space/Ctrl (E/Q) up/down; Shift faster; wheel changes speed; F wireframe; 1-9 toggle LOD; 0 toggle all levels; N/P change seed; T swap to round/winding research tunnels; Esc quit.\n");
|
|
std::printf("Owner data: %s; starting at first-strate open-space sample (%.2f, %.2f, %.2f) m.\n",
|
|
App.DataPath.c_str(),App.Position.X,App.Position.Y,App.Position.Z);
|
|
std::fflush(stdout);
|
|
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;
|
|
}
|