diff --git a/Standalone/Build.ps1 b/Standalone/Build.ps1 index bb7923a..7444552 100644 --- a/Standalone/Build.ps1 +++ b/Standalone/Build.ps1 @@ -1,10 +1,14 @@ [CmdletBinding()] -param() +param([string]$OutputName = 'VoxelForgeStandalone.exe') $ErrorActionPreference = 'Stop' $Root = [IO.Path]::GetFullPath($PSScriptRoot) $BuildDir = Join-Path $Root 'Build' $BinDir = Join-Path $Root 'bin' +if ([IO.Path]::GetFileName($OutputName) -ne $OutputName -or [IO.Path]::GetExtension($OutputName) -ne '.exe') { + throw "OutputName must be an executable filename, not a path: $OutputName" +} +$OutputPath = Join-Path $BinDir $OutputName [IO.Directory]::CreateDirectory($BuildDir) | Out-Null [IO.Directory]::CreateDirectory($BinDir) | Out-Null $VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' @@ -42,7 +46,7 @@ $RspLines = @( ('/I"{0}"' -f (Join-Path $Root 'Source')), ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Public')), ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')), - ('/Fe"{0}"' -f (Join-Path $BinDir 'VoxelForgeStandalone.exe')) + ('/Fe"{0}"' -f $OutputPath) ) foreach ($Source in $Sources) { $RspLines += ('"{0}"' -f $Source) } $RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib') @@ -56,4 +60,4 @@ try { } finally { Pop-Location } -Write-Host "Built $((Join-Path $BinDir 'VoxelForgeStandalone.exe'))" +Write-Host "Built $OutputPath" diff --git a/Standalone/Compat/CoreMinimal.h b/Standalone/Compat/CoreMinimal.h index 03796ca..ed71dc6 100644 --- a/Standalone/Compat/CoreMinimal.h +++ b/Standalone/Compat/CoreMinimal.h @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -88,13 +90,192 @@ template using TFunctionRef = std::function; template constexpr T&& Forward(std::remove_reference_t& Value){return std::forward(Value);} template constexpr T&& Forward(std::remove_reference_t&& Value){return std::forward(Value);} +struct FCompatAllocationHeader +{ + std::pmr::memory_resource* Pool=nullptr; + void* Base=nullptr; + std::size_t Bytes=0; + std::size_t Alignment=0; +}; + +inline std::pmr::memory_resource* FCompatThreadPool() +{ + // A synchronized pool can return a worker's allocations from the result-drain thread. Keep + // one process-lifetime pool per allocating thread, matching Unreal's per-thread small bins. + thread_local auto* Pool=new std::pmr::synchronized_pool_resource(); + return Pool; +} + +template +class FCompatAllocator +{ +public: + using value_type=T; + using is_always_equal=std::true_type; + using propagate_on_container_move_assignment=std::true_type; + FCompatAllocator() noexcept=default; + template FCompatAllocator(const FCompatAllocator&) noexcept {} + + [[nodiscard]] T* allocate(std::size_t Count) + { + if(Count>std::numeric_limits::max()/sizeof(T))throw std::bad_array_new_length(); + const std::size_t Bytes=Count*sizeof(T); + const std::size_t Alignment=std::max(alignof(T),alignof(FCompatAllocationHeader)); + const std::size_t Total=Bytes+sizeof(FCompatAllocationHeader)+Alignment; + std::pmr::memory_resource* Pool=Bytes<=65536?FCompatThreadPool():nullptr; + void* Base=Pool?Pool->allocate(Total,Alignment): ::operator new(Total,std::align_val_t(Alignment)); + void* Candidate=static_cast(Base)+sizeof(FCompatAllocationHeader); + std::size_t Space=Total-sizeof(FCompatAllocationHeader); + void* Data=std::align(alignof(T),Bytes,Candidate,Space); + if(!Data) + { + if(Pool)Pool->deallocate(Base,Total,Alignment); + else ::operator delete(Base,std::align_val_t(Alignment)); + throw std::bad_alloc(); + } + auto* Header=reinterpret_cast(static_cast(Data)-sizeof(FCompatAllocationHeader)); + ::new (Header) FCompatAllocationHeader{Pool,Base,Total,Alignment}; + return static_cast(Data); + } + + void deallocate(T* Data,std::size_t) noexcept + { + if(!Data)return; + auto* Header=reinterpret_cast(reinterpret_cast(Data)-sizeof(FCompatAllocationHeader)); + const FCompatAllocationHeader Allocation=*Header; + Header->~FCompatAllocationHeader(); + if(Allocation.Pool)Allocation.Pool->deallocate(Allocation.Base,Allocation.Bytes,Allocation.Alignment); + else ::operator delete(Allocation.Base,std::align_val_t(Allocation.Alignment)); + } + + template bool operator==(const FCompatAllocator&)const noexcept{return true;} + template bool operator!=(const FCompatAllocator&)const noexcept{return false;} +}; + +template struct FCompatInlineCapacity { static constexpr std::size_t Value=0; }; +template struct FCompatInlineCapacity> +{ static constexpr std::size_t Value=static_cast(N); }; + +template> +class FCompatSmallVector +{ +public: + using value_type=T; + using iterator=T*; + using const_iterator=const T*; + using reference=T&; + using const_reference=const T&; + + FCompatSmallVector()=default; + FCompatSmallVector(const FCompatSmallVector& Other){assign(Other.begin(),Other.end());} + FCompatSmallVector(FCompatSmallVector&& Other) noexcept + { + if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;} + else for(T& Value:Other)emplace_back(std::move(Value)); + Other.clear(); + } + FCompatSmallVector& operator=(const FCompatSmallVector& Other) + {if(this!=&Other)assign(Other.begin(),Other.end());return *this;} + FCompatSmallVector& operator=(FCompatSmallVector&& Other) noexcept + { + if(this==&Other)return *this; + clear(); + if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;} + else for(T& Value:Other)emplace_back(std::move(Value)); + Other.clear();return *this; + } + ~FCompatSmallVector(){clear();} + + T* data(){return bHeap?Heap.data():InlineData();} + const T* data()const{return bHeap?Heap.data():InlineData();} + std::size_t size()const{return bHeap?Heap.size():InlineSize;} + bool empty()const{return size()==0;} + std::size_t capacity()const{return bHeap?Heap.capacity():InlineCount;} + std::size_t heap_capacity()const{return bHeap?Heap.capacity():0;} + iterator begin(){return data();} iterator end(){return data()+size();} + const_iterator begin()const{return data();} const_iterator end()const{return data()+size();} + reference operator[](std::size_t I){return data()[I];} + const_reference operator[](std::size_t I)const{return data()[I];} + reference back(){return data()[size()-1];} const_reference back()const{return data()[size()-1];} + + void reserve(std::size_t Count) + { + if(Count<=capacity())return; + if(bHeap){Heap.reserve(Count);return;} + std::vector NewHeap;NewHeap.reserve(Count); + for(std::size_t I=0;ICount)InlineData()[--InlineSize].~T();} + return; + } + reserve(Count); + while(size() void assign(It First,It Last) + {clear();for(;First!=Last;++First)emplace_back(*First);} + void push_back(const T& Value){emplace_back(Value);} + void push_back(T&& Value){emplace_back(std::move(Value));} + template reference emplace_back(Args&&... ArgsIn) + { + if(bHeap){Heap.emplace_back(std::forward(ArgsIn)...);return Heap.back();} + if(InlineSize(InlineData()+InlineSize)) T(std::forward(ArgsIn)...); + ++InlineSize;return *Result; + } + reserve(std::max(InlineCount?InlineCount*2:1,InlineSize+1)); + Heap.emplace_back(std::forward(ArgsIn)...);return Heap.back(); + } + void pop_back(){if(bHeap)Heap.pop_back();else InlineData()[--InlineSize].~T();} + template iterator insert(iterator Position,It First,It Last) + { + const std::size_t Offset=static_cast(Position-begin()); + assert(Offset==size()); + for(;First!=Last;++First)emplace_back(*First); + return begin()+Offset; + } + iterator erase(iterator First,iterator Last) + { + const std::size_t Start=static_cast(First-begin()); + const std::size_t Finish=static_cast(Last-begin()); + const std::size_t Old=size(); + if(Finish<=Start)return begin()+Start; + const std::size_t Removed=Finish-Start; + for(std::size_t I=Finish;IOld-Removed)InlineData()[--InlineSize].~T(); + return begin()+Start; + } + +private: + T* InlineData(){return reinterpret_cast(InlineBytes);} + const T* InlineData()const{return reinterpret_cast(InlineBytes);} + void DestroyInline()noexcept{while(InlineSize>0)InlineData()[--InlineSize].~T();} + alignas(T) std::byte InlineBytes[(InlineCount?InlineCount:1)*sizeof(T)]{}; + std::vector Heap; + std::size_t InlineSize=0; + bool bHeap=false; +}; + template class TArray { public: using value_type = T; using FStored = std::conditional_t,uint8,T>; - using FStorage = std::vector; + using FStorage = FCompatSmallVector::Value,FCompatAllocator>; using iterator = typename FStorage::iterator; using const_iterator = typename FStorage::const_iterator; using reference = typename FStorage::reference; @@ -133,7 +314,7 @@ public: const T* Find(const T& Value) const { const int32 I=IndexOfByKey(Value); return I==INDEX_NONE?nullptr:&Values[I]; } T* GetData() { return reinterpret_cast(Values.data()); } const T* GetData() const { return reinterpret_cast(Values.data()); } - std::size_t GetAllocatedSize() const { return Values.capacity() * sizeof(T); } + std::size_t GetAllocatedSize() const { return Values.heap_capacity() * sizeof(T); } T& Last() { return Values.back(); } const T& Last() const { return Values.back(); } T Pop(bool = false) { T Out=std::move(Values.back()); Values.pop_back(); return Out; } @@ -151,42 +332,133 @@ private: template struct TPair { K Key{}; V Value{}; TPair()=default; TPair(const K& K0,const V& V0):Key(K0),Value(V0){}; }; +template +struct FCompatKeyHash +{ + std::size_t operator()(const K& Key) const noexcept + { + if constexpr (std::is_integral_v || std::is_enum_v) + return std::hash{}(Key); + else if constexpr (requires { GetTypeHash(Key); }) + return static_cast(GetTypeHash(Key)); + else + static_assert(sizeof(K) == 0, "Standalone TMap/TSet key type needs GetTypeHash"); + } +}; + template class TMap { public: int32 Num() const { return static_cast(Pairs.size()); } bool IsEmpty() const { return Pairs.empty(); } - void Reserve(int32 N) { Pairs.reserve(static_cast(std::max(0,N))); } - void Reset() { Pairs.clear(); } + void Reserve(int32 N) + { + const size_t Count=static_cast(std::max(0,N)); + Pairs.reserve(Count); + const size_t Needed=std::max(8,std::bit_ceil(std::max(1,(Count*10+6)/7))); + if(Buckets.size()Value; } - const V* Find(const K& Key) const { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; } - V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; Pairs.emplace_back(Key,V{}); return Pairs.back().Value; } - V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } Pairs.emplace_back(Key,Value); return Pairs.back().Value; } - V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } Pairs.emplace_back(Key,std::move(Value)); return Pairs.back().Value; } - int32 Remove(const K& Key) { auto I=FindPair(Key); if(I==Pairs.end())return 0; Pairs.erase(I);return 1; } + bool Contains(const K& Key) const { return FindIndex(Key)!=INDEX_NONE; } + V* Find(const K& Key) { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast(I)].Value; } + const V* Find(const K& Key) const { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast(I)].Value; } + V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; return Add(Key,V{}); } + V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } PrepareInsert();const int32 I=static_cast(Pairs.size());Pairs.emplace_back(Key,Value);InsertIndex(Key,I);return Pairs.back().Value; } + V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } PrepareInsert();const int32 I=static_cast(Pairs.size());Pairs.emplace_back(Key,std::move(Value));InsertIndex(Key,I);return Pairs.back().Value; } + int32 Remove(const K& Key) + { + const int32 I=FindIndex(Key);if(I==INDEX_NONE)return 0; + MarkRemoved(Key);const int32 Last=static_cast(Pairs.size()-1); + if(I!=Last){Pairs[static_cast(I)]=std::move(Pairs.back());UpdateIndex(Pairs[static_cast(I)].Key,I);} + Pairs.pop_back(); + if(Tombstones>Pairs.size()&&Tombstones>Buckets.size()/8)RebuildIndex(Buckets.size()); + return 1; + } V& operator[](const K& Key) { return FindOrAdd(Key); } const V& operator[](const K& Key) const { const V* P=Find(Key); assert(P); return *P; } auto begin(){return Pairs.begin();} auto end(){return Pairs.end();} auto begin()const{return Pairs.begin();} auto end()const{return Pairs.end();} private: - using It=typename std::vector>::iterator; - using CIt=typename std::vector>::const_iterator; - It FindPair(const K& Key){return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});} - CIt FindPair(const K& Key)const{return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});} - std::vector> Pairs; + struct FBucket { uint32 Generation=0;int32 Index=INDEX_NONE; }; + void PrepareInsert() + { + const size_t Count=Pairs.size()+1; + const size_t Needed=std::max(8,std::bit_ceil(std::max(1,(Count*10+6)/7))); + if(Buckets.size()Buckets.size()/8)RebuildIndex(std::max(Needed,Buckets.size())); + } + int32 FindIndex(const K& Key)const + { + if(Buckets.empty())return INDEX_NONE; + size_t Slot=FCompatKeyHash{}(Key)&(Buckets.size()-1); + for(size_t Probe=0;Probe=0&&Pairs[static_cast(B.Index)].Key==Key)return B.Index; + Slot=(Slot+1)&(Buckets.size()-1); + } + return INDEX_NONE; + } + void InsertIndex(const K& Key,int32 PairIndex) + { + size_t Slot=FCompatKeyHash{}(Key)&(Buckets.size()-1),FirstTombstone=Buckets.size(); + for(size_t Probe=0;Probe{}(Key)&(Buckets.size()-1); + for(size_t Probe=0;Probe=0&&Pairs[static_cast(B.Index)].Key==Key){B.Index=INDEX_NONE;++Tombstones;return;} + Slot=(Slot+1)&(Buckets.size()-1); + } + } + void UpdateIndex(const K& Key,int32 PairIndex) + { + size_t Slot=FCompatKeyHash{}(Key)&(Buckets.size()-1); + for(size_t Probe=0;Probe=0&&Pairs[static_cast(B.Index)].Key==Key){B.Index=PairIndex;return;} + Slot=(Slot+1)&(Buckets.size()-1); + } + } + void RebuildIndex(size_t Capacity) + { + Capacity=std::max(8,std::bit_ceil(Capacity));Buckets.assign(Capacity,FBucket{});Generation=1;Tombstones=0; + for(int32 I=0;I(Pairs.size());++I)InsertIndex(Pairs[static_cast(I)].Key,I); + } + std::vector,FCompatAllocator>> Pairs; + std::vector> Buckets; + uint32 Generation=1; + size_t Tombstones=0; }; template class TSet { public: - int32 Num()const{return Values.Num();} void Reset(){Values.Reset();} void Empty(){Reset();} - bool Contains(const T& V)const{return Values.Contains(V);} void Add(const T& V){Values.AddUnique(V);} void Add(T&& V){if(!Contains(V))Values.Add(std::move(V));} + int32 Num()const{return Values.Num();} void Reset(){Values.Reset();Indices.Reset();} void Empty(){Reset();} + void Reserve(int32 N){Values.Reserve(N);Indices.Reserve(N);} + bool Contains(const T& V)const{return Indices.Contains(V);} + void Add(const T& V){if(!Indices.Contains(V)){const int32 I=Values.Add(V);Indices.Add(V,I);}} + void Add(T&& V){if(!Indices.Contains(V)){const int32 I=Values.Add(std::move(V));Indices.Add(Values[I],I);}} auto begin(){return Values.begin();}auto end(){return Values.end();}auto begin()const{return Values.begin();}auto end()const{return Values.end();} -private:TArray Values; +private:TArray Values;TMap Indices; }; template class TUniquePtr @@ -225,6 +497,11 @@ public: static FString Join(const TArray& Parts, std::string_view Delimiter){FString Result;for(int32 I=0;I(std::min(N,sizeof(Buf)-1)));} }; +inline uint32 GetTypeHash(const FString& Value) +{ + const std::string_view View(Value.data(),Value.size()); + return static_cast(std::hash{}(View)); +} inline const char* operator*(const FString& S){return S.c_str();} using FName = FString; constexpr const char* NAME_None = ""; diff --git a/Standalone/SHARED_CPP_LIBRARY_PLAN.md b/Standalone/SHARED_CPP_LIBRARY_PLAN.md new file mode 100644 index 0000000..0eba9db --- /dev/null +++ b/Standalone/SHARED_CPP_LIBRARY_PLAN.md @@ -0,0 +1,30 @@ +# Shared C++ library plan + +## Library contents + +Move the deterministic CPU world pipeline into a portable `VoxelForgeCore` library: plain voxel +and world types, math/noise, density operations and caches, cave and passage fields, strate +composition, marching cubes, and desired clipmap tile selection. Keep renderer, file loading, +asset/reflection access, gameplay actors, and task scheduling in the host applications. + +## Compatibility boundary + +The library should expose standard C++ value types and explicit immutable world inputs. Unreal +adapters convert `UObject` settings/assets into those inputs and convert mesh results back to +Unreal types. The standalone loader maps its JSON snapshot into the same inputs. No Unreal headers, +reflection macros, `UObject`, or host allocator should cross the library boundary. + +## Plugin integration + +Build the core as one C++20 static library and link it from both the standalone viewer and the +VoxelForge Unreal module. The plugin keeps ownership of asset loading, worker admission, +cancellation, collision, game-thread mesh application, and per-frame upload budgets; workers call +the shared density and meshing API with immutable contexts. The standalone keeps its D3D11 renderer +and JSON loader and calls the same API. + +## Preventing drift before extraction + +Until the move lands, keep the standalone copies synchronized from the plugin source with a small +manifest of shared files and a checked sync script. The script should compare hashes and refuse an +ambiguous two-sided edit. Run the exported 3,436-sample bitwise parity corpus after every density +change, then remove the duplicate source only when both hosts build against the shared library. diff --git a/Standalone/Source/StandaloneViewer.cpp b/Standalone/Source/StandaloneViewer.cpp index 8a700a7..70602cf 100644 --- a/Standalone/Source/StandaloneViewer.cpp +++ b/Standalone/Source/StandaloneViewer.cpp @@ -1,5 +1,5 @@ #include "OwnerWorldLoader.h" -#include "WorldMesher.h" +#include "StreamingWorld.h" #include "StandaloneResearchTunnels.h" #ifdef TEXT @@ -25,6 +25,8 @@ #include #include #include +#include +#include #pragma comment(lib,"d3d11.lib") #pragma comment(lib,"dxgi.lib") @@ -70,19 +72,49 @@ namespace }; constexpr int32 MaxLevels=8; - struct FLevelGpu + 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(K.Level); + H=(H*0x9e3779b185ebca87ull)^static_cast(K.X); + H=(H*0x9e3779b185ebca87ull)^static_cast(K.Y); + H=(H*0x9e3779b185ebca87ull)^static_cast(K.Z); + return static_cast(H^(H>>32)); + } + }; + struct FResidentTileMesh + { + std::vector Vertices; + std::vector Indices; + uint64 Samples=0; + double WorkerSeconds=0.0; + }; + struct FStreamingGpuBatch { ID3D11Buffer* VertexBuffer=nullptr; ID3D11Buffer* IndexBuffer=nullptr; UINT IndexCount=0; uint64 VertexCount=0; - double Seconds=0.0; + }; + struct FCrossingWork + { uint64 Tiles=0; uint64 Samples=0; - bool Ready=false; + double WorkerSeconds=0.0; }; - struct FReadyLevel { int32 Level=0; FStandaloneCpuLevel Mesh; }; + 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 { @@ -98,22 +130,32 @@ namespace ID3D11Buffer* Constants=nullptr; ID3D11RasterizerState* SolidRasterizer=nullptr; ID3D11RasterizerState* WireRasterizer=nullptr; - FLevelGpu Levels[MaxLevels+1]; - std::unique_ptr World; + std::unique_ptr Streamer; std::string DataPath; - std::mutex ReadyMutex; - std::vector ReadyLevels; - std::thread GenerationThread; - std::atomic GenerationDone{false}; - std::atomic CancelGeneration{false}; - FStandaloneCpuClipmap LastClipmap; + std::unordered_map ResidentTiles; + std::unordered_map,FStandaloneBatchKeyHash> BatchMembers; + std::unordered_map StreamingBatches; + std::unordered_set DirtyBatchSet; + std::unordered_map CrossingWork; + std::vector DesiredTiles; + int32 DesiredByLevel[MaxLevels+1]{}; + uint64 GeneratedByLevel[MaxLevels+1]{}; + uint64 SamplesByLevel[MaxLevels+1]{}; + double WorkerSecondsByLevel[MaxLevels+1]{}; + std::vector 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; + 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; @@ -126,6 +168,8 @@ namespace uint64 TotalVertices=0; uint64 TotalIndices=0; FIntVector BuildTargetChunk=FIntVector::ZeroValue; + int32 MaxBatchUploadsPerFrame=4; + double BatchUploadBudgetMilliseconds=2.0; ~FApp(){ StopGeneration(true);ReleaseMeshes(); @@ -138,14 +182,23 @@ namespace void ReleaseMeshes() { - for(FLevelGpu& L:Levels){if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={};} + 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;TileSecondsByLevel[I].clear();} TotalVertices=0;TotalIndices=0; } void StopGeneration(bool bCancel=false) { - if(bCancel)CancelGeneration.store(true,std::memory_order_relaxed); - if(GenerationThread.joinable())GenerationThread.join(); + (void)bCancel; + if(Streamer)Streamer->Stop(); + Streamer.reset(); bGenerationRunning=false; } @@ -157,11 +210,7 @@ namespace bool NeedsRecenter(const FIntVector& Center) const { - const FIntVector Current=CameraChunk(); - const int32 Threshold=FMath::Max(1,(World?World->ClipRadius:6)/2); - return FMath::Abs(Current.X-Center.X)>=Threshold - ||FMath::Abs(Current.Y-Center.Y)>=Threshold - ||FMath::Abs(Current.Z-Center.Z)>=Threshold; + return CameraChunk()!=Center; } bool MakeBuffer(ID3D11Buffer** Out,const void* Data,size_t Bytes,UINT Bind) @@ -173,6 +222,83 @@ namespace return SUCCEEDED(Device->CreateBuffer(&Desc,&Initial,Out)); } + static void AppendPngU32(std::vector& Out,uint32 Value) + { + Out.push_back(static_cast(Value >> 24)); + Out.push_back(static_cast(Value >> 16)); + Out.push_back(static_cast(Value >> 8)); + Out.push_back(static_cast(Value)); + } + + static uint32 PngCrc32(const uint8* Data,size_t Count) + { + uint32 Crc=0xffffffffu; + for(size_t I=0;I>1)^((Crc&1)?0xedb88320u:0u); + } + return Crc^0xffffffffu; + } + + static void AppendPngChunk(std::vector& Png,const char Type[4],const std::vector& Data) + { + AppendPngU32(Png,static_cast(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(Desc.Width)*4; + std::vector Raw((RowBytes+1)*Desc.Height); + for(UINT Y=0;Y(Y)*(RowBytes+1); + Row[0]=0; + std::memcpy(Row+1,static_cast(Mapped.pData)+static_cast(Y)*Mapped.RowPitch,RowBytes); + } + Context->Unmap(Staging,0);Staging->Release(); + + std::vector 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(std::min(65535,Raw.size()-Offset)); + const bool bFinal=Offset+Block==Raw.size(); + Zlib.push_back(bFinal?1:0); + Zlib.push_back(static_cast(Block));Zlib.push_back(static_cast(Block>>8)); + const uint16 NotBlock=static_cast(~Block); + Zlib.push_back(static_cast(NotBlock));Zlib.push_back(static_cast(NotBlock>>8)); + Zlib.insert(Zlib.end(),Raw.begin()+Offset,Raw.begin()+Offset+Block); + Offset+=Block; + } + AppendPngU32(Zlib,(AdlerB<<16)|AdlerA); + + std::vector Png{137,80,78,71,13,10,26,10}; + std::vector 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(Png.data()),static_cast(Png.size())); + return File.good(); + } + bool Resize(UINT NewWidth,UINT NewHeight) { if(!Device||NewWidth==0||NewHeight==0)return false; @@ -300,69 +426,256 @@ float4 PS(O i):SV_TARGET{ {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); - ReadyLevels.clear();CancelGeneration.store(false,std::memory_order_relaxed); - GenerationDone.store(false,std::memory_order_relaxed);bGenerationRunning=true;bRebuildPending=false; - std::printf("Building seed %d%s full clipmap (six workers)...\n",Seed,bRoundTunnels?" with research round/winding tunnels":""); - GenerationThread=std::thread([this,CameraVoxels]() - { - LastClipmap=StandaloneWorldMesher::Build(*World,CameraVoxels,6, - [this](int32 Level,FStandaloneCpuLevel&& Mesh) - { - std::lock_guard Lock(ReadyMutex); - ReadyLevels.push_back({Level,std::move(Mesh)}); - },false,&CancelGeneration); - GenerationDone.store(true,std::memory_order_release); - }); + GenerationStart=std::chrono::steady_clock::now(); + bFullDetailReported=false;bInitialClipmapReported=false;bAutoCaptureWritten=false; + 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(*World,6); + const FStandaloneStreamingUpdate Update=Streamer->Start(CameraVoxels,{},CrossingIndex); + 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(Seed)+static_cast(Delta); Seed=static_cast(Next);bRebuildPending=true; - CancelGeneration.store(true,std::memory_order_relaxed); - std::printf("Seed changed to %d; rebuilding after the active clipmap finishes.\n",Seed); + std::printf("Seed changed to %d; replacing the current streaming world.\n",Seed); } - bool UploadLevel(int32 Index,FStandaloneCpuLevel&& Mesh) + void MarkBatchDirty(const FStandaloneBatchKey& Key) + {DirtyBatchSet.insert(Key);} + + static bool TileKeyLess(const FVoxelTileKey& A,const FVoxelTileKey& B) { - FLevelGpu& L=Levels[Index]; - if(L.VertexBuffer)L.VertexBuffer->Release();if(L.IndexBuffer)L.IndexBuffer->Release();L={}; - L.Seconds=Mesh.Seconds;L.Tiles=Mesh.TileCount;L.Samples=Mesh.DensitySamples; - L.VertexCount=Mesh.VertexCount;L.IndexCount=static_cast(Mesh.Indices.size()); - if(Mesh.Vertices.empty()||Mesh.Indices.empty()){L.Ready=true;return true;} - if(Mesh.Indices.size()>std::numeric_limits::max()) - {std::fprintf(stderr,"LOD %d has too many indices for one D3D11 draw.\n",Index);return false;} - const bool bVertex=MakeBuffer(&L.VertexBuffer,Mesh.Vertices.data(),Mesh.Vertices.size()*sizeof(FStandaloneCpuVertex),D3D11_BIND_VERTEX_BUFFER); - const bool bIndex=bVertex&&MakeBuffer(&L.IndexBuffer,Mesh.Indices.data(),Mesh.Indices.size()*sizeof(uint32),D3D11_BIND_INDEX_BUFFER); - if(!bIndex){if(L.VertexBuffer){L.VertexBuffer->Release();L.VertexBuffer=nullptr;}std::fprintf(stderr,"D3D11 buffer upload failed for LOD %d.\n",Index);return false;} - L.IndexCount=static_cast(Mesh.Indices.size());L.Ready=true; - TotalVertices+=L.VertexCount;TotalIndices+=L.IndexCount; - std::printf("LOD %d uploaded: %llu vertices, %.3f s, %llu tiles.\n",Index, - static_cast(L.VertexCount),L.Seconds,static_cast(L.Tiles)); + if(A.Level!=B.Level)return A.Levelsecond; + 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 NewSet; + NewSet.reserve(NewDesired.Tiles.size()*2+1); + for(const FVoxelTileKey& Key:NewDesired.Tiles)NewSet.insert(Key); + std::vector 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 ResidentKeys;ResidentKeys.reserve(ResidentTiles.size()); + for(const auto& Pair:ResidentTiles)ResidentKeys.push_back(Pair.first); + ++CrossingIndex; + const FStandaloneStreamingUpdate Update=Streamer->UpdateDesired(CameraVoxels,ResidentKeys,CrossingIndex); + 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(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); + } + + 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 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::max()||IndexCount>std::numeric_limits::max()) + {std::fprintf(stderr,"LOD %d batch exceeded D3D11 limits.\n",Key.Level);return false;} + std::vector Vertices;Vertices.reserve(VertexCount); + std::vector 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(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(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 Dirty(DirtyBatchSet.begin(),DirtyBatchSet.end()); + const FIntVector Center=Streamer?Streamer->CenterChunk():BuildTargetChunk; + auto Distance=[&](const FStandaloneBatchKey& K) + { + const double Scale=static_cast(1<=MaxBatchUploadsPerFrame)break; + if(Uploaded>0&&BatchUploadBudgetMilliseconds>0.0 + &&std::chrono::duration(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.LevelFullResClipLevels)++ResidentFullRes; + if(ResidentFullRes!=Streamer->FullResDesiredCount())return false; + for(const FStandaloneBatchKey& Key:DirtyBatchSet)if(Key.LevelFullResClipLevels)return false; + return true; + } + + void ReportTileTimings() + { + for(int32 L=0;L<=World->MaxClipLevel;++L) + { + if(GeneratedByLevel[L](DesiredByLevel[L])||DesiredByLevel[L]==0)continue; + std::vector& Times=TileSecondsByLevel[L];if(Times.empty())continue; + std::sort(Times.begin(),Times.end()); + const double Average=WorkerSecondsByLevel[L]/static_cast(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(GeneratedByLevel[L]),DesiredByLevel[L], + static_cast(SamplesByLevel[L])/1.0e6,WorkerSecondsByLevel[L],Average*1000.0,P50*1000.0,P95*1000.0); + Times.clear(); + } + } + void PumpGeneration() { - std::vector Ready; - {std::lock_guard Lock(ReadyMutex);Ready.swap(ReadyLevels);} - for(FReadyLevel& R:Ready)UploadLevel(R.Level,std::move(R.Mesh)); - if(bGenerationRunning&&GenerationDone.load(std::memory_order_acquire)) + if(bRebuildPending){StartGeneration();return;} + if(!Streamer)return; + std::vector Ready=Streamer->TakeReady(); + for(FStandaloneCpuTile& Tile:Ready) { - StopGeneration(); - if(!LastClipmap.bCancelled) - std::printf("Full clipmap ready: %.3f s on %d workers; %llu vertices, %llu indices.\n", - LastClipmap.TotalSeconds,LastClipmap.WorkerCount, - static_cast(TotalVertices),static_cast(TotalIndices)); - for(int32 L=0;L<=World->MaxClipLevel;++L) - std::printf(" LOD %d: %.3f s, %llu tiles, %llu vertices, %.2f M density samples\n",L, - LastClipmap.Levels[L].Seconds,static_cast(LastClipmap.Levels[L].TileCount), - static_cast(LastClipmap.Levels[L].VertexCount), - static_cast(LastClipmap.Levels[L].DensitySamples)/1.0e6); - std::fflush(stdout); - if(bRebuildPending)StartGeneration(); + if(!Streamer->IsDesired(Tile.Key)||ResidentTiles.contains(Tile.Key))continue; + const int32 Level=Tile.Key.Level; + FCrossingWork& Crossing=CrossingWork[Tile.RequestedCrossing]; + ++Crossing.Tiles;Crossing.Samples+=Tile.DensitySamples;Crossing.WorkerSeconds+=Tile.WorkerSeconds; + ++GeneratedByLevel[Level];SamplesByLevel[Level]+=Tile.DensitySamples;WorkerSecondsByLevel[Level]+=Tile.WorkerSeconds; + 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); } - else if(!bGenerationRunning&&bRebuildPending)StartGeneration(); + UploadDirtyBatches(); + if(!bFullDetailReported&&IsFullDetailUploaded()) + { + bFullDetailReported=true; + const double Elapsed=std::chrono::duration(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(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(TotalVertices), + static_cast(TotalIndices),StreamingBatches.size()); + ReportTileTimings();std::fflush(stdout); + } + bGenerationRunning=ResidentTiles.size()!=DesiredTiles.size()||!DirtyBatchSet.empty(); + } + + void CaptureRenderedFrame() + { + if(CapturePath.empty()||bAutoCaptureWritten||!IsFullDetailUploaded())return; + bAutoCaptureWritten=SaveBackBufferPng(CapturePath); + if(bAutoCaptureWritten) + { + const double Elapsed=std::chrono::duration(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) @@ -374,7 +687,7 @@ float4 PS(O i):SV_TARGET{ void UpdateTitle() { - uint64 VisibleIndices=0;for(int32 I=0;I<=MaxLevels;++I)if(bShowLevel[I])VisibleIndices+=Levels[I].IndexCount; + uint64 VisibleIndices=0;for(const auto& Pair:StreamingBatches)if(bShowLevel[Pair.first.Level])VisibleIndices+=Pair.second.IndexCount; std::ostringstream S;S<<"VoxelForge standalone | seed "<OMSetRenderTargets(1,&RenderTarget,DepthView);Context->ClearRenderTargetView(RenderTarget,Clear); Context->ClearDepthStencilView(DepthView,D3D11_CLEAR_DEPTH,1.0f,0); @@ -409,12 +719,14 @@ float4 PS(O i):SV_TARGET{ 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(int32 I=0;I<=MaxLevels;++I) + for(const auto& Pair:StreamingBatches) { - const FLevelGpu& L=Levels[I];if(!bShowLevel[I]||!L.VertexBuffer||!L.IndexBuffer||!L.IndexCount)continue; + 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(); } }; @@ -431,7 +743,7 @@ float4 PS(O i):SV_TARGET{ 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;GApp->CancelGeneration.store(true,std::memory_order_relaxed);} + 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(W-'1');GApp->bShowLevel[I]=!GApp->bShowLevel[I];} if(!bWasDown&&W=='9')GApp->bShowLevel[8]=!GApp->bShowLevel[8]; @@ -444,7 +756,7 @@ float4 PS(O i):SV_TARGET{ 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:GApp->CancelGeneration.store(true,std::memory_order_relaxed);PostQuitMessage(0);return 0; + case WM_DESTROY:if(GApp->Streamer)GApp->Streamer->Stop();PostQuitMessage(0);return 0; } return DefWindowProc(H,Message,W,L); } @@ -459,7 +771,13 @@ float4 PS(O i):SV_TARGET{ int main(int Argc,char** Argv) { - FApp App;App.DataPath=Argc>1?Argv[1]:DefaultDataPath().string(); + FApp App;App.DataPath=DefaultDataPath().string(); + for(int32 I=1;I +#include + +struct FStandaloneTileKeyHash +{ + std::size_t operator()(const FVoxelTileKey& Key) const noexcept + { return static_cast(GetTypeHash(Key)); } +}; + +struct FStandaloneDesiredTileSet +{ + std::vector Tiles; + FIntVector CenterChunk=FIntVector::ZeroValue; + int32 MinimumChunkZ=MIN_int32; + int32 MaximumChunkZ=MAX_int32; + int32 StrateTopChunkZ=0; + int32 StrateBottomChunkZ=0; + bool bHasStrate=false; +}; + +struct FStandaloneCpuTile +{ + FVoxelTileKey Key; + uint64 RequestedCrossing=0; + std::vector Vertices; + std::vector Indices; + uint64 DensitySamples=0; + double WorkerSeconds=0.0; +}; + +struct FStandaloneStreamingUpdate +{ + FIntVector CenterChunk=FIntVector::ZeroValue; + int32 DesiredTiles=0; + int32 RetainedResidentTiles=0; + int32 NewJobs=0; + int32 CancelledQueuedJobs=0; + int32 FullResTiles=0; +}; + +struct FStandaloneTileJob +{ + FVoxelTileKey Key; + int32 BandZMin=MIN_int32; + int32 BandZMax=MAX_int32; + uint64 RequestedCrossing=0; +}; + +namespace StandaloneWorldMesher +{ + inline double TileDistanceSquaredChunks(const FVoxelTileKey& Tile,const FIntVector& Center) + { + const double Scale=static_cast(1<(Tile.Coord.X)+0.5)*Scale-Center.X; + const double DY=(static_cast(Tile.Coord.Y)+0.5)*Scale-Center.Y; + const double DZ=(static_cast(Tile.Coord.Z)+0.5)*Scale-Center.Z; + return DX*DX+DY*DY+DZ*DZ; + } + + inline FStandaloneDesiredTileSet BuildDesiredSet( + const FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels) + { + FStandaloneDesiredTileSet Desired; + Desired.CenterChunk=CameraChunk(CameraPositionVoxels); + if(World.Settings->bClampViewToStrate) + Desired.bHasStrate=World.Manager->GetStrateChunkZBounds( + Desired.CenterChunk.Z,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ); + VoxelClipmapDesiredTiles::ResolveVerticalBounds( + Desired.CenterChunk.Z,World.Settings->ViewDistanceUp,World.Settings->ViewDistanceDown, + World.Settings->StrateViewMarginChunks,World.Settings->bClampViewToStrate, + Desired.bHasStrate,Desired.StrateTopChunkZ,Desired.StrateBottomChunkZ, + Desired.MinimumChunkZ,Desired.MaximumChunkZ); + VoxelClipmapDesiredTiles::FParameters Parameters; + Parameters.ClipRadius=World.ClipRadius; + Parameters.MaxClipLevel=FMath::Clamp(World.MaxClipLevel,0,8); + Parameters.RenderDistanceChunks=World.RenderDistanceChunks; + TArray Tiles; + VoxelClipmapDesiredTiles::Build(Desired.CenterChunk,Desired.MinimumChunkZ, + Desired.MaximumChunkZ,Parameters,Tiles); + Desired.Tiles.reserve(static_cast(Tiles.Num())); + for(const FVoxelTileKey& Tile:Tiles)Desired.Tiles.push_back(Tile); + std::sort(Desired.Tiles.begin(),Desired.Tiles.end(),[&](const FVoxelTileKey& A,const FVoxelTileKey& B) + { + const double DA=TileDistanceSquaredChunks(A,Desired.CenterChunk); + const double DB=TileDistanceSquaredChunks(B,Desired.CenterChunk); + return DA==DB?A.Level& ResidentKeys={}, + uint64 CrossingIndex=0) + { + Stop(); + { + std::lock_guard Lock(Mutex); + bStop=false; + } + for(int32 I=0;I Lock(Mutex); + bStop=true; + Queue.clear(); + } + Wake.notify_all(); + for(std::thread& Worker:Workers)if(Worker.joinable())Worker.join(); + Workers.clear(); + std::lock_guard Lock(Mutex); + Ready.clear();Outstanding.clear();Desired.clear(); + } + + FStandaloneStreamingUpdate UpdateDesired( + const FVector& CameraPositionVoxels,const std::vector& ResidentKeys, + uint64 CrossingIndex=0) + { + FStandaloneDesiredTileSet NewDesired=StandaloneWorldMesher::BuildDesiredSet(*World,CameraPositionVoxels); + FStandaloneStreamingUpdate Stats; + Stats.CenterChunk=NewDesired.CenterChunk; + Stats.DesiredTiles=static_cast(NewDesired.Tiles.size()); + std::unordered_set Resident; + Resident.reserve(ResidentKeys.size()*2+1); + for(const FVoxelTileKey& Key:ResidentKeys)Resident.insert(Key); + for(const FVoxelTileKey& Key:NewDesired.Tiles) + { + if(Key.LevelFullResClipLevels)++Stats.FullResTiles; + if(Resident.contains(Key))++Stats.RetainedResidentTiles; + } + + std::lock_guard Lock(Mutex); + Desired.clear();Desired.reserve(NewDesired.Tiles.size()*2+1); + for(const FVoxelTileKey& Key:NewDesired.Tiles)Desired.insert(Key); + FullResTileCount=Stats.FullResTiles; + auto QueueEnd=std::remove_if(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& Job) + { + if(Desired.contains(Job.Key))return false; + Outstanding.erase(Job.Key);++Stats.CancelledQueuedJobs;return true; + }); + Queue.erase(QueueEnd,Queue.end()); + + const int32 BandMin=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate + ? NewDesired.StrateBottomChunkZ*CHUNK_SIZE:MIN_int32; + const int32 BandMax=World->StrateContentCutMinLevel<9&&NewDesired.bHasStrate + ? (NewDesired.StrateTopChunkZ+1)*CHUNK_SIZE:MAX_int32; + for(const FVoxelTileKey& Key:NewDesired.Tiles) + { + if(Resident.contains(Key)||Outstanding.contains(Key))continue; + Queue.push_back({Key,BandMin,BandMax,CrossingIndex}); + Outstanding.insert(Key);++Stats.NewJobs; + } + CurrentDesired=std::move(NewDesired); + std::sort(Queue.begin(),Queue.end(),[&](const FStandaloneTileJob& A,const FStandaloneTileJob& B) + { + const double DA=StandaloneWorldMesher::TileDistanceSquaredChunks(A.Key,CurrentDesired.CenterChunk); + const double DB=StandaloneWorldMesher::TileDistanceSquaredChunks(B.Key,CurrentDesired.CenterChunk); + return DA==DB?A.Key.Level TakeReady() + { + std::vector Out; + std::lock_guard Lock(Mutex); + Out.swap(Ready); + for(const FStandaloneCpuTile& Tile:Out)Outstanding.erase(Tile.Key); + return Out; + } + + bool IsDesired(const FVoxelTileKey& Key)const + {std::lock_guard Lock(Mutex);return Desired.contains(Key);} + int32 DesiredCount()const + {std::lock_guard Lock(Mutex);return static_cast(Desired.size());} + int32 FullResDesiredCount()const + {std::lock_guard Lock(Mutex);return FullResTileCount;} + FIntVector CenterChunk()const + {std::lock_guard Lock(Mutex);return CurrentDesired.CenterChunk;} + int32 GetWorkerCount()const{return WorkerCount;} + +private: + FStandaloneCpuTile Generate(const FStandaloneTileJob& Job) + { + const auto StartTime=FClock::now(); + FStandaloneCpuTile Out;Out.Key=Job.Key;Out.RequestedCrossing=Job.RequestedCrossing; + UVoxelMarchingCubesMesher Mesher; + Mesher.SetGenerator(World->Generator.get()); + Mesher.bGenerateSkirts=World->bGenerateSkirts; + Mesher.SkirtCells=static_cast(World->SkirtCells); + Mesher.LODOctaveDrop=World->LODOctaveDrop; + const int32 Level=Job.Key.Level; + const int32 Extent=Job.Key.ExtentVoxels(); + const int32 Cells=LevelFullResClipLevels?CHUNK_SIZE:FMath::Max(1,World->CoarseTileCells); + const int32 Step=FMath::Max(1,Extent/Cells); + int64 SampleCount=0; + FVoxelMeshData Mesh=Mesher.GenerateMesh(Job.Key.OriginVoxels(),Step,Cells,nullptr, + Job.BandZMin,Job.BandZMax,&SampleCount); + Out.DensitySamples=static_cast(FMath::Max(0,SampleCount)); + Out.Vertices.reserve(static_cast(Mesh.Vertices.Num())); + for(int32 I=0;I(P.X*0.01),static_cast(P.Y*0.01),static_cast(P.Z*0.01)}, + {static_cast(N.X),static_cast(N.Y),static_cast(N.Z)}}); + } + Out.Indices.reserve(static_cast(Mesh.Triangles.Num())); + for(const int32 Index:Mesh.Triangles)Out.Indices.push_back(static_cast(Index)); + Out.WorkerSeconds=std::chrono::duration(FClock::now()-StartTime).count(); + return Out; + } + + void WorkerLoop() + { + for(;;) + { + FStandaloneTileJob Job; + { + std::unique_lock Lock(Mutex); + Wake.wait(Lock,[&](){return bStop||!Queue.empty();}); + if(bStop)return; + Job=Queue.front();Queue.erase(Queue.begin()); + } + FStandaloneCpuTile Tile=Generate(Job); + { + std::lock_guard Lock(Mutex); + if(bStop)return; + Ready.push_back(std::move(Tile)); + } + } + } + + using FClock=std::chrono::steady_clock; + FStandaloneOwnerWorld* World=nullptr; + int32 WorkerCount=6; + mutable std::mutex Mutex; + std::condition_variable Wake; + bool bStop=true; + std::vector Workers; + std::vector Queue; + std::vector Ready; + std::unordered_set Desired; + std::unordered_set Outstanding; + FStandaloneDesiredTileSet CurrentDesired; + int32 FullResTileCount=0; +}; diff --git a/Standalone/data/owner_normals_before.png b/Standalone/data/owner_normals_before.png new file mode 100644 index 0000000..091a080 Binary files /dev/null and b/Standalone/data/owner_normals_before.png differ diff --git a/Standalone/data/owner_normals_pool_before.png b/Standalone/data/owner_normals_pool_before.png new file mode 100644 index 0000000..091a080 Binary files /dev/null and b/Standalone/data/owner_normals_pool_before.png differ diff --git a/Standalone/data/owner_view_striping.png b/Standalone/data/owner_view_striping.png new file mode 100644 index 0000000..8567ff7 Binary files /dev/null and b/Standalone/data/owner_view_striping.png differ